diff --git a/mpv/input.conf b/mpv/input.conf index bf92724..267d272 100644 --- a/mpv/input.conf +++ b/mpv/input.conf @@ -197,3 +197,7 @@ CTRL+5 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights. CTRL+6 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Upscale_Denoise_CNN_x2_VL.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Restore_CNN_M.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode C+A (HQ)" CTRL+0 no-osd change-list glsl-shaders clr ""; show-text "GLSL shaders cleared" + +TAB script-binding skip-key + +o write-watch-later-config; loadfile "${path}" diff --git a/mpv/mpv.conf b/mpv/mpv.conf index a3068fe..9e6b605 100644 --- a/mpv/mpv.conf +++ b/mpv/mpv.conf @@ -33,7 +33,7 @@ #fs=yes # force starting with centered window -#geometry=50%:50% +# geometry=50%:50% # don't allow a new window to have a size larger than 90% of the screen size #autofit-larger=90%x90% @@ -51,6 +51,7 @@ # Keep the player window on top of all other windows. #ontop=yes +# window=scale=1.0 # Specify high quality video rendering preset (for --vo=gpu only) # Can cause performance problems with some drivers and GPUs. @@ -94,7 +95,7 @@ profile=svp # Pretend to be a web browser. Might fix playback with some streaming sites, # but also will break with shoutcast streams. -#user-agent="Mozilla/5.0" +user-agent="Mozilla/5.0" # cache settings # diff --git a/mpv/scripts/discord.lua b/mpv/scripts/discord.lua new file mode 100644 index 0000000..240d454 --- /dev/null +++ b/mpv/scripts/discord.lua @@ -0,0 +1,106 @@ +msg = require("mp.msg") +opts = require("mp.options") +utils = require("mp.utils") + +options = { + key = "D", + active = true, + client_id = "737663962677510245", + binary_path = "", + socket_path = "/tmp/mpvsocket", + use_static_socket_path = true, + autohide_threshold = 0 +} +opts.read_options(options, "discord") + +if options.binary_path == "" then + msg.fatal("Missing binary path in config file.") + os.exit(1) +end + +function file_exists(path) -- fix(#23): use this instead of utils.file_info + local f = io.open(path, "r") + if f ~= nil then io.close(f) return true else return false end +end + +if not file_exists(options.binary_path) then + msg.fatal("The specified binary path does not exist.") + os.exit(1) +end + +version = "1.6.0" +msg.info(("mpv-discord v%s by tnychn"):format(version)) + +socket_path = options.socket_path +if not options.use_static_socket_path then + pid = utils.getpid() + filename = ("mpv-discord-%s"):format(pid) + if socket_path == "" then + socket_path = "/tmp/" -- default + end + socket_path = utils.join_path(socket_path, filename) +elseif socket_path == "" then + msg.fatal("Missing socket path in config file.") + os.exit(1) +end +msg.info(("(mpv-ipc): %s"):format(socket_path)) +mp.set_property("input-ipc-server", socket_path) + +cmd = nil + +function start() + if cmd == nil then + cmd = mp.command_native_async({ + name = "subprocess", + playback_only = false, + args = { + options.binary_path, + socket_path, + options.client_id + } + }, function() end) + msg.info("launched subprocess") + mp.osd_message("Discord Rich Presence: Started") + end +end + +function stop() + mp.abort_async_command(cmd) + cmd = nil + msg.info("aborted subprocess") + mp.osd_message("Discord Rich Presence: Stopped") +end + +if options.active then + mp.register_event("file-loaded", start) +end + +mp.add_key_binding(options.key, "toggle-discord", function() + if cmd ~= nil then stop() + else start() end +end) + +mp.register_event("shutdown", function() + if cmd ~= nil then stop() end + if not options.use_static_socket_path then + os.remove(socket_path) + end +end) + +if options.autohide_threshold > 0 then + local timer = nil + local t = options.autohide_threshold + mp.observe_property("pause", "bool", function(_, value) + if value == true then + timer = mp.add_timeout(t, function() + if cmd ~= nil then stop() end + end) + else + if timer ~= nil then + timer:kill() + timer = nil + end + if options.active and cmd == nil then start() end + end + end) +end diff --git a/mpv/scripts/pause-indicator.lua b/mpv/scripts/pause-indicator.lua new file mode 100644 index 0000000..8d96ac5 --- /dev/null +++ b/mpv/scripts/pause-indicator.lua @@ -0,0 +1,1025 @@ +local msg = require('mp.msg') +local log = { + debug = function(format, ...) + return msg.debug(format:format(...)) + end, + info = function(format, ...) + return msg.info(format:format(...)) + end, + warn = function(format, ...) + return msg.warn(format:format(...)) + end, + dump = function(item, ignore) + local level = 2 + if "table" ~= type(item) then + msg.info(tostring(item)) + return + end + local count = 1 + local tablecount = 1 + local result = { + "{ @" .. tostring(tablecount) + } + local seen = { + [item] = tablecount + } + local recurse + recurse = function(item, space) + for key, value in pairs(item) do + if not (key == ignore) then + if "table" == type(value) then + if not (seen[value]) then + tablecount = tablecount + 1 + seen[value] = tablecount + count = count + 1 + result[count] = space .. tostring(key) .. ": { @" .. tostring(tablecount) + recurse(value, space .. " ") + count = count + 1 + result[count] = space .. "}" + else + count = count + 1 + result[count] = space .. tostring(key) .. ": @" .. tostring(seen[value]) + end + else + if "string" == type(value) then + value = ("%q"):format(value) + end + count = count + 1 + result[count] = space .. tostring(key) .. ": " .. tostring(value) + end + end + end + end + recurse(item, " ") + count = count + 1 + result[count] = "}" + return msg.info(table.concat(result, "\n")) + end +} +local options = require('mp.options') +local utils = require('mp.utils') +local script_name = 'torque-progressbar' +mp.get_osd_size = mp.get_osd_size or mp.get_screen_size +local settings = { + __defaults = { } +} +local settingsMeta = { + __reload = function(self) + for key, value in pairs(self.__defaults) do + settings[key] = value + end + options.read_options(self, script_name .. '/main') + if self['bar-height-inactive'] <= 0 then + self['bar-hide-inactive'] = true + self['bar-height-inactive'] = 1 + end + end, + __migrate = function(self) + local oldConfig = mp.find_config_file(('lua-settings/%s.conf'):format(script_name)) + local newConfigFile = ('lua-settings/%s/main.conf'):format(script_name) + local newConfig = mp.find_config_file(newConfigFile) + if oldConfig and not newConfig then + local folder, _ = utils.split_path(oldConfig) + local configDir = utils.join_path(folder, script_name) + newConfig = utils.join_path(configDir, 'main.conf') + log.info(('Old configuration detected. Attempting to migrate %q -> %q'):format(oldConfig, newConfig)) + local dirExists = mp.find_config_file(configDir) + if dirExists and not utils.readdir(configDir) then + log.warn(('Configuration migration failed. %q exists and does not appear to be a folder'):format(configDir)) + return + else + if not dirExists then + local res = utils.subprocess({ + args = { + 'mkdir', + configDir + } + }) + if res.error or res.status ~= 0 then + log.warn(('Making directory %q failed.'):format(configDir)) + return + end + end + end + local res = utils.subprocess({ + args = { + 'mv', + oldConfig, + newConfig + } + }) + if res.error or res.status ~= 0 then + log.warn(('Moving file %q -> %q failed.'):format(oldConfig, newConfig)) + return + end + if mp.find_config_file(newConfigFile) then + return log.info('Configuration successfully migrated.') + end + end + end, + __newindex = function(self, key, value) + self.__defaults[key] = value + return rawset(self, key, value) + end +} +settingsMeta.__index = settingsMeta +setmetatable(settings, settingsMeta) +settings:__migrate() +local helpText = { } +settings['hover-zone-height'] = 40 +helpText['hover-zone-height'] = [[Sets the height of the rectangular area at the bottom of the screen that expands +the progress bar and shows playback time information when the mouse is hovered +over it. +]] +settings['top-hover-zone-height'] = 40 +helpText['top-hover-zone-height'] = [[Sets the height of the rectangular area at the top of the screen that shows the +file name and system time when the mouse is hovered over it. +]] +settings['display-scale-factor'] = 1 +helpText['display-scale-factor'] = [[Acts as a multiplier to increase the size of every UI element. Useful for high- +DPI displays that cause the UI to be rendered too small (happens at least on +macOS). +]] +settings['default-style'] = [[\fnSource Sans Pro\b1\bord2\shad0\fs30\c&HFC799E&\3c&H2D2D2D&]] +helpText['default-style'] = [[Default style that is applied to all UI elements. A string of ASS override tags. +Individual elements have their own style settings which override the tags here. +Changing the font will likely require changing the hover-time margin settings +and the offscreen-pos settings. + +Here are some useful ASS override tags (omit square brackets): +\fn[Font Name]: sets the font to the named font. +\fs[number]: sets the font size to the given number. +\b[1/0]: sets the text bold or not (\b1 is bold, \b0 is regular weight). +\i[1/0]: sets the text italic or not (same semantics as bold). +\bord[number]: sets the outline width to the given number (in pixels). +\shad[number]: sets the shadow size to the given number (pixels). +\c&H[BBGGRR]&: sets the fill color for the text to the given color (hex pairs in + the order, blue, green, red). +\3c&H[BBGGRR]&: sets the outline color of the text to the given color. +\4c&H[BBGGRR]&: sets the shadow color of the text to the given color. +\alpha&H[AA]&: sets the line's transparency as a hex pair. 00 is fully opaque + and FF is fully transparent. Some UI elements are composed of + multiple layered lines, so adding transparency may not look good. + For further granularity, \1a&H[AA]& controls the fill opacity, + \3a&H[AA]& controls the outline opacity, and \4a&H[AA]& controls + the shadow opacity. +]] +settings['enable-bar'] = true +helpText['enable-bar'] = [[Controls whether or not the progress bar is drawn at all. If this is disabled, +it also (naturally) disables the click-to-seek functionality. +]] +settings['bar-hide-inactive'] = false +helpText['bar-hide-inactive'] = [[Causes the bar to not be drawn unless the mouse is hovering over it or a +request-display call is active. This is somewhat redundant with setting bar- +height-inactive=0, except that it can allow for very rudimentary context- +sensitive behavior because it can be toggled at runtime. For example, by using +the binding `f cycle pause; script-binding progressbar/toggle-inactive-bar`, it +is possible to have the bar be persistently present only in windowed or +fullscreen contexts, depending on the default setting. +]] +settings['bar-height-inactive'] = 2 +helpText['bar-height-inactive'] = [[Sets the height of the bar display when the mouse is not in the active zone and +there is no request-display active. A value of 0 or less will cause bar-hide- +inactive to be set to true and the bar height to be set to 1. This should result +in the desired behavior while avoiding annoying debug logging in mpv (libass +does not like zero-height objects). +]] +settings['bar-height-active'] = 8 +helpText['bar-height-active'] = [[Sets the height of the bar display when the mouse is in the active zone or +request-display is active. There is no logic attached to this, so 0 or negative +values may have unexpected results. +]] +settings['progress-bar-width'] = 0 +helpText['progress-bar-width'] = [[If greater than zero, changes the progress bar style to be a small segment +rather than a continuous bar and sets its width. +]] +settings['seek-precision'] = 'exact' +helpText['seek-precision'] = [[Affects precision of seeks due to clicks on the progress bar. Should be 'exact' or +'keyframes'. Exact is slightly slower, but won't jump around between two +different times when clicking in the same place. + +Actually, this gets passed directly into the `seek` command, so the value can be +any of the arguments supported by mpv, though the ones above are the only ones +that really make sense. +]] +settings['bar-default-style'] = [[\bord0\shad0]] +helpText['bar-default-style'] = [[A string of ASS override tags that get applied to all three layers of the bar: +progress, cache, and background. You probably don't want to remove \bord0 unless +your default-style includes it. +]] +settings['bar-foreground-style'] = '' +helpText['bar-foreground-style'] = [[A string of ASS override tags that get applied only to the progress layer of the +bar. +]] +settings['bar-cache-style'] = [[\c&H515151&]] +helpText['bar-cache-style'] = [[A string of ASS override tags that get applied only to the cache layer of the +bar. The default sets only the color. +]] +settings['bar-background-style'] = [[\c&H2D2D2D&]] +helpText['bar-background-style'] = [[A string of ASS override tags that get applied only to the background layer of +the bar. The default sets only the color. +]] +settings['enable-elapsed-time'] = true +helpText['enable-elapsed-time'] = [[Sets whether or not the elapsed time is displayed at all. +]] +settings['elapsed-style'] = '' +helpText['elapsed-style'] = [[A string of ASS override tags that get applied only to the elapsed time display. +]] +settings['elapsed-left-margin'] = 4 +helpText['elapsed-left-margin'] = [[Controls how far from the left edge of the window the elapsed time display is +positioned. +]] +settings['elapsed-bottom-margin'] = 0 +helpText['elapsed-bottom-margin'] = [[Controls how far above the expanded progress bar the elapsed time display is +positioned. +]] +settings['enable-remaining-time'] = true +helpText['enable-remaining-time'] = [[Sets whether or not the remaining time is displayed at all. +]] +settings['remaining-style'] = '' +helpText['remaining-style'] = [[A string of ASS override tags that get applied only to the remaining time +display. +]] +settings['remaining-right-margin'] = 4 +helpText['remaining-right-margin'] = [[Controls how far from the right edge of the window the remaining time display is +positioned. +]] +settings['remaining-bottom-margin'] = 0 +helpText['remaining-bottom-margin'] = [[Controls how far above the expanded progress bar the remaining time display is +positioned. +]] +settings['enable-hover-time'] = true +helpText['enable-hover-time'] = [[Sets whether or not the calculated time corresponding to the mouse position +is displayed when the mouse hovers over the progress bar. +]] +settings['hover-time-style'] = [[\fs26]] +helpText['hover-time-style'] = [[A string of ASS override tags that get applied only to the hover time display. +Unfortunately, due to the way the hover time display is animated, alpha values +set here will be overridden. This is subject to change in future versions. +]] +settings['hover-time-left-margin'] = 120 +helpText['hover-time-left-margin'] = [[Controls how close to the left edge of the window the hover time display can +get. If this value is too small, it will end up overlapping the elapsed time +display. +]] +settings['hover-time-right-margin'] = 130 +helpText['hover-time-right-margin'] = [[Controls how close to the right edge of the window the hover time display can +get. If this value is too small, it will end up overlapping the remaining time +display. +]] +settings['hover-time-bottom-margin'] = 0 +helpText['hover-time-bottom-margin'] = [[Controls how far above the expanded progress bar the remaining time display is +positioned. +]] +settings['enable-title'] = true +helpText['enable-title'] = [[Sets whether or not the video title is displayed at all. +]] +settings['title-style'] = '' +helpText['title-style'] = [[A string of ASS override tags that get applied only to the video title display. +]] +settings['title-left-margin'] = 4 +helpText['title-left-margin'] = [[Controls how far from the left edge of the window the video title display is +positioned. +]] +settings['title-top-margin'] = 0 +helpText['title-top-margin'] = [[Controls how far from the top edge of the window the video title display is +positioned. +]] +settings['title-print-to-cli'] = true +helpText['title-print-to-cli'] = [[Controls whether or not the script logs the video title and playlist position +to the console every time a new video starts. +]] +settings['enable-system-time'] = true +helpText['enable-system-time'] = [[Sets whether or not the system time is displayed at all. +]] +settings['system-time-style'] = '' +helpText['system-time-style'] = [[A string of ASS override tags that get applied only to the system time display. +]] +settings['system-time-format'] = '%H:%M' +helpText['system-time-format'] = [[Sets the format used for the system time display. This must be a strftime- +compatible format string. +]] +settings['system-time-right-margin'] = 4 +helpText['system-time-right-margin'] = [[Controls how far from the right edge of the window the system time display is +positioned. +]] +settings['system-time-top-margin'] = 0 +helpText['system-time-top-margin'] = [[Controls how far from the top edge of the window the system time display is +positioned. +]] +settings['pause-indicator'] = true +helpText['pause-indicator'] = [[Sets whether or not the pause indicator is displayed. The pause indicator is a +momentary icon that flashes in the middle of the screen, similar to youtube. +]] +settings['pause-indicator-foreground-style'] = [[\c&HFC799E&]] +helpText['pause-indicator-foreground-style'] = [[A string of ASS override tags that get applied only to the foreground of the +pause indicator. +]] +settings['pause-indicator-background-style'] = [[\c&H2D2D2D&]] +helpText['pause-indicator-background-style'] = [[A string of ASS override tags that get applied only to the background of the +pause indicator. +]] +settings['enable-chapter-markers'] = true +helpText['enable-chapter-markers'] = [[Sets whether or not the progress bar is decorated with chapter markers. Due to +the way the chapter markers are currently implemented, videos with a large +number of chapters may slow down the script somewhat, but I have yet to run +into this being a problem. +]] +settings['chapter-marker-width'] = 2 +helpText['chapter-marker-width'] = [[Controls the width of each chapter marker when the progress bar is inactive. +]] +settings['chapter-marker-width-active'] = 4 +helpText['chapter-marker-width-active'] = [[Controls the width of each chapter marker when the progress bar is active. +]] +settings['chapter-marker-active-height-fraction'] = 1 +helpText['chapter-marker-active-height-fraction'] = [[Modifies the height of the chapter markers when the progress bar is active. Acts +as a multiplier on the height of the active progress bar. A value greater than 1 +will cause the markers to be taller than the expanded progress bar, whereas a +value less than 1 will cause them to be shorter. +]] +settings['chapter-marker-before-style'] = [[\c&HFC799E&]] +helpText['chapter-marker-before-style'] = [[A string of ASS override tags that get applied only to chapter markers that have +not yet been passed. +]] +settings['chapter-marker-after-style'] = [[\c&H2D2D2D&]] +helpText['chapter-marker-after-style'] = [[A string of ASS override tags that get applied only to chapter markers that have +already been passed. +]] +settings['request-display-duration'] = 1 +helpText['request-display-duration'] = [[Sets the amount of time in seconds that the UI stays on the screen after it +receives a request-display signal. A value of 0 will keep the display on screen +only as long as the key bound to it is held down. +]] +settings['redraw-period'] = 0.03 +helpText['redraw-period'] = [[Controls how often the display is redrawn, in seconds. This does not seem to +significantly affect the smoothness of animations, and it is subject to the +accuracy limits imposed by the scheduler mpv uses. Probably not worth changing +unless you have major performance problems. +]] +settings['animation-duration'] = 0.25 +helpText['animation-duration'] = [[Controls how long the UI animations take. A value of 0 disables all animations +(which breaks the pause indicator). +]] +settings['elapsed-offscreen-pos'] = -100 +helpText['elapsed-offscreen-pos'] = [[Controls how far off the left side of the window the elapsed time display tries +to move when it is inactive. If you use a non-default font, this value may need +to be tweaked. If this value is not far enough off-screen, the elapsed display +will disappear without animating all the way off-screen. Positive values will +cause the display to animate the wrong direction. +]] +settings['remaining-offscreen-pos'] = -100 +helpText['remaining-offscreen-pos'] = [[Controls how far off the left side of the window the remaining time display +tries to move when it is inactive. If you use a non-default font, this value may +need to be tweaked. If this value is not far enough off-screen, the elapsed +display will disappear without animating all the way off-screen. Positive values +will cause the display to animate the wrong direction. +]] +settings['hover-time-offscreen-pos'] = -50 +helpText['hover-time-offscreen-pos'] = [[Controls how far off the bottom of the window the mouse hover time display tries +to move when it is inactive. If you use a non-default font, this value may need +to be tweaked. If this value is not far enough off-screen, the elapsed +display will disappear without animating all the way off-screen. Positive values +will cause the display to animate the wrong direction. +]] +settings['system-time-offscreen-pos'] = -100 +helpText['system-time-offscreen-pos'] = [[Controls how far off the left side of the window the system time display tries +to move when it is inactive. If you use a non-default font, this value may need +to be tweaked. If this value is not far enough off-screen, the elapsed display +will disappear without animating all the way off-screen. Positive values will +cause the display to animate the wrong direction. +]] +settings['title-offscreen-pos'] = -40 +helpText['title-offscreen-pos'] = [[Controls how far off the left side of the window the video title display tries +to move when it is inactive. If you use a non-default font, this value may need +to be tweaked. If this value is not far enough off-screen, the elapsed display +will disappear without animating all the way off-screen. Positive values will +cause the display to animate the wrong direction. +]] +settings:__reload() +local Stack +do + local _class_0 + local removeElementMetadata, reindex + local _base_0 = { + insert = function(self, element, index) + if index then + table.insert(self, index, element) + element[self] = index + else + table.insert(self, element) + element[self] = #self + end + if self.containmentKey then + element[self.containmentKey] = true + end + end, + remove = function(self, element) + if element[self] == nil then + error("Trying to remove an element that doesn't exist in this stack.") + end + table.remove(self, element[self]) + reindex(self, element[self]) + return removeElementMetadata(self, element) + end, + clear = function(self) + local element = table.remove(self) + while element do + removeElementMetadata(self, element) + element = table.remove(self) + end + end, + removeSortedList = function(self, elementList) + if #elementList < 1 then + return + end + for i = 1, #elementList - 1 do + local element = table.remove(elementList) + table.remove(self, element[self]) + removeElementMetadata(self, element) + end + local lastElement = table.remove(elementList) + table.remove(self, lastElement[self]) + reindex(self, lastElement[self]) + return removeElementMetadata(self, lastElement) + end, + removeList = function(self, elementList) + table.sort(elementList, function(a, b) + return a[self] < b[self] + end) + return self:removeSortedList(elementList) + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, containmentKey) + self.containmentKey = containmentKey + end, + __base = _base_0, + __name = "Stack" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + local self = _class_0 + removeElementMetadata = function(self, element) + element[self] = nil + if self.containmentKey then + element[self.containmentKey] = false + end + end + reindex = function(self, start) + if start == nil then + start = 1 + end + for i = start, #self do + (self[i])[self] = i + end + end + Stack = _class_0 +end +local Window +do + local _class_0 + local osdScale + local _base_0 = { } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function() end, + __base = _base_0, + __name = "Window" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + local self = _class_0 + osdScale = settings['display-scale-factor'] + self.__class.w, self.__class.h = 0, 0 + self.update = function(self) + local w, h = mp.get_osd_size() + w, h = math.floor(w / osdScale), math.floor(h / osdScale) + if w ~= self.w or h ~= self.h then + self.w, self.h = w, h + return true + else + return false + end + end + Window = _class_0 +end +local Rect +do + local _class_0 + local _base_0 = { + cacheMaxBounds = function(self) + self.xMax = self.x + self.w + self.yMax = self.y + self.h + end, + setPosition = function(self, x, y) + self.x = x or self.x + self.y = y or self.y + return self:cacheMaxBounds() + end, + setSize = function(self, w, h) + self.w = w or self.w + self.h = h or self.h + return self:cacheMaxBounds() + end, + reset = function(self, x, y, w, h) + self.x = x or self.x + self.y = y or self.y + self.w = w or self.w + self.h = h or self.h + return self:cacheMaxBounds() + end, + move = function(self, x, y) + self.x = self.x + (x or self.x) + self.y = self.y + (y or self.y) + return self:cacheMaxBounds() + end, + stretch = function(self, w, h) + self.w = self.w + (w or self.w) + self.h = self.h + (h or self.h) + return self:cacheMaxBounds() + end, + containsPoint = function(self, x, y) + return (x >= self.x) and (x < self.xMax) and (y >= self.y) and (y < self.yMax) + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, x, y, w, h) + if x == nil then + x = -1 + end + if y == nil then + y = -1 + end + if w == nil then + w = -1 + end + if h == nil then + h = -1 + end + self.x, self.y, self.w, self.h = x, y, w, h + return self:cacheMaxBounds() + end, + __base = _base_0, + __name = "Rect" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Rect = _class_0 +end +local AnimationQueue +do + local _class_0 + local animationList, deletionQueue + local _base_0 = { } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function() end, + __base = _base_0, + __name = "AnimationQueue" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + local self = _class_0 + animationList = Stack('active') + deletionQueue = { } + self.addAnimation = function(animation) + if not (animation.active) then + return animationList:insert(animation) + end + end + self.removeAnimation = function(animation) + if animation.active then + return animationList:remove(animation) + end + end + self.destroyAnimationStack = function() + return animationList:clear() + end + self.animate = function() + if #animationList == 0 then + return + end + local currentTime = mp.get_time() + for _, animation in ipairs(animationList) do + if animation:update(currentTime) then + table.insert(deletionQueue, animation) + end + end + if #deletionQueue > 0 then + return animationList:removeSortedList(deletionQueue) + end + end + self.active = function() + return #animationList > 0 + end + AnimationQueue = _class_0 +end +local EventLoop +do + local _class_0 + local _base_0 = { + reconfigure = function(self) + settings:__reload() + AnimationQueue.destroyAnimationStack() + for _, zone in ipairs(self.activityZones) do + zone:reconfigure() + end + for _, element in ipairs(self.uiElements) do + element:reconfigure() + end + end, + addZone = function(self, zone) + if zone == nil then + return + end + return self.activityZones:insert(zone) + end, + removeZone = function(self, zone) + if zone == nil then + return + end + return self.activityZones:remove(zone) + end, + generateUIFromZones = function(self) + local seenUIElements = { } + self.script = { } + self.uiElements:clear() + AnimationQueue.destroyAnimationStack() + for _, zone in ipairs(self.activityZones) do + for _, uiElement in ipairs(zone.elements) do + if not (seenUIElements[uiElement]) then + self:addUIElement(uiElement) + seenUIElements[uiElement] = true + end + end + end + return self.updateTimer:resume() + end, + addUIElement = function(self, uiElement) + if uiElement == nil then + error('nil UIElement added.') + end + self.uiElements:insert(uiElement) + return table.insert(self.script, '') + end, + removeUIElement = function(self, uiElement) + if uiElement == nil then + error('nil UIElement removed.') + end + table.remove(self.script, uiElement[self.uiElements]) + return self.uiElements:remove(uiElement) + end, + resize = function(self) + for _, zone in ipairs(self.activityZones) do + zone:resize() + end + for _, uiElement in ipairs(self.uiElements) do + uiElement:resize() + end + end, + redraw = function(self, forceRedraw) + if Window:update() then + self:resize() + end + for index, zone in ipairs(self.activityZones) do + zone:update(self.displayRequested, false) + end + AnimationQueue.animate() + for index, uiElement in ipairs(self.uiElements) do + if uiElement:redraw() then + self.script[index] = uiElement:stringify() + end + end + return mp.set_osd_ass(Window.w, Window.h, table.concat(self.script, '\n')) + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self) + self.script = { } + self.uiElements = Stack() + self.activityZones = Stack() + self.displayRequested = false + self.updateTimer = mp.add_periodic_timer(settings['redraw-period'], (function() + local _base_1 = self + local _fn_0 = _base_1.redraw + return function(...) + return _fn_0(_base_1, ...) + end + end)()) + self.updateTimer:stop() + mp.register_event('shutdown', function() + return self.updateTimer:kill() + end) + local displayRequestTimer + local displayDuration = settings['request-display-duration'] + mp.add_key_binding("tab", "request-display", function(event) + if event.event == "repeat" then + return + end + if event.event == "down" or event.event == "press" then + if displayRequestTimer then + displayRequestTimer:kill() + end + self.displayRequested = true + end + if event.event == "up" or event.event == "press" then + if displayDuration == 0 then + self.displayRequested = false + else + displayRequestTimer = mp.add_timeout(displayDuration, function() + self.displayRequested = false + end) + end + end + end, { + complex = true + }) + return mp.add_key_binding('ctrl+r', 'reconfigure', (function() + local _base_1 = self + local _fn_0 = _base_1.reconfigure + return function(...) + return _fn_0(_base_1, ...) + end + end)(), { + repeatable = false + }) + end, + __base = _base_0, + __name = "EventLoop" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + EventLoop = _class_0 +end +local Animation +do + local _class_0 + local _base_0 = { + update = function(self, now) + if self.isReversed then + self.linearProgress = math.max(0, math.min(1, self.linearProgress + (self.lastUpdate - now) * self.durationR)) + if self.linearProgress == 0 then + self.isFinished = true + end + else + self.linearProgress = math.max(0, math.min(1, self.linearProgress + (now - self.lastUpdate) * self.durationR)) + if self.linearProgress == 1 then + self.isFinished = true + end + end + self.lastUpdate = now + local progress = math.pow(self.linearProgress, self.accel) + self.value = (1 - progress) * self.initialValue + progress * self.endValue + self.updateCb(self.value) + if self.isFinished and self.finishedCb then + self:finishedCb() + end + return self.isFinished + end, + interrupt = function(self, reverse) + self.finishedCb = nil + self.lastUpdate = mp.get_time() + self.isReversed = reverse + if not (self.active) then + self.isFinished = false + return AnimationQueue.addAnimation(self) + end + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, initialValue, endValue, duration, updateCb, finishedCb, accel) + if accel == nil then + accel = 1 + end + self.initialValue, self.endValue, self.duration, self.updateCb, self.finishedCb, self.accel = initialValue, endValue, duration, updateCb, finishedCb, accel + self.value = self.initialValue + self.linearProgress = 0 + self.lastUpdate = mp.get_time() + self.durationR = 1 / self.duration + self.isFinished = (self.duration <= 0) + self.active = false + self.isReversed = false + end, + __base = _base_0, + __name = "Animation" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Animation = _class_0 +end +local UIElement +do + local _class_0 + local _base_0 = { + stringify = function(self) + self.needsUpdate = false + if not self.active then + return '' + else + return table.concat(self.line) + end + end, + activate = function(self, activate) + if activate == true then + self.animation:interrupt(false) + self.active = true + else + self.animation:interrupt(true) + self.animation.finishedCb = function() + self.active = false + end + end + end, + reconfigure = function(self) + self.needsUpdate = true + self.animationDuration = settings['animation-duration'] + end, + resize = function(self) + return error('UIElement updateSize called') + end, + redraw = function(self) + return self.needsUpdate + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self) + self.needsUpdate = false + self.active = false + self.animationDuration = settings['animation-duration'] + end, + __base = _base_0, + __name = "UIElement" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + UIElement = _class_0 +end +local PauseIndicator +do + local _class_0 + local _base_0 = { + stringify = function(self) + return table.concat(self.line) + end, + resize = function(self) + local w, h = 0.5 * Window.w, 0.5 * Window.h + self.line[5] = ([[%g,%g]]):format(w, h) + self.line[12] = ([[%g,%g]]):format(w, h) + end, + redraw = function() + return true + end, + animate = function(self, value) + local scale = value * 50 + 100 + local scaleStr = ([[{\fscx%g\fscy%g]]):format(scale, scale) + local alphaStr = ('%02X'):format(value * value * 255) + self.line[1] = scaleStr + self.line[8] = scaleStr + self.line[3] = alphaStr + self.line[10] = alphaStr + end, + destroy = function(self, animation) + return self.eventLoop:removeUIElement(self) + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, eventLoop, paused) + self.eventLoop = eventLoop + local w, h = 0.5 * Window.w, 0.5 * Window.h + self.line = { + [[{\fscx0\fscy0]], + [[\alpha&H]], + 0, + [[&\pos(]], + ([[%g,%g]]):format(w, h), + ([[)\an5\bord0%s\p1}]]):format(settings['pause-indicator-background-style']), + 0, + [[{\fscx0\fscy0]], + [[\alpha&H]], + 0, + [[&\pos(]], + ([[%g,%g]]):format(w, h), + ([[)\an5\bord0%s\p1}]]):format(settings['pause-indicator-foreground-style']), + 0 + } + if paused then + self.line[7] = 'm 75 37.5 b 75 58.21 58.21 75 37.5 75 16.79 75 0 58.21 0 37.5 0 16.79 16.79 0 37.5 0 58.21 0 75 16.79 75 37.5 m 23 20 l 23 55 33 55 33 20 m 42 20 l 42 55 52 55 52 20\n' + self.line[14] = 'm 0 0 m 75 75 m 23 20 l 23 55 33 55 33 20 m 42 20 l 42 55 52 55 52 20' + else + self.line[7] = 'm 75 37.5 b 75 58.21 58.21 75 37.5 75 16.79 75 0 58.21 0 37.5 0 16.79 16.79 0 37.5 0 58.21 0 75 16.79 75 37.5 m 25.8333 17.18 l 25.8333 57.6 60.8333 37.39\n' + self.line[14] = 'm 0 0 m 75 75 m 25.8333 17.18 l 25.8333 57.6 60.8333 37.39' + end + AnimationQueue.addAnimation(Animation(0, 1, settings['animation-duration'], (function() + local _base_1 = self + local _fn_0 = _base_1.animate + return function(...) + return _fn_0(_base_1, ...) + end + end)(), (function() + local _base_1 = self + local _fn_0 = _base_1.destroy + return function(...) + return _fn_0(_base_1, ...) + end + end)())) + return self.eventLoop:addUIElement(self) + end, + __base = _base_0, + __name = "PauseIndicator" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + PauseIndicator = _class_0 +end +local eventLoop = EventLoop() +local notFrameStepping = false +if settings['pause-indicator'] then + local PauseIndicatorWrapper + PauseIndicatorWrapper = function(event, paused) + if notFrameStepping then + return PauseIndicator(eventLoop, paused) + elseif paused then + notFrameStepping = true + end + end + mp.add_key_binding('.', 'step-forward', function() + notFrameStepping = false + return mp.commandv('frame_step') + end, { + repeatable = true + }) + mp.add_key_binding(',', 'step-backward', function() + notFrameStepping = false + return mp.commandv('frame_back_step') + end, { + repeatable = true + }) + mp.observe_property('pause', 'bool', PauseIndicatorWrapper) +end +local initDraw +initDraw = function() + mp.unregister_event(initDraw) + notFrameStepping = true + eventLoop:resize() + eventLoop:redraw() + return eventLoop.updateTimer:resume() +end +local fileLoaded +fileLoaded = function() + return mp.register_event('playback-restart', initDraw) +end +return mp.register_event('file-loaded', fileLoaded) diff --git a/mpv/scripts/reload.lua b/mpv/scripts/reload.lua new file mode 100644 index 0000000..8b9e355 --- /dev/null +++ b/mpv/scripts/reload.lua @@ -0,0 +1,418 @@ +-- reload.lua +-- +-- When an online video is stuck buffering or got very slow CDN +-- source, restarting often helps. This script provides automatic +-- reloading of videos that doesn't have buffering progress for some +-- time while keeping the current time position. It also adds `Ctrl+r` +-- keybinding to reload video manually. +-- +-- SETTINGS +-- +-- To override default setting put the `lua-settings/reload.conf` file in +-- mpv user folder, on linux it is `~/.config/mpv`. NOTE: config file +-- name should match the name of the script. +-- +-- Default `reload.conf` settings: +-- +-- ``` +-- # enable automatic reload on timeout +-- # when paused-for-cache event fired, we will wait +-- # paused_for_cache_timer_timeout sedonds and then reload the video +-- paused_for_cache_timer_enabled=yes +-- +-- # checking paused_for_cache property interval in seconds, +-- # can not be less than 0.05 (50 ms) +-- paused_for_cache_timer_interval=1 +-- +-- # time in seconds to wait until reload +-- paused_for_cache_timer_timeout=10 +-- +-- # enable automatic reload based on demuxer cache +-- # if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout +-- # time interval, the video will be reloaded as soon as demuxer cache depleated +-- demuxer_cache_timer_enabled=yes +-- +-- # checking demuxer-cache-time property interval in seconds, +-- # can not be less than 0.05 (50 ms) +-- demuxer_cache_timer_interval=2 +-- +-- # if demuxer cache didn't receive any data during demuxer_cache_timer_timeout +-- # we decide that it has no progress and will reload the stream when +-- # paused_for_cache event happens +-- demuxer_cache_timer_timeout=20 +-- +-- # when the end-of-file is reached, reload the stream to check +-- # if there is more content available. +-- reload_eof_enabled=no +-- +-- # keybinding to reload stream from current time position +-- # you can disable keybinding by setting it to empty value +-- # reload_key_binding= +-- reload_key_binding=Ctrl+r +-- ``` +-- +-- DEBUGGING +-- +-- Debug messages will be printed to stdout with mpv command line option +-- `--msg-level='reload=debug'`. You may also need to add the `--no-msg-color` +-- option to make the debug logs visible if you are using a dark colorscheme +-- in terminal. + +local msg = require 'mp.msg' +local options = require 'mp.options' +local utils = require 'mp.utils' + + +local settings = { + paused_for_cache_timer_enabled = true, + paused_for_cache_timer_interval = 1, + paused_for_cache_timer_timeout = 10, + demuxer_cache_timer_enabled = true, + demuxer_cache_timer_interval = 2, + demuxer_cache_timer_timeout = 20, + reload_eof_enabled = false, + reload_key_binding = "Ctrl+r", +} + +-- global state stores properties between reloads +local property_path = nil +local property_time_pos = 0 +local property_keep_open = nil + +-- FSM managing the demuxer cache. +-- +-- States: +-- +-- * fetch - fetching new data +-- * stale - unable to fetch new data for time < 'demuxer_cache_timer_timeout' +-- * stuck - unable to fetch new data for time >= 'demuxer_cache_timer_timeout' +-- +-- State transitions: +-- +-- +---------------------------+ +-- v | +-- +-------+ +-------+ +-------+ +-- + fetch +<--->+ stale +---->+ stuck | +-- +-------+ +-------+ +-------+ +-- | ^ | ^ | ^ +-- +---+ +---+ +---+ +local demuxer_cache = { + timer = nil, + + state = { + name = 'uninitialized', + demuxer_cache_time = 0, + in_state_time = 0, + }, + + events = { + continue_fetch = { name = 'continue_fetch', from = 'fetch', to = 'fetch' }, + continue_stale = { name = 'continue_stale', from = 'stale', to = 'stale' }, + continue_stuck = { name = 'continue_stuck', from = 'stuck', to = 'stuck' }, + fetch_to_stale = { name = 'fetch_to_stale', from = 'fetch', to = 'stale' }, + stale_to_fetch = { name = 'stale_to_fetch', from = 'stale', to = 'fetch' }, + stale_to_stuck = { name = 'stale_to_stuck', from = 'stale', to = 'stuck' }, + stuck_to_fetch = { name = 'stuck_to_fetch', from = 'stuck', to = 'fetch' }, + }, + +} + +-- Always start with 'fetch' state +function demuxer_cache.reset_state() + demuxer_cache.state = { + name = demuxer_cache.events.continue_fetch.to, + demuxer_cache_time = 0, + in_state_time = 0, + } +end + +-- Has 'demuxer_cache_time' changed +function demuxer_cache.has_progress_since(t) + return demuxer_cache.state.demuxer_cache_time ~= t +end + +function demuxer_cache.is_state_fetch() + return demuxer_cache.state.name == demuxer_cache.events.continue_fetch.to +end + +function demuxer_cache.is_state_stale() + return demuxer_cache.state.name == demuxer_cache.events.continue_stale.to +end + +function demuxer_cache.is_state_stuck() + return demuxer_cache.state.name == demuxer_cache.events.continue_stuck.to +end + +function demuxer_cache.transition(event) + if demuxer_cache.state.name == event.from then + + -- state setup + demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time + + if event.name == 'continue_fetch' then + demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval + elseif event.name == 'continue_stale' then + demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval + elseif event.name == 'continue_stuck' then + demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval + elseif event.name == 'fetch_to_stale' then + demuxer_cache.state.in_state_time = 0 + elseif event.name == 'stale_to_fetch' then + demuxer_cache.state.in_state_time = 0 + elseif event.name == 'stale_to_stuck' then + demuxer_cache.state.in_state_time = 0 + elseif event.name == 'stuck_to_fetch' then + demuxer_cache.state.in_state_time = 0 + end + + -- state transition + demuxer_cache.state.name = event.to + + msg.debug('demuxer_cache.transition', event.name, utils.to_string(demuxer_cache.state)) + else + msg.error( + 'demuxer_cache.transition', + 'illegal transition', event.name, + 'from state', demuxer_cache.state.name) + end +end + +function demuxer_cache.initialize(demuxer_cache_timer_interval) + demuxer_cache.reset_state() + demuxer_cache.timer = mp.add_periodic_timer( + demuxer_cache_timer_interval, + function() + demuxer_cache.demuxer_cache_timer_tick( + mp.get_property_native('demuxer-cache-time'), + demuxer_cache_timer_interval) + end + ) +end + +-- If there is no progress of demuxer_cache_time in +-- settings.demuxer_cache_timer_timeout time interval switch state to +-- 'stuck' and switch back to 'fetch' as soon as any progress is made +function demuxer_cache.demuxer_cache_timer_tick(demuxer_cache_time, demuxer_cache_timer_interval) + local event = nil + local cache_has_progress = demuxer_cache.has_progress_since(demuxer_cache_time) + + -- I miss pattern matching so much + if demuxer_cache.is_state_fetch() then + if cache_has_progress then + event = demuxer_cache.events.continue_fetch + else + event = demuxer_cache.events.fetch_to_stale + end + elseif demuxer_cache.is_state_stale() then + if cache_has_progress then + event = demuxer_cache.events.stale_to_fetch + elseif demuxer_cache.state.in_state_time < settings.demuxer_cache_timer_timeout then + event = demuxer_cache.events.continue_stale + else + event = demuxer_cache.events.stale_to_stuck + end + elseif demuxer_cache.is_state_stuck() then + if cache_has_progress then + event = demuxer_cache.events.stuck_to_fetch + else + event = demuxer_cache.events.continue_stuck + end + end + + event.demuxer_cache_time = demuxer_cache_time + event.interval = demuxer_cache_timer_interval + demuxer_cache.transition(event) +end + + +local paused_for_cache = { + timer = nil, + time = 0, +} + +function paused_for_cache.reset_timer() + msg.debug('paused_for_cache.reset_timer', paused_for_cache.time) + if paused_for_cache.timer then + paused_for_cache.timer:kill() + paused_for_cache.timer = nil + paused_for_cache.time = 0 + end +end + +function paused_for_cache.start_timer(interval_seconds, timeout_seconds) + msg.debug('paused_for_cache.start_timer', paused_for_cache.time) + if not paused_for_cache.timer then + paused_for_cache.timer = mp.add_periodic_timer( + interval_seconds, + function() + paused_for_cache.time = paused_for_cache.time + interval_seconds + if paused_for_cache.time >= timeout_seconds then + paused_for_cache.reset_timer() + reload_resume() + end + msg.debug('paused_for_cache', 'tick', paused_for_cache.time) + end + ) + end +end + +function paused_for_cache.handler(property, is_paused) + if is_paused then + + if demuxer_cache.is_state_stuck() then + msg.info("demuxer cache has no progress") + -- reset demuxer state to avoid immediate reload if + -- paused_for_cache event triggered right after reload + demuxer_cache.reset_state() + reload_resume() + end + + paused_for_cache.start_timer( + settings.paused_for_cache_timer_interval, + settings.paused_for_cache_timer_timeout) + else + paused_for_cache.reset_timer() + end +end + +function read_settings() + options.read_options(settings, mp.get_script_name()) + msg.debug(utils.to_string(settings)) +end + +function reload(path, time_pos) + msg.debug("reload", path, time_pos) + if time_pos == nil then + mp.commandv("loadfile", path, "replace") + else + mp.commandv("loadfile", path, "replace", "start=+" .. time_pos) + end +end + +function reload_resume() + local path = mp.get_property("path", property_path) + local time_pos = mp.get_property("time-pos") + local reload_duration = mp.get_property_native("duration") + + local playlist_count = mp.get_property_number("playlist/count") + local playlist_pos = mp.get_property_number("playlist-pos") + local playlist = {} + for i = 0, playlist_count-1 do + playlist[i] = mp.get_property("playlist/" .. i .. "/filename") + end + -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero + -- duration property. When reloading VOD, to keep the current time position + -- we should provide offset from the start. Stream doesn't have fixed start. + -- Decent choice would be to reload stream from it's current 'live' positon. + -- That's the reason we don't pass the offset when reloading streams. + if reload_duration and reload_duration > 0 then + msg.info("reloading video from", time_pos, "second") + reload(path, time_pos) + -- VODs get stuck when reload is called without a time_pos + -- this is most noticeable in youtube videos whenever download gets stuck in the first frames + -- video would stay paused without being actually paused + -- issue surfaced in mpv 0.33, afaik + elseif reload_duration and reload_duration == 0 then + msg.info("reloading video from", time_pos, "second") + reload(path, time_pos) + else + msg.info("reloading stream") + reload(path, nil) + end + msg.info("file ", playlist_pos+1, "of", playlist_count, "in playlist") + for i = 0, playlist_pos-1 do + mp.commandv("loadfile", playlist[i], "append") + end + mp.commandv("playlist-move", 0, playlist_pos+1) + for i = playlist_pos+1, playlist_count-1 do + mp.commandv("loadfile", playlist[i], "append") + end +end + +function reload_eof(property, eof_reached) + msg.debug("reload_eof", property, eof_reached) + local time_pos = mp.get_property_number("time-pos") + local duration = mp.get_property_number("duration") + + if eof_reached and math.floor(time_pos) == math.floor(duration) then + msg.debug("property_time_pos", property_time_pos, "time_pos", time_pos) + + -- Check that playback time_pos made progress after the last reload. When + -- eof is reached we try to reload video, in case there is more content + -- available. If time_pos stayed the same after reload, it means that vidkk + -- to avoid infinite reload loop when playback ended + -- math.floor function rounds time_pos to a second, to avoid inane reloads + if math.floor(property_time_pos) == math.floor(time_pos) then + msg.info("eof reached, playback ended") + mp.set_property("keep-open", property_keep_open) + else + msg.info("eof reached, checking if more content available") + reload_resume() + mp.set_property_bool("pause", false) + property_time_pos = time_pos + end + end +end + +function on_file_loaded(event) + local debug_info = { + event = event, + time_pos = mp.get_property("time-pos"), + stream_pos = mp.get_property("stream-pos"), + stream_end = mp.get_property("stream-end"), + duration = mp.get_property("duration"), + seekable = mp.get_property("seekable"), + pause = mp.get_property("pause"), + paused_for_cache = mp.get_property("paused-for-cache"), + cache_buffering_state = mp.get_property("cache-buffering-state"), + } + msg.debug("debug_info", utils.to_string(debug_info)) + + -- When the video is reloaded after being paused for cache, it won't start + -- playing again while all properties looks fine: + -- `pause=no`, `paused-for-cache=no` and `cache-buffering-state=100`. + -- As a workaround, we cycle through the paused state by sending two SPACE + -- keypresses. + -- What didn't work: + -- - Cycling through the `pause` property. + -- - Run the `playlist-play-index current` command. + mp.commandv("keypress", 'SPACE') + mp.commandv("keypress", 'SPACE') +end + +-- main + +read_settings() + +if settings.reload_key_binding ~= "" then + mp.add_key_binding(settings.reload_key_binding, "reload_resume", reload_resume) +end + +if settings.paused_for_cache_timer_enabled then + mp.observe_property("paused-for-cache", "bool", paused_for_cache.handler) +end + +if settings.demuxer_cache_timer_enabled then + demuxer_cache.initialize(settings.demuxer_cache_timer_interval) +end + +if settings.reload_eof_enabled then + -- vo-configured == video output created && its configuration went ok + mp.observe_property( + "vo-configured", + "bool", + function(name, vo_configured) + msg.debug(name, vo_configured) + if vo_configured then + property_path = mp.get_property("path") + property_keep_open = mp.get_property("keep-open") + mp.set_property("keep-open", "yes") + mp.set_property("keep-open-pause", "no") + end + end + ) + + mp.observe_property("eof-reached", "bool", reload_eof) +end + +mp.register_event("file-loaded", on_file_loaded) diff --git a/mpv/scripts/stats.lua b/mpv/scripts/stats.lua new file mode 100644 index 0000000..6ee491b --- /dev/null +++ b/mpv/scripts/stats.lua @@ -0,0 +1,754 @@ +-- 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' + +-- Options +local o = { + -- Default key bindings + key_oneshot = "i", + key_toggle = "I", + key_page_1 = "1", + key_page_2 = "2", + key_page_3 = "3", + + 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 + print_perfdata_passes = false, -- when true, print the full information about all passes + filter_params_max_length = 100, -- a filter list longer than this many characters will be shown one filter per line instead + debug = false, + + -- Graph options and style + plot_perfdata = true, + plot_vsync_ratio = true, + plot_vsync_jitter = true, + 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", + + -- Text style + font = "Source Sans Pro", + font_mono = "Source Sans Pro", -- monospaced digits are sufficient + font_size = 8, + font_color = "FFFFFF", + border_size = 0.8, + border_color = "262626", + shadow_x_offset = 0.0, + shadow_y_offset = 0.0, + shadow_color = "000000", + alpha = "11", + + -- 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", +} +options.read_options(o) + +local format = string.format +local max = math.max +local min = math.min + +-- Function used to record performance data +local recorder = nil +-- Timer used for redrawing (toggling) and clearing the screen (oneshot) +local display_timer = nil +-- Current page and : mappings +local curr_page = o.key_page_1 +local pages = {} +-- 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 +-- Save all properties known to this version of mpv +local property_list = {} +for p in string.gmatch(mp.get_property("property-list"), "([^,]+)") do property_list[p] = true end +-- Mapping of properties to their deprecated names +local property_aliases = { + ["decoder-frame-drop-count"] = "drop-frame-count", + ["frame-drop-count"] = "vo-drop-frame-count", + ["container-fps"] = "fps", +} + + +-- Return deprecated name for the given property +local function compat(p) + while not property_list[p] and property_aliases[p] do + p = property_aliases[p] + end + return p +end + + +local function set_ASS(b) + if not o.use_ass or o.persistent_overlay then + return "" + end + return b and ass_start or ass_stop +end + + +local function no_ASS(t) + return set_ASS(false) .. t .. set_ASS(true) +end + + +local function b(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 set_ASS(true) .. o.custom_header + else + return format("%s{\\r}{\\an7}{\\fs%d}{\\fn%s}{\\bord%f}{\\3c&H%s&}" .. + "{\\1c&H%s&}{\\alpha&H%s&}{\\xshad%f}{\\yshad%f}{\\4c&H%s&}", + set_ASS(true), o.font_size, o.font, o.border_size, + o.border_color, o.font_color, o.alpha, o.shadow_x_offset, + o.shadow_y_offset, o.shadow_color) + end +end + + +local function has_vo_window() + return mp.get_property("vo-configured") == "yes" +end + + +local function has_ansi() + local is_windows = type(package) == 'table' + and type(package.config) == 'string' + and package.config:sub(1, 1) == '\\' + if is_windows then + return os.getenv("ANSICON") + end + return true +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 = o.border_size + local y_max = o.font_size * 0.66 + local x = 0 + + -- try and center the graph if possible, but avoid going above `scale` + if v_avg then + scale = min(scale, v_max / (2 * v_avg)) + end + + local s = {format("m 0 0 n %f %f l ", x, y_max - (y_max * values[i] / v_max * scale))} + i = ((i - 2) % len) + 1 + + for p = 1, len - 1 do + if values[i] then + x = x - x_tics + s[#s+1] = format("%f %f ", x, y_max - (y_max * values[i] / v_max * scale)) + 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("{\\bord0.5}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0", + o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max) + return format("%s{\\r}{\\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 b(attr.prefix) + s[#s+1] = 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 append_perfdata(s, dedicated_page) + local vo_p = mp.get_property_native("vo-passes") + if not vo_p then + return + end + + local ds = mp.get_property_bool("display-sync-active", false) + local target_fps = ds and mp.get_property_number("display-fps", 0) + or mp.get_property_number(compat("container-fps"), 0) + if target_fps > 0 then target_fps = 1 / target_fps * 1e9 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("%05d", 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 + return format("{\\b%d}%02d%%{\\b0}", w, i * 100) + end + + s[#s+1] = format("%s%s%s%s{\\fs%s}%s{\\fs%s}", + dedicated_page and "" or o.nl, dedicated_page and "" or o.indent, + b("Frame Timings:"), o.prefix_sep, o.font_size * 0.66, + "(last/average/peak μs)", o.font_size) + + for frame, data in pairs(vo_p) do + local f = "%s%s%s{\\fn%s}%s / %s / %s %s%s{\\fn%s}%s%s%s" + + if dedicated_page then + s[#s+1] = format("%s%s%s:", o.nl, o.indent, + b(frame:gsub("^%l", string.upper))) + + for _, pass in ipairs(data) do + s[#s+1] = format(f, o.nl, o.indent, o.indent, + o.font_mono, pp(pass["last"]), + pp(pass["avg"]), pp(pass["peak"]), + o.prefix_sep .. o.prefix_sep, p(pass["last"], last_s[frame]), + o.font, o.prefix_sep, o.prefix_sep, pass["desc"]) + + if o.plot_perfdata and o.use_ass then + s[#s+1] = 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, + o.font_mono, pp(last_s[frame]), + pp(avg_s[frame]), pp(peak_s[frame]), "", "", o.font, + o.prefix_sep, o.prefix_sep, b("Total")) + else + -- for the simplified view, we just print the sum of each pass + s[#s+1] = format(f, o.nl, o.indent, o.indent, o.font_mono, + pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]), + "", "", o.font, o.prefix_sep, o.prefix_sep, + frame:gsub("^%l", string.upper)) + end + end +end + + +local function append_display_sync(s) + if not mp.get_property_bool("display-sync-active", false) then + return + end + + local vspeed = append_property(s, "video-speed-correction", {prefix="DS:"}) + if vspeed then + append_property(s, "audio-speed-correction", + {prefix="/", nl="", indent=" ", prefix_sep=" ", no_prefix_markup=true}) + else + append_property(s, "audio-speed-correction", + {prefix="DS:" .. o.prefix_sep .. " - / ", prefix_sep=""}) + end + + append_property(s, "mistimed-frame-count", {prefix="Mistimed:", nl=""}) + append_property(s, "vo-delayed-frame-count", {prefix="Delayed:", nl=""}) + + -- As we need to plot some graphs we print jitter and ratio on their own lines + if not display_timer.oneshot and (o.plot_vsync_ratio or o.plot_vsync_jitter) and o.use_ass then + local ratio_graph = "" + local jitter_graph = "" + if o.plot_vsync_ratio then + ratio_graph = generate_graph(vsratio_buf, vsratio_buf.pos, vsratio_buf.len, vsratio_buf.max, nil, 0.8, 1) + end + if o.plot_vsync_jitter then + jitter_graph = generate_graph(vsjitter_buf, vsjitter_buf.pos, vsjitter_buf.len, vsjitter_buf.max, nil, 0.8, 1) + end + append_property(s, "vsync-ratio", {prefix="VSync Ratio:", suffix=o.prefix_sep .. ratio_graph}) + append_property(s, "vsync-jitter", {prefix="VSync Jitter:", suffix=o.prefix_sep .. jitter_graph}) + else + -- Since no graph is needed we can print ratio/jitter on the same line and save some space + local vratio = append_property(s, "vsync-ratio", {prefix="VSync Ratio:"}) + append_property(s, "vsync-jitter", {prefix="VSync Jitter:", nl="" or o.nl}) + end +end + + +local function append_filters(s, prop, prefix) + local length = 0 + local filters = {} + + for _,f in ipairs(mp.get_property_native(prop, {})) do + local n = f.name + if f.enabled ~= nil and not f.enabled then + n = n .. " (disabled)" + end + + local p = {} + for key,value in pairs(f.params) do + p[#p+1] = key .. "=" .. value + end + if #p > 0 then + p = " [" .. table.concat(p, " ") .. "]" + else + p = "" + end + + length = length + n:len() + p:len() + filters[#filters+1] = no_ASS(n) .. it(no_ASS(p)) + end + + if #filters > 0 then + local ret + if length < o.filter_params_max_length then + ret = table.concat(filters, ", ") + else + local sep = o.nl .. o.indent .. o.indent + ret = sep .. table.concat(filters, sep) + end + s[#s+1] = o.nl .. o.indent .. b(prefix) .. o.prefix_sep .. ret + end +end + + +local function add_header(s) + s[#s+1] = text_style() +end + + +local function add_file(s) + append(s, "", {prefix="File:", nl="", indent=""}) + append_property(s, "filename", {prefix_sep="", nl="", indent=""}) + if not (mp.get_property_osd("filename") == mp.get_property_osd("media-title")) then + append_property(s, "media-title", {prefix="Title:"}) + end + + local ch_index = mp.get_property_number("chapter") + if ch_index and ch_index >= 0 then + append_property(s, "chapter-list/" .. tostring(ch_index) .. "/title", {prefix="Chapter:"}) + append_property(s, "chapter-list/count", + {prefix="(" .. tostring(ch_index + 1) .. "/", suffix=")", nl="", + indent=" ", prefix_sep=" ", no_prefix_markup=true}) + end + + local demuxer_cache = mp.get_property_native("demuxer-cache-state", {}) + if demuxer_cache["fw-bytes"] then + demuxer_cache = demuxer_cache["fw-bytes"] -- returns bytes + else + demuxer_cache = 0 + end + local demuxer_secs = mp.get_property_number("demuxer-cache-duration", 0) + local stream_cache = mp.get_property_number("cache-used", 0) * 1024 -- returns KiB + if stream_cache + demuxer_cache + demuxer_secs > 0 then + append(s, utils.format_bytes_humanized(stream_cache + demuxer_cache), {prefix="Total Cache:"}) + append(s, utils.format_bytes_humanized(demuxer_cache), {prefix="(Demuxer:", + suffix=",", nl="", no_prefix_markup=true, indent=o.prefix_sep}) + append(s, format("%.1f", demuxer_secs), {suffix=" sec)", nl="", indent="", + no_prefix_markup=true}) + local speed = mp.get_property_number("cache-speed", 0) + if speed > 0 then + append(s, utils.format_bytes_humanized(speed) .. "/s", {prefix="Speed:", nl="", + indent=o.prefix_sep, no_prefix_markup=true}) + end + end + append_property(s, "file-size", {prefix="Size:"}) +end + + +local function add_video(s) + local r = mp.get_property_native("video-params") + -- in case of e.g. lavi-complex there can be no input video, only output + if not r then + r = mp.get_property_native("video-out-params") + end + if not r then + return + end + + append(s, "", {prefix=o.nl .. o.nl .. "Video:", nl="", indent=""}) + if append_property(s, "video-codec", {prefix_sep="", nl="", indent=""}) then + append_property(s, "hwdec-current", {prefix="(hwdec:", nl="", indent=" ", + no_prefix_markup=true, suffix=")"}, {no=true, [""]=true}) + end + append_property(s, "avsync", {prefix="A-V:"}) + if append_property(s, compat("decoder-frame-drop-count"), + {prefix="Dropped Frames:", suffix=" (decoder)"}) then + append_property(s, compat("frame-drop-count"), {suffix=" (output)", nl="", indent=""}) + end + if append_property(s, "display-fps", {prefix="Display FPS:", suffix=" (specified)"}) then + append_property(s, "estimated-display-fps", + {suffix=" (estimated)", nl="", indent=""}) + else + append_property(s, "estimated-display-fps", + {prefix="Display FPS:", suffix=" (estimated)"}) + end + if append_property(s, compat("container-fps"), {prefix="FPS:", suffix=" (specified)"}) then + append_property(s, "estimated-vf-fps", + {suffix=" (estimated)", nl="", indent=""}) + else + append_property(s, "estimated-vf-fps", + {prefix="FPS:", suffix=" (estimated)"}) + end + + append_display_sync(s) + append_perfdata(s, o.print_perfdata_passes) + + if append(s, r["w"], {prefix="Native Resolution:"}) then + append(s, r["h"], {prefix="x", nl="", indent=" ", prefix_sep=" ", no_prefix_markup=true}) + end + append_property(s, "window-scale", {prefix="Window Scale:"}) + append(s, format("%.2f", r["aspect"]), {prefix="Aspect Ratio:"}) + append(s, r["pixelformat"], {prefix="Pixel Format:"}) + + -- Group these together to save vertical space + local prim = append(s, r["primaries"], {prefix="Primaries:"}) + local cmat = append(s, r["colormatrix"], {prefix="Colormatrix:", nl=prim and "" or o.nl}) + append(s, r["colorlevels"], {prefix="Levels:", nl=cmat and "" or o.nl}) + + -- Append HDR metadata conditionally (only when present and interesting) + local hdrpeak = r["sig-peak"] or 0 + local hdrinfo = "" + if hdrpeak > 1 then + hdrinfo = " (HDR peak: " .. format("%.2f", hdrpeak) .. ")" + end + + append(s, r["gamma"], {prefix="Gamma:", suffix=hdrinfo}) + append_property(s, "packet-video-bitrate", {prefix="Bitrate:", suffix=" kbps"}) + append_filters(s, "vf", "Filters:") +end + + +local function add_audio(s) + local r = mp.get_property_native("audio-params") + -- in case of e.g. lavi-complex there can be no input audio, only output + if not r then + r = mp.get_property_native("audio-out-params") + end + if not r then + return + end + + append(s, "", {prefix=o.nl .. o.nl .. "Audio:", nl="", indent=""}) + append_property(s, "audio-codec", {prefix_sep="", nl="", indent=""}) + append(s, r["format"], {prefix="Format:"}) + append(s, r["samplerate"], {prefix="Sample Rate:", suffix=" Hz"}) + append(s, r["channel-count"], {prefix="Channels:"}) + append_property(s, "packet-audio-bitrate", {prefix="Bitrate:", suffix=" kbps"}) + append_filters(s, "af", "Filters:") +end + + +-- Determine whether ASS formatting shall/can be used and set formatting sequences +local function eval_ass_formatting() + o.use_ass = o.ass_formatting and has_vo_window() + if o.use_ass then + o.nl = o.ass_nl + o.indent = o.ass_indent + o.prefix_sep = o.ass_prefix_sep + o.b1 = o.ass_b1 + o.b0 = o.ass_b0 + o.it1 = o.ass_it1 + o.it0 = o.ass_it0 + else + o.nl = o.no_ass_nl + o.indent = o.no_ass_indent + o.prefix_sep = o.no_ass_prefix_sep + if not has_ansi() then + o.b1 = "" + o.b0 = "" + o.it1 = "" + o.it0 = "" + else + o.b1 = o.no_ass_b1 + o.b0 = o.no_ass_b0 + o.it1 = o.no_ass_it1 + o.it0 = o.no_ass_it0 + end + end +end + + +-- Returns an ASS string with "normal" stats +local function default_stats() + local stats = {} + eval_ass_formatting() + add_header(stats) + add_file(stats) + add_video(stats) + add_audio(stats) + return table.concat(stats) +end + + +-- Returns an ASS string with extended VO stats +local function vo_stats() + local stats = {} + eval_ass_formatting() + add_header(stats) + append_perfdata(stats, true) + return table.concat(stats) +end + + +-- Returns an ASS string with stats about filters/profiles/shaders +local function filter_stats() + return "coming soon" +end + + +-- Current page and : mapping +curr_page = o.key_page_1 +pages = { + [o.key_page_1] = { f = default_stats, desc = "Default" }, + [o.key_page_2] = { f = vo_stats, desc = "Extended Frame Timings" }, + --[o.key_page_3] = { f = filter_stats, desc = "Dummy" }, +} + + +-- Returns a function to record vsratio/jitter with the specified `skip` value +local function record_data(skip) + init_buffers() + skip = max(skip, 0) + local i = skip + return function() + if i < skip then + i = i + 1 + return + else + i = 0 + end + + if o.plot_vsync_jitter then + local r = mp.get_property_number("vsync-jitter", nil) + if r then + vsjitter_buf.pos = (vsjitter_buf.pos % vsjitter_buf.len) + 1 + vsjitter_buf[vsjitter_buf.pos] = r + vsjitter_buf.max = max(vsjitter_buf.max, r) + end + end + + if o.plot_vsync_ratio then + local r = mp.get_property_number("vsync-ratio", nil) + if r then + vsratio_buf.pos = (vsratio_buf.pos % vsratio_buf.len) + 1 + vsratio_buf[vsratio_buf.pos] = r + vsratio_buf.max = max(vsratio_buf.max, r) + end + end + end +end + + +-- Call the function for `page` and print it to OSD +local function print_page(page) + if o.persistent_overlay then + mp.set_osd_ass(0, 0, pages[page].f()) + else + mp.osd_message(pages[page].f(), display_timer.oneshot and o.duration or o.redraw_delay + 1) + end +end + + +local function clear_screen() + if o.persistent_overlay then mp.set_osd_ass(0, 0, "") else mp.osd_message("", 0) end +end + + +-- Add keybindings for every page +local function add_page_bindings() + local function a(k) + return function() + curr_page = k + print_page(k) + if display_timer.oneshot then display_timer:kill() ; display_timer:resume() end + end + end + for k, _ in pairs(pages) do + mp.add_forced_key_binding(k, k, a(k), {repeatable=true}) + end +end + + +-- Remove keybindings for every page +local function remove_page_bindings() + for k, _ in pairs(pages) do + mp.remove_key_binding(k) + end +end + + +local function process_key_binding(oneshot) + -- Stats are already being displayed + if display_timer:is_enabled() then + -- Previous and current keys were oneshot -> restart timer + if display_timer.oneshot and oneshot then + display_timer:kill() + print_page(curr_page) + display_timer:resume() + -- Previous and current keys were toggling -> end toggling + elseif not display_timer.oneshot and not oneshot then + display_timer:kill() + clear_screen() + remove_page_bindings() + if recorder then + mp.unregister_event(recorder) + recorder = nil + end + end + -- No stats are being displayed yet + else + if not oneshot and (o.plot_vsync_jitter or o.plot_vsync_ratio) then + recorder = record_data(o.skip_frames) + mp.register_event("tick", recorder) + end + display_timer:kill() + display_timer.oneshot = oneshot + display_timer.timeout = oneshot and o.duration or o.redraw_delay + add_page_bindings() + print_page(curr_page) + display_timer:resume() + end +end + + +-- Create the timer used for redrawing (toggling) or clearing the screen (oneshot) +-- The duration here is not important and always set in process_key_binding() +display_timer = mp.add_periodic_timer(o.duration, + function() + if display_timer.oneshot then + display_timer:kill() ; clear_screen() ; remove_page_bindings() + else + print_page(curr_page) + end + end) +display_timer:kill() + +-- Single invocation key binding +mp.add_key_binding(o.key_oneshot, "display-stats", function() process_key_binding(true) end, + {repeatable=true}) + +-- Toggling key binding +mp.add_key_binding(o.key_toggle, "display-stats-toggle", function() process_key_binding(false) end, + {repeatable=false}) + +-- Single invocation bindings without key, can be used in input.conf to create +-- bindings for a specific page: "e script-binding stats/display-page-2" +for k, _ in pairs(pages) do + mp.add_key_binding(nil, "display-page-" .. k, function() process_key_binding(true) end, + {repeatable=true}) +end + +-- Reprint stats immediately when VO was reconfigured, only when toggled +mp.register_event("video-reconfig", + function() + if display_timer:is_enabled() then + print_page(curr_page) + end + end) diff --git a/mpv/scripts/status-line.lua b/mpv/scripts/status-line.lua new file mode 100755 index 0000000..e40dce2 --- /dev/null +++ b/mpv/scripts/status-line.lua @@ -0,0 +1,92 @@ +-- Rebuild the terminal status line as a lua script +-- Be aware that this will require more cpu power! +-- Also, this is based on a rather old version of the +-- builtin mpv status line. + +-- Add a string to the status line +function atsl(s) + newStatus = newStatus .. s +end + +function update_status_line() + -- Reset the status line + newStatus = "" + + if mp.get_property_bool("pause") then + atsl("(Paused) ") + elseif mp.get_property_bool("paused-for-cache") then + atsl("(Buffering) ") + end + + if mp.get_property("aid") ~= "no" then + atsl("A") + end + if mp.get_property("vid") ~= "no" then + atsl("V") + end + + atsl(": ") + + atsl(mp.get_property_osd("time-pos")) + + atsl(" / "); + atsl(mp.get_property_osd("duration")); + + atsl(" (") + atsl(mp.get_property_osd("percent-pos", -1)) + atsl("%)") + + local r = mp.get_property_number("speed", -1) + if r ~= 1 then + atsl(string.format(" x%4.2f", r)) + end + + r = mp.get_property_number("avsync", nil) + if r ~= nil then + atsl(string.format(" A-V: %f", r)) + end + + r = mp.get_property("total-avsync-change", 0) + if math.abs(r) > 0.05 then + atsl(string.format(" ct:%7.3f", r)) + end + + r = mp.get_property_number("decoder-drop-frame-count", -1) + if r > 0 then + atsl(" Late: ") + atsl(r) + end + + r = mp.get_property_osd("video-bitrate") + if r ~= nil and r ~= "" then + atsl(" Vb: ") + atsl(r) + end + + r = mp.get_property_osd("audio-bitrate") + if r ~= nil and r ~= "" then + atsl(" Ab: ") + atsl(r) + end + + r = mp.get_property_number("cache", 0) + if r > 0 then + atsl(string.format(" Cache: %d%% ", r)) + end + + -- Set the new status line + mp.set_property("options/term-status-msg", newStatus) +end + +timer = mp.add_periodic_timer(1, update_status_line) + +function on_pause_change(name, value) + if value == false then + timer:resume() + else + timer:stop() + end + mp.add_timeout(0.1, update_status_line) +end +mp.observe_property("pause", "bool", on_pause_change) +mp.register_event("seek", update_status_line) diff --git a/mpv/scripts/tv.lua b/mpv/scripts/tv.lua new file mode 100644 index 0000000..4135e73 --- /dev/null +++ b/mpv/scripts/tv.lua @@ -0,0 +1,83 @@ +-- Simple script for configurable TV out activation and/or deactivation on mpv playback +-- +-- Intended to activate TV on mpv startup and deactivate TV on mpv close if TV is connected. +-- The script executes fully configurable shell sequences (e.g. xrandr on linux) +-- Can also be used for activating ambient lighting while watching etc ... +-- +-- Note: There are implicit security issues due to a nature of direct execution +-- of command line tools without any sanitization ... +-- +-- TV configurable options: +-- test ... check if TV is connected (result is non empty, exitcode 0) +-- on ... executed once on mpv player startup (TV ON) +-- off ... executed once on mpv player shutdown (TV OFF) +-- +-- Note: xrandr seems to have problem turning off and on devices on single execution. +-- Therefore it is wise to split execution to multiple commands, for example: +-- problem : xrandr --output LVDS1 --off --output TV1 --auto +-- works ok: xrandr --output LVDS1 --off && xrandr --output TV1 --auto +-- +-- To customize configuration place tv.conf into ~/.config/mpv/lua-settings/ and edit +-- +-- Place script into ~/.config/mpv/scripts/ for autoload +-- +-- GitHub: https://github.com/blue-sky-r/mpv/tree/master/scripts + +local options = require("mp.options") +local utils = require("mp.utils") + +-- defaults +local cfg = { + test = "xrandr | grep 'VGA1 connected'", + on = 'xrandr --output LVDS1 --off && xrandr --output VGA1 --mode 720x400 --output TV1 --auto', + off = 'xrandr --output LVDS1 --auto' +} + +-- string v is empty +local function empty(v) + return not v or v == '' or string.find(v,"^%s*$") +end + +-- evaluate shell condition by executing cmd +local function test(cmd) + -- return success if there is nothing to test + if empty(cmd) then return true end + -- get only exitcode + local exitcode = io.popen(cmd..' >/dev/null 2>&1; echo $?'):read('*n') + -- log + mp.msg.info("test '" .. cmd .. "' returned exitcode:"..exitcode) + -- success if exitcode is zero + return exitcode == 0 +end + +-- execute shell cmd +local function exec(cmd) + -- return if there is nothing to execute + if empty(cmd) then return end + -- get stdout and stderr combined + local stdcom = io.popen(cmd..' 2>&1'):read('*all') + -- log + mp.msg.info("exec '" .. cmd .. "'") + if stdcom then mp.msg.verbose(stdcom) end +end + +-- read lua-settings/tv.conf +options.read_options(cfg, 'tv') + +-- log active config +mp.msg.verbose('cfg = '..utils.to_string(cfg)) + +-- execute only if test condition +if test(cfg.test) then + -- optional TV.ON execute now + if not empty(cfg.on) then exec(cfg.on) end + + -- optional TV.OFF execute on shutdown + if not empty(cfg.off) then + mp.register_event("shutdown", + function() + exec(cfg.off) + end + ) + end +end diff --git a/mpv/scripts/xscreensaver.lua b/mpv/scripts/xscreensaver.lua new file mode 100644 index 0000000..4fbb41c --- /dev/null +++ b/mpv/scripts/xscreensaver.lua @@ -0,0 +1,24 @@ +-- this script periodically deactivates xscreensaver +-- when video playback is active + +local function heartbeat() + if mp.get_property_native("pause") or + mp.get_property_native("idle") or + not mp.get_property_native("vo-configured") then + return + end + + mp.command_native_async( + { + name = "subprocess", + args = { "xscreensaver-command", "-deactivate" }, + capture_stdout = true, + }, + function () end) +end + +mp.add_periodic_timer(60, heartbeat) + +for _, prop in ipairs({"pause", "idle", "vo-configured"}) do + mp.observe_property(prop, nil, heartbeat) +end