aboutsummaryrefslogtreecommitdiff
path: root/mpv
diff options
context:
space:
mode:
authortrainytrain <trainytrain@rape.lol>2021-05-09 01:29:07 -0700
committertrainytrain <trainytrain@rape.lol>2021-05-09 01:29:07 -0700
commitc765e68f05bfe9f0d2e4990bdc8dfabf11cdbc87 (patch)
tree1dd446298beeb5d1a411d5516689dee8c2ee26ee /mpv
init
Diffstat (limited to 'mpv')
-rw-r--r--mpv/input.conf209
-rw-r--r--mpv/mpv.conf65
-rw-r--r--mpv/scripts/autoloop.lua53
-rw-r--r--mpv/scripts/autosave.lua38
-rw-r--r--mpv/scripts/autospeed.lua420
-rw-r--r--mpv/scripts/autosub.lua254
-rw-r--r--mpv/scripts/autosubsync.lua44
-rw-r--r--mpv/scripts/quack.lua44
-rw-r--r--mpv/scripts/uosc.lua3230
-rw-r--r--mpv/scripts/webm.lua2689
-rw-r--r--mpv/scripts/youtube-quality.lua275
11 files changed, 7321 insertions, 0 deletions
diff --git a/mpv/input.conf b/mpv/input.conf
new file mode 100644
index 0000000..8309354
--- /dev/null
+++ b/mpv/input.conf
@@ -0,0 +1,209 @@
+# mpv keybindings
+#
+# Location of user-defined bindings: ~/.config/mpv/input.conf
+#
+# Lines starting with # are comments. Use SHARP to assign the # key.
+# Copy this file and uncomment and edit the bindings you want to change.
+#
+# List of commands and further details: DOCS/man/input.rst
+# List of special keys: --input-keylist
+# Keybindings testing mode: mpv --input-test --force-window --idle
+#
+# Use 'ignore' to unbind a key fully (e.g. 'ctrl+a ignore').
+#
+# Strings need to be quoted and escaped:
+# KEY show-text "This is a single backslash: \\ and a quote: \" !"
+#
+# You can use modifier-key combinations like Shift+Left or Ctrl+Alt+x with
+# the modifiers Shift, Ctrl, Alt and Meta (may not work on the terminal).
+#
+# The default keybindings are hardcoded into the mpv binary.
+# You can disable them completely with: --no-input-default-bindings
+
+# Developer note:
+# On compilation, this file is baked into the mpv binary, and all lines are
+# uncommented (unless '#' is followed by a space) - thus this file defines the
+# default key bindings.
+
+# If this is enabled, treat all the following bindings as default.
+#default-bindings start
+
+#MBTN_LEFT ignore # don't do anything
+#MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen on/off
+#MBTN_RIGHT cycle pause # toggle pause on/off
+
+# Mouse wheels, touchpad or other input devices that have axes
+# if the input devices supports precise scrolling it will also scale the
+# numeric value accordingly
+#WHEEL_UP seek 10
+#WHEEL_DOWN seek -10
+#WHEEL_LEFT add volume -2
+#WHEEL_RIGHT add volume 2
+
+## Seek units are in seconds, but note that these are limited by keyframes
+#RIGHT seek 5
+#LEFT seek -5
+#UP seek 60
+#DOWN seek -60
+# Do smaller, always exact (non-keyframe-limited), seeks with shift.
+# Don't show them on the OSD (no-osd).
+#Shift+RIGHT no-osd seek 1 exact
+#Shift+LEFT no-osd seek -1 exact
+#Shift+UP no-osd seek 5 exact
+#Shift+DOWN no-osd seek -5 exact
+# Skip to previous/next subtitle (subject to some restrictions; see manpage)
+#Ctrl+LEFT no-osd sub-seek -1
+#Ctrl+RIGHT no-osd sub-seek 1
+# Adjust timing to previous/next subtitle
+#Ctrl+Shift+LEFT sub-step -1
+#Ctrl+Shift+RIGHT sub-step 1
+# Move video rectangle
+#Alt+left add video-pan-x 0.1
+#Alt+right add video-pan-x -0.1
+#Alt+up add video-pan-y 0.1
+#Alt+down add video-pan-y -0.1
+# Zoom/unzoom video
+#Alt++ add video-zoom 0.1
+#Alt+- add video-zoom -0.1
+# Reset video zoom/pan settings
+#Alt+BS set video-zoom 0 ; set video-pan-x 0 ; set video-pan-y 0
+#PGUP add chapter 1 # skip to next chapter
+#PGDWN add chapter -1 # skip to previous chapter
+#Shift+PGUP seek 600
+#Shift+PGDWN seek -600
+#[ multiply speed 1/1.1 # scale playback speed
+#] multiply speed 1.1
+#{ multiply speed 0.5
+#} multiply speed 2.0
+#BS set speed 1.0 # reset speed to normal
+#Shift+BS revert-seek # undo previous (or marked) seek
+#Shift+Ctrl+BS revert-seek mark # mark position for revert-seek
+#q quit
+#Q quit-watch-later
+#q {encode} quit 4
+#ESC set fullscreen no
+#ESC {encode} quit 4
+#p cycle pause # toggle pause/playback mode
+#. frame-step # advance one frame and pause
+#, frame-back-step # go back by one frame and pause
+#SPACE cycle pause
+#> playlist-next # skip to next file
+#ENTER playlist-next # skip to next file
+#< playlist-prev # skip to previous file
+#O no-osd cycle-values osd-level 3 1 # cycle through OSD mode
+#o show-progress
+#P show-progress
+#i script-binding stats/display-stats
+#I script-binding stats/display-stats-toggle
+#z add sub-delay -0.1 # subtract 100 ms delay from subs
+#Z add sub-delay +0.1 # add
+#x add sub-delay +0.1 # same as previous binding (discouraged)
+#ctrl++ add audio-delay 0.100 # this changes audio/video sync
+#ctrl+- add audio-delay -0.100
+#9 add volume -2
+#/ add volume -2
+#0 add volume 2
+#* add volume 2
+#m cycle mute
+#1 add contrast -1
+#2 add contrast 1
+#3 add brightness -1
+#4 add brightness 1
+#5 add gamma -1
+#6 add gamma 1
+#7 add saturation -1
+#8 add saturation 1
+#Alt+0 set window-scale 0.5
+#Alt+1 set window-scale 1.0
+#Alt+2 set window-scale 2.0
+# toggle deinterlacer (automatically inserts or removes required filter)
+#d cycle deinterlace
+#r add sub-pos -1 # move subtitles up
+#R add sub-pos +1 # down
+#t add sub-pos +1 # same as previous binding (discouraged)
+#v cycle sub-visibility
+# stretch SSA/ASS subtitles with anamorphic videos to match historical
+#V cycle sub-ass-vsfilter-aspect-compat
+# switch between applying no style overrides to SSA/ASS subtitles, and
+# overriding them almost completely with the normal subtitle style
+#u cycle-values sub-ass-override "force" "no"
+#j cycle sub # cycle through subtitles
+#J cycle sub down # ...backwards
+#SHARP cycle audio # switch audio streams
+#_ cycle video
+#T cycle ontop # toggle video window ontop of other windows
+#f cycle fullscreen # toggle fullscreen
+#s async screenshot # take a screenshot
+#S async screenshot video # ...without subtitles
+#Ctrl+s async screenshot window # ...with subtitles and OSD, and scaled
+#Alt+s screenshot each-frame # automatically screenshot every frame
+#w add panscan -0.1 # zoom out with -panscan 0 -fs
+#W add panscan +0.1 # in
+#e add panscan +0.1 # same as previous binding (discouraged)
+# cycle video aspect ratios; "-1" is the container aspect
+#A cycle-values video-aspect "16:9" "4:3" "2.35:1" "-1"
+#POWER quit
+#PLAY cycle pause
+#PAUSE cycle pause
+#PLAYPAUSE cycle pause
+#STOP quit
+#FORWARD seek 60
+#REWIND seek -60
+#NEXT playlist-next
+#PREV playlist-prev
+#VOLUME_UP add volume 2
+#VOLUME_DOWN add volume -2
+#MUTE cycle mute
+#CLOSE_WIN quit
+#CLOSE_WIN {encode} quit 4
+#E cycle edition # next edition
+#l ab-loop # Set/clear A-B loop points
+#L cycle-values loop-file "inf" "no" # toggle infinite looping
+#ctrl+c quit 4
+#DEL script-binding osc/visibility # cycle OSC display
+#ctrl+h cycle-values hwdec "auto" "no" # cycle hardware decoding
+#F8 show_text ${playlist} # show playlist
+#F9 show_text ${track-list} # show list of audio/sub streams
+
+# Apple Remote section
+#AR_PLAY cycle pause
+#AR_PLAY_HOLD quit
+#AR_CENTER cycle pause
+#AR_CENTER_HOLD quit
+#AR_NEXT seek 10
+#AR_NEXT_HOLD seek 120
+#AR_PREV seek -10
+#AR_PREV_HOLD seek -120
+#AR_MENU show-progress
+#AR_MENU_HOLD cycle mute
+#AR_VUP add volume 2
+#AR_VUP_HOLD add chapter 1
+#AR_VDOWN add volume -2
+#AR_VDOWN_HOLD add chapter -1
+
+#
+# Legacy bindings (may or may not be removed in the future)
+#
+#! add chapter -1 # skip to previous chapter
+#@ add chapter 1 # next
+
+#
+# Not assigned by default
+# (not an exhaustive list of unbound commands)
+#
+
+# ? add sub-scale +0.1 # increase subtitle font size
+# ? add sub-scale -0.1 # decrease subtitle font size
+# ? cycle angle # switch DVD/Bluray angle
+# ? cycle sub-forced-only # toggle DVD forced subs
+# ? cycle program # cycle transport stream programs
+# ? stop # stop playback (quit or enter idle mode)
+CTRL+1 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Upscale_CNN_L_x2_Denoise.glsl:~~/shaders/Anime4K_Auto_Downscale_Pre_x4.glsl:~~/shaders/Anime4K_Upscale_CNN_M_x2_Deblur.glsl"; show-text "Anime4k: 480/720p (Faithful)"
+CTRL+2 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Upscale_CNN_L_x2_Denoise.glsl:~~/shaders/Anime4K_Auto_Downscale_Pre_x4.glsl:~~/shaders/Anime4K_DarkLines_HQ.glsl:~~/shaders/Anime4K_ThinLines_HQ.glsl:~~/shaders/Anime4K_Upscale_CNN_M_x2_Deblur.glsl"; show-text "Anime4k: 480/720p (Perceptual Quality)"
+CTRL+3 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Upscale_CNN_L_x2_Denoise.glsl:~~/shaders/Anime4K_Auto_Downscale_Pre_x4.glsl:~~/shaders/Anime4K_Deblur_DoG.glsl:~~/shaders/Anime4K_DarkLines_HQ.glsl:~~/shaders/Anime4K_ThinLines_HQ.glsl:~~/shaders/Anime4K_Upscale_CNN_M_x2_Deblur.glsl"; show-text "Anime4k: 480/720p (Perceptual Quality and Deblur)"
+CTRL+4 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Denoise_Bilateral_Mode.glsl:~~/shaders/Anime4K_Upscale_CNN_M_x2_Deblur.glsl"; show-text "Anime4k: 1080p (Faithful)"
+CTRL+5 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Denoise_Bilateral_Mode.glsl:~~/shaders/Anime4K_DarkLines_HQ.glsl:~~/shaders/Anime4K_ThinLines_HQ.glsl:~~/shaders/Anime4K_Upscale_CNN_M_x2_Deblur.glsl"; show-text "Anime4k: 1080p (Perceptual Quality)"
+CTRL+6 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Denoise_Bilateral_Mode.glsl:~~/shaders/Anime4K_Deblur_DoG.glsl:~~/shaders/Anime4K_DarkLines_HQ.glsl:~~/shaders/Anime4K_ThinLines_HQ.glsl:~~/shaders/Anime4K_Upscale_CNN_M_x2_Deblur.glsl"; show-text "Anime4k: 1080p (Perceptual Quality and Deblur)"
+CTRL+0 no-osd change-list glsl-shaders clr ""; show-text "GLSL shaders cleared"
+
+F1 vf toggle rotate=1
diff --git a/mpv/mpv.conf b/mpv/mpv.conf
new file mode 100644
index 0000000..7c8a68f
--- /dev/null
+++ b/mpv/mpv.conf
@@ -0,0 +1,65 @@
+##################
+# video settings #
+##################
+#The default profile which sets some recommended settings
+#profile=gpu-hq
+#force-window=yes
+
+#The called API
+#Use "opengl" if you have compatibility issues
+#vo=gpu
+#gpu-api=opengl
+#fbo-format=rgba16f
+
+#Decoding API for 8bit h264 (or whatever your CPU supports) content
+#Only should be used when you get many frame drops
+#hwdec=auto
+
+#Don't close the player after finishing the video
+keep-open=yes
+
+#The last position of your video is saved when quitting mpv
+save-position-on-quit=yes
+
+#Start mpv with a % smaller resolution of your screen
+#autofit=50%
+
+#Force seeking (if seeking doesn't work)
+force-seekable=yes
+# OSC
+osc=no
+osd-bar=no
+border=no
+
+##################
+# audio/subtitles settings #
+##################
+
+volume=70
+# Display English subtitles if available.
+slang=eng,en,en-US,enUS
+blend-subtitles=yes
+
+# Play Finnish audio if available, fall back to English otherwise.
+alang=jpn,ja,jp,en
+
+# Screenshots
+#Output format of screenshots
+screenshot-format=png
+
+#Same output bitdepth as the video
+#Set it "no" if you want to save disc space
+screenshot-high-bit-depth=yes
+
+#Compression of the PNG picture (1-9)
+#Higher value means better compression, but takes more time
+screenshot-png-compression=1
+
+#Quality of JPG pictures (0-100)
+#Higher value means better quality
+screenshot-jpeg-quality=95
+#Output directory
+screenshot-directory="~/pics/screenshots"
+
+#Name format you want to save the pictures
+screenshot-template="%f-%wH.%wM.%wS.%wT-#%#00n"
diff --git a/mpv/scripts/autoloop.lua b/mpv/scripts/autoloop.lua
new file mode 100644
index 0000000..1c2794d
--- /dev/null
+++ b/mpv/scripts/autoloop.lua
@@ -0,0 +1,53 @@
+-- mpv issue 5222
+-- Automatically set loop-file=inf for duration <= given length. Default is 5s
+-- Use autoloop_duration=n in script-opts/autoloop.conf to set your preferred length
+-- Alternatively use script-opts=autoloop-autoloop_duration=n in mpv.conf (takes priority)
+
+
+require 'mp.options'
+
+function getOption()
+ -- Use recommended way to get options
+ local options = {autoloop_duration = 5}
+ read_options(options)
+ autoloop_duration = options.autoloop_duration
+
+
+ -- Keep old way just for compatibility (remove lines 15-27 soon)
+ if autoloop_duration ~= 5 then
+ return
+ end
+
+ local opt = tonumber(mp.get_opt("autoloop-duration"))
+ if not opt then
+ return
+ end
+ print("Depracted configuration! Please use script-opts directory to set auto_loop duration")
+ print("Or use 'script-opts=autoloop-autoloop_duration' in mpv.conf")
+ autoloop_duration = opt
+ -- Remove lines 15-27 soon
+end
+
+function set_loop()
+ local duration = mp.get_property_native("duration")
+
+ -- Checks whether the loop status was changed for the last file
+ was_loop = mp.get_property_native("loop-file")
+
+ -- Cancel operation if there is no file duration
+ if not duration then
+ return
+ end
+
+ -- Loops file if was_loop is false, and file meets requirements
+ if not was_loop and duration <= autoloop_duration then
+ mp.set_property_native("loop-file", true)
+ -- Unloops file if was_loop is true, and file does not meet requirements
+ elseif was_loop and duration > autoloop_duration then
+ mp.set_property_native("loop-file", false)
+ end
+end
+
+
+getOption()
+mp.register_event("file-loaded", set_loop)
diff --git a/mpv/scripts/autosave.lua b/mpv/scripts/autosave.lua
new file mode 100644
index 0000000..c86cdb1
--- /dev/null
+++ b/mpv/scripts/autosave.lua
@@ -0,0 +1,38 @@
+-- autosave.lua
+--
+-- Periodically saves "watch later" data during playback, rather than only saving on quit.
+-- This lets you easily recover your position in the case of an ungraceful shutdown of mpv (crash, power failure, etc.).
+--
+-- You can configure the save period by creating a "lua-settings" directory inside your mpv configuration directory.
+-- Inside the "lua-settings" directory, create a file named "autosave.conf".
+-- The save period can be set like so:
+--
+-- save_period=60
+--
+-- This will set the save period to once every 60 seconds of playback, time while paused is not counted towards the save period timer.
+-- The default save period is 30 seconds.
+local options = require 'mp.options'
+
+local o = {
+ save_period = 30
+}
+
+options.read_options(o)
+
+local mp = require 'mp'
+
+local function save()
+ mp.command("write-watch-later-config")
+end
+
+local save_period_timer = mp.add_periodic_timer(o.save_period, save)
+
+local function pause(name, paused)
+ if paused then
+ save_period_timer:stop()
+ else
+ save_period_timer:resume()
+ end
+end
+
+mp.observe_property("pause", "bool", pause) \ No newline at end of file
diff --git a/mpv/scripts/autospeed.lua b/mpv/scripts/autospeed.lua
new file mode 100644
index 0000000..465ccdf
--- /dev/null
+++ b/mpv/scripts/autospeed.lua
@@ -0,0 +1,420 @@
+--[[
+ See script details on https://github.com/kevinlekiller/mpv_scripts
+
+ Valid --script-opts are (they are all optional):
+ autospeed-xrandr=false true/false - Use xrandr to change display refresh rate?.
+ autospeed-speed=false true/false - Adjust speed of the video?
+ autospeed-display=HDMI1 - Use specified xrandr display, find with xrandr -q, if set to "auto", uses the primary monitor.
+ autospeed-exitmode=0x48 - Changes the monitor mode (refresh rate) when exiting mpv.
+ autospeed-exitmode=false Don't change the mode when exiting. If autospeed-exitmode is not set, this is the default.
+ autospeed-exitmode=auto Change the mode to the mode used when mpv started.
+ autospeed-exitmode=0x48 Revert to specified mode when exiting mpv. Find a mode using xrandr --verbose (it should look something like 0x123).
+ autospeed-interlaced=false true/false - Allow using a interlaced mode when switching refresh rates with xrandr?
+ autospeed-mblacklist=false - List of modes to blacklist.
+ Modes in this list will be ignored.
+ If more than one mode is specified, seperate them by semicolon.
+ Example: autospeed-mblacklist="0x128;0x2fa"
+ autospeed-minspeed=0.9 Number - Minimum allowable speed to play video at.
+ autospeed-maxspeed=1.1 Number - Maximum allowable speed to play video at.
+ autospeed-osd=true true/false - Enable OSD.
+ autospeed-osdtime=10 Number - How many seconds the OSD will be shown.
+ autospeed-osdkey=y - Key to press to show the OSD.
+ autospeed-estfps=false true/false - Calculate/change speed if a video has a variable fps at the cost of higher CPU usage (most videos have a fixed fps).
+ autospeed-spause true/false - Pause video while switching display modes.
+ Number - If you set this a number, it will pause for that amount of seconds.
+
+ Example: mpv file.mkv --script-opts=autospeed-xrandr=true,autospeed-speed=false,autospeed-minspeed=0.8
+--]]
+--[[
+ Copyright (C) 2015-2017 kevinlekiller
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ https://www.gnu.org/licenses/gpl-2.0.html
+--]]
+local _global = {
+ osd_start = mp.get_property_osd("osd-ass-cc/0"),
+ osd_end = mp.get_property_osd("osd-ass-cc/1"),
+ utils = require 'mp.utils',
+ modes = {},
+ modeCache = {},
+ lastDrr = 0,
+ speedCache = {},
+ next = next,
+}
+
+function round(number)
+ return math.floor(number + 0.5)
+end
+
+function osdEcho()
+ if (_global.options["osd"] ~= true) then
+ return
+ end
+ setOSD()
+ if (_global.temp["output"] ~= nil) then
+ mp.osd_message(_global.temp["output"], _global.options["osdtime"])
+ end
+end
+
+function getOptions()
+ _global.options = {
+ ["xrandr"] = false,
+ ["speed"] = false,
+ ["display"] = "auto",
+ ["exitmode"] = "auto",
+ ["interlaced"] = false,
+ ["mblacklist"] = "false",
+ ["minspeed"] = 0.9,
+ ["maxspeed"] = 1.1,
+ ["osd"] = false,
+ ["osdtime"] = 10,
+ ["osdkey"] = "y",
+ ["estfps"] = false,
+ ["spause"] = false,
+ }
+ for key, value in pairs(_global.options) do
+ local opt = mp.get_opt("autospeed-" .. key)
+ if (opt ~= nil) then
+ if (key == "xrandr" or key == "osd" or key == "estfps" or key == "interlaced" or key == "speed") then
+ if (opt == "true") then
+ _global.options[key] = true
+ end
+ elseif (key == "minspeed" or key == "maxspeed" or key == "osdtime") then
+ opt = tonumber(opt)
+ if (opt ~= nil) then
+ _global.options[key] = opt
+ end
+ elseif (key == "spause") then
+ if (opt == "true") then
+ _global.options[key] = 0
+ else
+ opt = tonumber(opt)
+ if (opt ~= nil) then
+ _global.options[key] = opt
+ end
+ end
+ else
+ _global.options[key] = opt
+ end
+ end
+ end
+ if (_global.options["mblacklist"] == "false") then
+ _global.options["mblacklist"] = false
+ else
+ local tmp_blacklist = {}
+ for blacklist in string.gmatch(_global.options["mblacklist"], '[0-9a-zA-Z]+') do
+ tmp_blacklist[blacklist] = true
+ end
+ _global.options["mblacklist"] = tmp_blacklist
+ end
+end
+getOptions()
+
+function main(name, fps)
+ if (fps == nil) then
+ return
+ end
+ _global.temp["fps"] = fps
+ findRefreshRate()
+ if (_global.options["speed"] == true) then
+ determineSpeed()
+ if (_global.temp["speed"] >= _global.options["minspeed"] and _global.temp["speed"] <= _global.options["maxspeed"]) then
+ mp.set_property_number("speed", _global.temp["speed"])
+ else
+ _global.temp["speed"] = _global.confSpeed
+ end
+ else
+ _global.temp["speed"] = _global.confSpeed
+ end
+end
+
+function setOSD()
+ _global.temp["output"] = (_global.osd_start ..
+ "{\\b1}Original monitor refresh rate{\\b0}\\h\\h" .. _global.temp["start_drr"] .. "Hz\\N" ..
+ "{\\b1}Current monitor refresh rate{\\b0}\\h\\h" .. _global.temp["drr"] .. "Hz\\N" ..
+ "{\\b1}Original video fps{\\b0}\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h" .. _global.temp["fps"] .. "fps\\N" ..
+ "{\\b1}Current video fps{\\b0}\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h\\h" .. (_global.temp["fps"] * _global.temp["speed"]) .. "fps\\N" ..
+ "{\\b1}Original mpv speed setting{\\b0}\\h\\h\\h\\h\\h\\h" .. _global.confSpeed .. "x\\N" ..
+ "{\\b1}Current mpv speed setting{\\b0}\\h\\h\\h\\h\\h\\h" .. _global.temp["speed"] .. "x" ..
+ _global.osd_end
+ )
+end
+
+function determineSpeed()
+ local id = _global.temp["drr"] .. _global.temp["fps"]
+ if (_global.speedCache[id] ~= nil) then
+ _global.temp["speed"] = _global.speedCache[id]
+ return
+ end
+ if (_global.temp["drr"] > _global.temp["fps"]) then
+ local difference = (_global.temp["drr"] / _global.temp["fps"])
+ if (difference >= 2) then
+ -- fps = 24fps, drr = 60hz
+ -- difference = 60hz/24fps = 3 rounded
+ -- 24fps * 3 = 72fps
+ -- 60hz / 72fps = 0.833333333333 speed
+ -- 72fps * 0.833333333333 = 60fps
+ _global.temp["speed"] = (_global.temp["drr"] / (_global.temp["fps"] * round(difference)))
+ else
+ -- fps = 50fps, drr = 60hz
+ -- 60hz / 50fps = 1.2 speed
+ -- 50fps * 1.2 speed = 60fps
+
+ -- fps = 59.94fps, drr = 60hz
+ -- 60hz / 59.94fps = 1.001001001001001 speed
+ -- 59.94fps * 1.001001001001001 = 60fps
+ _global.temp["speed"] = difference
+ end
+ elseif (_global.temp["drr"] < _global.temp["fps"]) then
+ local difference = (_global.temp["fps"] / _global.temp["drr"])
+ if (difference >= 2) then
+ -- fps = 120fps, drr = 25hz
+ -- difference = 120fps/25hz = 5 rounded
+ -- 120fps/5 = 24fps ; 25hz / 24fps = 1.04166666667 speed
+ -- 24fps * 1.04166666667 speed = 25fps
+ _global.temp["speed"] = (_global.temp["drr"] / (_global.temp["fps"] / round(difference)))
+ else
+ -- fps = 60fps, drr = 50hz
+ -- difference = 50hz / 60fps = 0.833333333333 speed
+ -- 60fps * 0.833333333333 speed = 50fps
+
+ -- fps = 60fps, drr = 59.94hz
+ -- difference = 59.94hz / 60fps = 0.999 speed
+ -- 60fps * 0.999 speed = 59.94fps
+ _global.temp["speed"] = (_global.temp["drr"] / _global.temp["fps"])
+ end
+ elseif (_global.temp["drr"] == _global.temp["fps"]) then
+ _global.temp["speed"] = 1
+ end
+ _global.speedCache[id] = _global.temp["speed"]
+end
+
+function findRefreshRate()
+ -- This is to prevent a system call if the screen refresh / video fps has not changed.
+ if (_global.temp["drr"] == _global.lastDrr) then
+ return
+ elseif (_global.modeCache[_global.temp["drr"]] ~= nil) then
+ setXrandrRate(_global.modeCache[_global.temp["drr"]])
+ return
+ end
+ if (_global.options["xrandr"] ~= true or getXrandrModes() == false) then
+ return
+ end
+ -- If the current monitor rate is already a multiple, don't change the mode.
+ if (_global.temp["fps"] <= _global.temp["drr"] and _global.temp["drr"] % _global.temp["fps"] == 0) then
+ return
+ end
+ local round_fps = round(_global.temp["fps"])
+ local iterator = 1
+ if (_global.temp["maxclock"] > round_fps) then
+ iterator = round(_global.temp["maxclock"] / round_fps)
+ elseif (_global.temp["maxclock"] < round_fps) then
+ iterator = round(round_fps / _global.temp["maxclock"])
+ else
+ setXrandrRate(_global.modes[_global.temp["maxclock"]])
+ return
+ end
+ local smallest = 0
+ local foundMode = false
+ for rate, mode in pairs(_global.modes) do
+ local min = (rate * _global.options["minspeed"])
+ local max = (rate * _global.options["maxspeed"])
+ for multiplier = 1, iterator do
+ local multiplied_fps = (multiplier * round_fps)
+ if (multiplied_fps >= min and multiplied_fps <= max) then
+ if (multiplied_fps < rate) then
+ local difference = (rate - multiplied_fps)
+ if (smallest == 0 or difference < smallest) then
+ smallest = difference
+ foundMode = mode
+ end
+ elseif (multiplied_fps > rate) then
+ local difference = (multiplied_fps - rate)
+ if (smallest == 0 or difference < smallest) then
+ smallest = difference
+ foundMode = mode
+ end
+ else
+ setXrandrRate(mode)
+ return
+ end
+ end
+ end
+ end
+ if (foundMode ~= false) then
+ setXrandrRate(foundMode)
+ end
+end
+
+function setXrandrRate(mode)
+ if (mode == _global.temp["currentmode"]) then
+ return
+ end
+ local vars = {vid = nil, time_pos = nil, vdpau = (mp.get_property("options/vo") == "vdpau" or mp.get_property("options/hwdec") == "vdpau")}
+ if (_global.options["spause"]) then
+ mp.set_property("pause", "yes")
+ end
+ if (vars.vdpau) then
+ vars.vid = mp.get_property("vid")
+ vars.time_pos = mp.get_property("time-pos")
+ mp.set_property("vid", "no")
+ end
+ _global.utils.subprocess({["cancellable"] = false, ["args"] = {[1] = "xrandr", [2] = "--output", [3] = _global.options["display"], [4] = "--mode", [5] = mode,}})
+ if (_global.options["spause"]) then
+ if (tonumber(_global.options["spause"]) ~= nil and _global.options["spause"] > 0) then
+ _global.utils.subprocess({["cancellable"] = false, ["args"] = {[1] = "sleep", [2] = _global.options["spause"]}})
+ end
+ mp.set_property("pause", "no")
+ end
+ if (vars.vdpau) then
+ mp.set_property("vid", vars.vid)
+ if (vars.time_pos ~= nil) then
+ mp.commandv("seek", vars.time_pos, "absolute", "keyframes")
+ end
+ end
+ _global.utils.subprocess({["cancellable"] = false, ["args"] = {[1] = "sleep", [2] = "0.5"}})
+ _global.temp["drr"] = mp.get_property_native("display-fps")
+ _global.modeCache[_global.temp["drr"]] = mode
+ _global.lastDrr = _global.temp["drr"]
+ _global.temp["currentmode"] = mode
+end
+
+function getXrandrModes()
+ if (_global.next(_global.modes) ~= nil) then
+ return true
+ end
+ if not (_global.modes) then
+ return false
+ end
+ local vars = {
+ handle = assert(io.popen("xrandr --verbose")),
+ foundDisp = false,
+ foundRes = false,
+ count = 0,
+ resolution,
+ }
+ if (_global.options["display"] == "auto") then
+ vars.disp = "^%S+%sconnected%sprimary"
+ else
+ vars.disp = "^" .. string.gsub(_global.options["display"], "%-", "%%-")
+ end
+ _global.temp["maxclock"] = 0
+ for line in vars.handle:lines() do
+ if (vars.foundDisp == false and string.match(line, vars.disp) ~= nil) then -- Check if the display name (ie HDMI1) matches the one in the config.
+ if (string.find(line, "disconnected") ~= nil) then
+ break -- Wrong display name was given.
+ else
+ local res = string.match(line, vars.disp .. "%D+([%dx]+)") -- Find current monitor resolution.
+ if (res ~= nil and res ~= "") then
+ if (_global.options["display"] == "auto") then
+ _global.options["display"] = string.match(line, "^%S+")
+ end
+ vars.resolution = res
+ vars.foundDisp = true
+ else
+ break -- Could not find display resolution.
+ end
+ end
+ elseif (vars.foundDisp == true) then -- We found the display name.
+ if (vars.foundRes == false and string.match(line, "^%s+" .. vars.resolution) ~= nil) then -- Check if mode uses current display resolution.
+ vars.foundRes = true
+ end
+ if (vars.foundRes == true) then -- We found a matching screen resolution.
+ vars.count = vars.count + 1
+ if (vars.count == 1) then -- Log the mode name.
+ vars.mode = string.match(line, "%((.+)%)%s+[%d.]+MHz")
+ if (_global.temp["origmode"] == nil) then
+ if (string.find(line, "%*current") ~= nil) then
+ _global.temp["origmode"] = vars.mode
+ _global.temp["currentmode"] = vars.mode
+ end
+ end
+ vars.interlaced = false
+ if (string.find(line, "Interlace") ~= nil) then
+ vars.interlaced = true
+ end
+ elseif (vars.count == 2) then
+
+ elseif (vars.count == 3) then
+ if ((_global.options["interlaced"] == false and vars.interlaced == true) or (_global.options["mblacklist"] ~= false and _global.options["mblacklist"][vars.mode] ~= nil)) then
+ -- ignore these modes
+ else
+ local clock = string.match(line, "total%s+%d+.+clock%s+([%d.]+)[KkHh]+z")
+ clock = round(clock)
+ if (_global.temp["maxclock"] < clock) then
+ _global.temp["maxclock"] = clock
+ end
+ _global.modes[clock] = vars.mode
+ end
+ vars.count = 0 -- Reset variables to look for another matching resolution.
+ vars.foundRes = false
+ end
+ elseif (string.match(line, "^%S") ~= nil) then
+ break -- We reached the next display or EOF.
+ end
+ end
+ end
+ vars.handle:close()
+ if (_global.next(_global.modes) == nil) then
+ _global.modes = false
+ return false
+ end
+end
+
+function start()
+ mp.unobserve_property(start)
+ _global.temp = {}
+ _global.temp["start_drr"] = mp.get_property_native("display-fps")
+ if not (_global.temp["start_drr"]) then
+ return
+ end
+ _global.temp["drr"] = _global.temp["start_drr"]
+ if not (_global.confSpeed) then
+ _global.confSpeed = mp.get_property_native("speed")
+ end
+ local test = mp.get_property("container-fps")
+ if (test == nil or test == "nil property unavailable") then
+ if (_global.options["estfps"] ~= true) then
+ return
+ end
+ test = mp.get_property("estimated-vf-fps")
+ if (test == nil or test == "nil property unavailable") then
+ return
+ end
+ mp.observe_property("estimated-vf-fps", "number", main)
+ else
+ mp.observe_property("container-fps", "number", main)
+ end
+ mp.add_key_binding(_global.options["osdkey"], mp.get_script_name(), osdEcho, {repeatable=true})
+ if (_global.options["xrandr"] == true and _global.options.exitmode ~= "false") then
+ function revertDrr()
+ if (_global.options["display"] ~= "auto") then
+ if (_global.options["exitmode"] == "auto" and _global.temp["origmode"] ~= nil) then
+ os.execute("xrandr --output " .. _global.options["display"] .. " --mode " .. _global.temp["origmode"] .. " &")
+ else
+ os.execute("xrandr --output " .. _global.options["display"] .. " --mode " .. _global.options["exitmode"] .. " &")
+ end
+ end
+ end
+ mp.register_event("shutdown", revertDrr)
+ end
+end
+
+-- Wait until we get a video fps.
+function check()
+ mp.observe_property("estimated-vf-fps", "string", start)
+end
+
+mp.register_event("file-loaded", check)
diff --git a/mpv/scripts/autosub.lua b/mpv/scripts/autosub.lua
new file mode 100644
index 0000000..56d49fa
--- /dev/null
+++ b/mpv/scripts/autosub.lua
@@ -0,0 +1,254 @@
+--=============================================================================
+-->> SUBLIMINAL PATH:
+--=============================================================================
+-- This script uses Subliminal to download subtitles,
+-- so make sure to specify your system's Subliminal location below:
+local subliminal = '/usr/bin/subliminal'
+--=============================================================================
+-->> SUBTITLE LANGUAGE:
+--=============================================================================
+-- Specify languages in this order:
+-- { 'language name', 'ISO-639-1', 'ISO-639-2' } !
+-- (See: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
+local languages = {
+-- If subtitles are found for the first language,
+-- other languages will NOT be downloaded,
+-- so put your preferred language first:
+ { 'English', 'en', 'eng' },
+ { 'Dutch', 'nl', 'dut' },
+-- { 'Spanish', 'es', 'spa' },
+-- { 'French', 'fr', 'fre' },
+-- { 'German', 'de', 'ger' },
+-- { 'Italian', 'it', 'ita' },
+-- { 'Portuguese', 'pt', 'por' },
+-- { 'Polish', 'pl', 'pol' },
+-- { 'Russian', 'ru', 'rus' },
+-- { 'Chinese', 'zh', 'chi' },
+-- { 'Arabic', 'ar', 'ara' },
+}
+--=============================================================================
+-->> PROVIDER LOGINS:
+--=============================================================================
+-- These are completely optional and not required
+-- for the functioning of the script!
+-- If you use any of these services, simply uncomment it
+-- and replace 'USERNAME' and 'PASSWORD' with your own:
+local logins = {
+-- { '--addic7ed', 'USERNAME', 'PASSWORD' },
+-- { '--legendastv', 'USERNAME', 'PASSWORD' },
+-- { '--opensubtitles', 'USERNAME', 'PASSWORD' },
+-- { '--subscenter', 'USERNAME', 'PASSWORD' },
+}
+--=============================================================================
+-->> ADDITIONAL OPTIONS:
+--=============================================================================
+local bools = {
+ auto = true, -- Automatically download subtitles, no hotkeys required
+ debug = false, -- Use `--debug` in subliminal command for debug output
+ force = true, -- Force download; will overwrite existing subtitle files
+ utf8 = true, -- Save all subtitle files as UTF-8
+}
+local excludes = {
+ -- Movies with a path containing any of these strings/paths
+ -- will be excluded from auto-downloading subtitles.
+ -- Full paths are also allowed, e.g.:
+ -- '/home/david/Videos',
+ 'no-subs-dl',
+}
+local includes = {
+ -- If anything is defined here, only the movies with a path
+ -- containing any of these strings/paths will auto-download subtitles.
+ -- Full paths are also allowed, e.g.:
+ -- '/home/david/Videos',
+}
+--=============================================================================
+local utils = require 'mp.utils'
+
+
+-- Download function: download the best subtitles in most preferred language
+function download_subs(language)
+ language = language or languages[1]
+ log('Searching ' .. language[1] .. ' subtitles ...', 30)
+
+ -- Build the `subliminal` command, starting with the executable:
+ local table = { args = { subliminal } }
+ local a = table.args
+
+ for _, login in ipairs(logins) do
+ a[#a + 1] = login[1]
+ a[#a + 1] = login[2]
+ a[#a + 1] = login[3]
+ end
+ if bools.debug then
+ -- To see `--debug` output start MPV from the terminal!
+ a[#a + 1] = '--debug'
+ end
+
+ a[#a + 1] = 'download'
+ if bools.force then
+ a[#a + 1] = '-f'
+ end
+ if bools.utf8 then
+ a[#a + 1] = '-e'
+ a[#a + 1] = 'utf-8'
+ end
+
+ a[#a + 1] = '-l'
+ a[#a + 1] = language[2]
+ a[#a + 1] = '-d'
+ a[#a + 1] = directory
+ a[#a + 1] = filename --> Subliminal command ends with the movie filename.
+
+ local result = utils.subprocess(table)
+
+ if string.find(result.stdout, 'Downloaded 1 subtitle') then
+ -- When multiple external files are present,
+ -- always activate the most recently downloaded:
+ mp.set_property('slang', language[2])
+ -- Subtitles are downloaded successfully, so rescan to activate them:
+ mp.commandv('rescan_external_files')
+ log(language[1] .. ' subtitles ready!')
+ return true
+ else
+ log('No ' .. language[1] .. ' subtitles found\n')
+ return false
+ end
+end
+
+-- Manually download second language subs by pressing 'n':
+function download_subs2()
+ download_subs(languages[2])
+end
+
+-- Control function: only download if necessary
+function control_downloads()
+ -- Make MPV accept external subtitle files with language specifier:
+ mp.set_property('sub-auto', 'fuzzy')
+ -- Set subtitle language preference:
+ mp.set_property('slang', languages[1][2])
+ mp.msg.warn('Reactivate external subtitle files:')
+ mp.commandv('rescan_external_files')
+ directory, filename = utils.split_path(mp.get_property('path'))
+
+ if not autosub_allowed() then
+ return
+ end
+
+ sub_tracks = {}
+ for _, track in ipairs(mp.get_property_native('track-list')) do
+ if track['type'] == 'sub' then
+ sub_tracks[#sub_tracks + 1] = track
+ end
+ end
+ if bools.debug then -- Log subtitle properties to terminal:
+ for _, track in ipairs(sub_tracks) do
+ mp.msg.warn('Subtitle track', track['id'], ':\n{')
+ for k, v in pairs(track) do
+ if type(v) == 'string' then v = '"' .. v .. '"' end
+ mp.msg.warn(' "' .. k .. '":', v)
+ end
+ mp.msg.warn('}\n')
+ end
+ end
+
+ for _, language in ipairs(languages) do
+ if should_download_subs_in(language) then
+ if download_subs(language) then return end -- Download successful!
+ else return end -- No need to download!
+ end
+ log('No subtitles were found')
+end
+
+-- Check if subtitles should be auto-downloaded:
+function autosub_allowed()
+ local duration = tonumber(mp.get_property('duration'))
+ local active_format = mp.get_property('file-format')
+
+ if not bools.auto then
+ mp.msg.warn('Automatic downloading disabled!')
+ return false
+ elseif duration < 900 then
+ mp.msg.warn('Video is less than 15 minutes\n' ..
+ '=> NOT auto-downloading subtitles')
+ return false
+ elseif directory:find('^http') then
+ mp.msg.warn('Automatic subtitle downloading is disabled for web streaming')
+ return false
+ elseif active_format:find('^cue') then
+ mp.msg.warn('Automatic subtitle downloading is disabled for cue files')
+ return false
+ else
+ local not_allowed = {'aiff', 'ape', 'flac', 'mp3', 'ogg', 'wav', 'wv'}
+
+ for _, file_format in pairs(not_allowed) do
+ if file_format == active_format then
+ mp.msg.warn('Automatic subtitle downloading is disabled for audio files')
+ return false
+ end
+ end
+
+ for _, exclude in pairs(excludes) do
+ local escaped_exclude = exclude:gsub('%W','%%%0')
+ local excluded = directory:find(escaped_exclude)
+
+ if excluded then
+ mp.msg.warn('This path is excluded from auto-downloading subs')
+ return false
+ end
+ end
+
+ for i, include in ipairs(includes) do
+ local escaped_include = include:gsub('%W','%%%0')
+ local included = directory:find(escaped_include)
+
+ if included then break
+ elseif i == #includes then
+ mp.msg.warn('This path is not included for auto-downloading subs')
+ return false
+ end
+ end
+ end
+
+ return true
+end
+
+-- Check if subtitles should be downloaded in this language:
+function should_download_subs_in(language)
+ for i, track in ipairs(sub_tracks) do
+ local subtitles = track['external'] and
+ 'subtitle file' or 'embedded subtitles'
+
+ if not track['lang'] and (track['external'] or not track['title'])
+ and i == #sub_tracks then
+ local status = track['selected'] and ' active' or ' present'
+ log('Unknown ' .. subtitles .. status)
+ mp.msg.warn('=> NOT downloading new subtitles')
+ return false -- Don't download if 'lang' key is absent
+ elseif track['lang'] == language[3] or track['lang'] == language[2] or
+ (track['title'] and track['title']:lower():find(language[3])) then
+ if not track['selected'] then
+ mp.set_property('sid', track['id'])
+ log('Enabled ' .. language[1] .. ' ' .. subtitles .. '!')
+ else
+ log(language[1] .. ' ' .. subtitles .. ' active')
+ end
+ mp.msg.warn('=> NOT downloading new subtitles')
+ return false -- The right subtitles are already present
+ end
+ end
+ mp.msg.warn('No ' .. language[1] .. ' subtitles were detected\n' ..
+ '=> Proceeding to download:')
+ return true
+end
+
+-- Log function: log to both terminal and MPV OSD (On-Screen Display)
+function log(string, secs)
+ secs = secs or 2.5 -- secs defaults to 2.5 when secs parameter is absent
+ mp.msg.warn(string) -- This logs to the terminal
+ mp.osd_message(string, secs) -- This logs to MPV screen
+end
+
+
+mp.add_key_binding('b', 'download_subs', download_subs)
+mp.add_key_binding('n', 'download_subs2', download_subs2)
+mp.register_event('file-loaded', control_downloads)
diff --git a/mpv/scripts/autosubsync.lua b/mpv/scripts/autosubsync.lua
new file mode 100644
index 0000000..fb3a6c3
--- /dev/null
+++ b/mpv/scripts/autosubsync.lua
@@ -0,0 +1,44 @@
+-- default keybinding: n
+-- add the following to your input.conf to change the default keybinding:
+-- keyname script_binding auto_sync_subs
+local utils = require 'mp.utils'
+
+function display_error()
+ mp.msg.warn("Subtitle synchronization failed: ")
+ mp.osd_message("Subtitle synchronization failed")
+end
+
+-- Courtesy of https://stackoverflow.com/questions/4990990/check-if-a-file-exists-with-lua
+function file_exists(filepath)
+ local f=io.open(filepath,"r")
+ if f~=nil then io.close(f) return true else return false end
+end
+
+function sync_sub_fn()
+ path = mp.get_property("path")
+ srt_path = string.gsub(path, "%.%w+$", ".srt")
+ if file_exists(srt_path)==false then
+ mp.msg.warn("Couldn't find",srt_path)
+ display_error()
+ do return end
+ end
+ subsync = "/home/user/.local/bin/ffsubsync" -- use 'which ffsubsync' to find the path
+ t = {}
+ t.args = {subsync, path, "-i",srt_path,"-o",srt_path}
+
+ mp.osd_message("Sync subtitle...")
+ mp.msg.info("Starting ffsubsync...")
+ res = utils.subprocess(t)
+ if res.error == nil then
+ if mp.commandv("sub_add", srt_path) then
+ mp.msg.info("Subtitle updated")
+ mp.osd_message("Subtitle at'" .. srt_path .. "' synchronized")
+ else
+ display_error()
+ end
+ else
+ display_error()
+ end
+end
+
+mp.add_key_binding("n", "auto_sync_subs", sync_sub_fn)
diff --git a/mpv/scripts/quack.lua b/mpv/scripts/quack.lua
new file mode 100644
index 0000000..ed09ce1
--- /dev/null
+++ b/mpv/scripts/quack.lua
@@ -0,0 +1,44 @@
+local options = require 'mp.options'
+
+local o = {
+ ducksecs = 2, -- lol
+ duckratio = 0.5
+}
+options.read_options(o)
+
+local duck_progress = 0
+local duck_timer = nil
+local orig_vol = nil
+
+function update_quack()
+ duck_progress = duck_progress + 1
+ if duck_progress >= o.ducksecs * 10 then
+ duck_timer:kill()
+ end
+ mp.set_property_number("volume", math.min(orig_vol, orig_vol * o.duckratio + orig_vol * (1 - o.duckratio) * (duck_progress / (o.ducksecs * 10))))
+ -- print(mp.get_property_number("volume"))
+end
+
+function engage_ducking(name, val)
+ pos = mp.get_property_number("time-pos")
+ if val == nil or val == false then
+ return
+ end
+ if pos == 0 then
+ return
+ end
+ duck_progress = 0
+ if duck_timer == nil then
+ duck_timer = mp.add_periodic_timer(0.1, update_quack)
+ orig_vol = mp.get_property_number("volume")
+ update_quack() -- fire for immediate effect
+ else
+ if duck_timer:is_enabled() == false then
+ orig_vol = mp.get_property_number("volume")
+ duck_timer:resume()
+ update_quack()
+ end
+ end
+end
+
+mp.observe_property("seeking", "bool", engage_ducking)
diff --git a/mpv/scripts/uosc.lua b/mpv/scripts/uosc.lua
new file mode 100644
index 0000000..a2a5074
--- /dev/null
+++ b/mpv/scripts/uosc.lua
@@ -0,0 +1,3230 @@
+--[[
+
+uosc 2.10.1 - 2020-Jun-20 | https://github.com/darsain/uosc
+
+Minimalist cursor proximity based UI for MPV player.
+
+uosc replaces the default osc UI, so that has to be disabled first.
+Place these options into your `mpv.conf` file:
+
+```
+# required so that the 2 UIs don't fight each other
+osc=no
+# uosc provides its own seeking/volume indicators, so you also don't need this
+osd-bar=no
+# uosc will draw its own window controls if you disable window border
+border=no
+```
+
+Options go in `script-opts/uosc.conf`. Defaults:
+
+```
+# timeline size when fully retracted, 0 will hide it completely
+timeline_size_min=2
+# timeline size when fully expanded, in pixels, 0 to disable
+timeline_size_max=40
+# same as ^ but when in fullscreen
+timeline_size_min_fullscreen=0
+timeline_size_max_fullscreen=60
+# same thing as calling toggle-progress command once on startup
+timeline_start_hidden=no
+# timeline opacity
+timeline_opacity=0.8
+# top (and bottom in no-border mode) border of background color to help visually
+# separate elapsed bar from a video of similar color or desktop background
+timeline_border=1
+# when scrolling above timeline, wheel will seek by this amount of seconds
+timeline_step=5
+# display seekable buffered ranges for streaming videos, syntax `color:opacity`,
+# color is an BBGGRR hex code, set to `none` to disable
+timeline_cached_ranges=345433:0.5
+# floating number font scale adjustment
+timeline_font_scale=1
+
+# timeline chapters style: none, dots, lines, lines-top, lines-bottom
+chapters=dots
+chapters_opacity=0.3
+
+# where to display volume controls: none, left, right
+volume=right
+volume_size=40
+volume_size_fullscreen=60
+volume_opacity=0.8
+volume_border=1
+volume_step=1
+volume_font_scale=1
+
+# playback speed widget: mouse drag or wheel to change, click to reset
+speed=no
+speed_size=46
+speed_size_fullscreen=68
+speed_opacity=1
+speed_step=0.1
+speed_font_scale=1
+
+# controls all menus, such as context menu, subtitle loader/selector, etc
+menu_item_height=36
+menu_item_height_fullscreen=50
+menu_wasd_navigation=no
+menu_hjkl_navigation=no
+menu_opacity=0.8
+menu_font_scale=1
+
+# top bar with window controls and media title shown only in no-border mode
+top_bar_size=40
+top_bar_size_fullscreen=46
+top_bar_controls=yes
+top_bar_title=yes
+
+# pause video on clicks shorter than this number of milliseconds, 0 to disable
+pause_on_click_shorter_than=0
+# flash duration in milliseconds used by `flash-{element}` commands
+flash_duration=400
+# distances in pixels below which elements are fully faded in/out
+proximity_in=40
+proximity_out=120
+# BBGGRR - BLUE GREEN RED hex color codes
+color_foreground=ffffff
+color_foreground_text=000000
+color_background=000000
+color_background_text=ffffff
+# use bold font weight throughout the whole UI
+font_bold=no
+# hide UI when mpv autohides the cursor
+autohide=no
+# can be: none, flash, static
+pause_indicator=flash
+# load first file when calling next on a last file in a directory and vice versa
+directory_navigation_loops=no
+# file types to look for when navigating media files
+media_types=3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv
+# file types to look for when loading external subtitles
+subtitle_types=aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt
+# used to approximate text width
+# if you are using some wide font and see a lot of right side clipping in menus,
+# try bumping this up
+font_height_to_letter_width_ratio=0.5
+
+# `chapter_ranges` lets you transform chapter indicators into range indicators.
+#
+# Chapter range definition syntax:
+# ```
+# start_pattern<color:opacity>end_pattern
+# ```
+#
+# Multiple start and end patterns can be defined by separating them with `|`:
+# ```
+# p1|pN<color:opacity>p1|pN
+# ```
+#
+# Multiple chapter ranges can be defined by separating them with comma:
+#
+# chapter_ranges=range1,rangeN
+#
+# One of `start_pattern`s can be a custom keyword `{bof}` that will match
+# beginning of file when it makes sense.
+#
+# One of `end_pattern`s can be a custom keyword `{eof}` that will match end of
+# file when it makes sense.
+#
+# Patterns are lua patterns (http://lua-users.org/wiki/PatternsTutorial).
+# They only need to occur in a title, not match it completely.
+# Matching is case insensitive.
+#
+# `color` is a `bbggrr` hexadecimal color code.
+# `opacity` is a float number from 0 to 1.
+#
+# Examples:
+#
+# Display anime openings and endings as ranges:
+# ```
+# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
+# ```
+#
+# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
+# ```
+# chapter_ranges=sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
+# ```
+chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
+```
+
+Available keybindings (place into `input.conf`):
+
+```
+Key script-binding uosc/peek-timeline
+Key script-binding uosc/toggle-progress
+Key script-binding uosc/flash-timeline
+Key script-binding uosc/flash-volume
+Key script-binding uosc/flash-speed
+Key script-binding uosc/menu
+Key script-binding uosc/load-subtitles
+Key script-binding uosc/subtitles
+Key script-binding uosc/audio
+Key script-binding uosc/video
+Key script-binding uosc/playlist
+Key script-binding uosc/chapters
+Key script-binding uosc/open-file
+Key script-binding uosc/next
+Key script-binding uosc/prev
+Key script-binding uosc/first
+Key script-binding uosc/last
+Key script-binding uosc/next-file
+Key script-binding uosc/prev-file
+Key script-binding uosc/first-file
+Key script-binding uosc/last-file
+Key script-binding uosc/delete-file-next
+Key script-binding uosc/delete-file-quit
+Key script-binding uosc/show-in-directory
+Key script-binding uosc/open-config-directory
+```
+]]
+
+if mp.get_property('osc') == 'yes' then
+ mp.msg.info('Disabled because original osc is enabled!')
+ return
+end
+
+local assdraw = require('mp.assdraw')
+local opt = require('mp.options')
+local utils = require('mp.utils')
+local msg = require('mp.msg')
+local osd = mp.create_osd_overlay('ass-events')
+local infinity = 1e309
+
+-- OPTIONS/CONFIG/STATE
+local options = {
+ timeline_size_min = 2,
+ timeline_size_max = 40,
+ timeline_size_min_fullscreen = 0,
+ timeline_size_max_fullscreen = 60,
+ timeline_start_hidden = false,
+ timeline_opacity = 0.8,
+ timeline_border = 1,
+ timeline_step = 5,
+ timeline_cached_ranges = '345433:0.5',
+ timeline_font_scale = 1,
+
+ chapters = 'dots',
+ chapters_opacity = 0.3,
+
+ volume = 'right',
+ volume_size = 40,
+ volume_size_fullscreen = 60,
+ volume_opacity = 0.8,
+ volume_border = 1,
+ volume_step = 1,
+ volume_font_scale = 1,
+
+ speed = false,
+ speed_size = 46,
+ speed_size_fullscreen = 68,
+ speed_opacity = 1,
+ speed_step = 0.1,
+ speed_font_scale = 1,
+
+ menu_item_height = 36,
+ menu_item_height_fullscreen = 50,
+ menu_wasd_navigation = false,
+ menu_hjkl_navigation = false,
+ menu_opacity = 0.8,
+ menu_font_scale = 1,
+
+ top_bar_size = 40,
+ top_bar_size_fullscreen = 46,
+ top_bar_controls = true,
+ top_bar_title = true,
+
+ pause_on_click_shorter_than = 0,
+ flash_duration = 400,
+ proximity_in = 40,
+ proximity_out = 120,
+ color_foreground = 'ffffff',
+ color_foreground_text = '000000',
+ color_background = '000000',
+ color_background_text = 'ffffff',
+ font_bold = false,
+ autohide = false,
+ pause_indicator = 'flash',
+ directory_navigation_loops = false,
+ media_types = '3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv',
+ subtitle_types = 'aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt',
+ font_height_to_letter_width_ratio = 0.5,
+ chapter_ranges = '^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end',
+}
+opt.read_options(options, 'uosc')
+local config = {
+ render_delay = 0.03, -- sets max rendering frequency
+ font = mp.get_property('options/osd-font'),
+ menu_parent_opacity = 0.4,
+ menu_min_width = 260
+}
+local bold_tag = options.font_bold and '\\b1' or ''
+local display = {
+ width = 1280,
+ height = 720,
+ aspect = 1.77778,
+}
+local cursor = {
+ hidden = true, -- true when autohidden or outside of the player window
+ x = 0,
+ y = 0,
+}
+local state = {
+ os = (function()
+ if os.getenv('windir') ~= nil then return 'windows' end
+ local homedir = os.getenv('HOME')
+ if homedir ~= nil and string.sub(homedir,1,6) == '/Users' then return 'macos' end
+ return 'linux'
+ end)(),
+ cwd = mp.get_property('working-directory'),
+ media_title = '',
+ duration = nil,
+ position = nil,
+ pause = false,
+ chapters = nil,
+ chapter_ranges = nil,
+ fullscreen = mp.get_property_native('fullscreen'),
+ maximized = mp.get_property_native('window-maximized'),
+ render_timer = nil,
+ render_last_time = 0,
+ volume = nil,
+ volume_max = nil,
+ mute = nil,
+ cursor_autohide_timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function()
+ if not options.autohide then return end
+ handle_mouse_leave()
+ end),
+ mouse_bindings_enabled = false,
+ cached_ranges = nil,
+}
+local forced_key_bindings -- defined at the bottom next to events
+
+-- HELPERS
+
+function round(number)
+ local modulus = number % 1
+ return modulus < 0.5 and math.floor(number) or math.ceil(number)
+end
+
+function call_me_maybe(fn, value1, value2, value3)
+ if fn then fn(value1, value2, value3) end
+end
+
+function split(str, pattern)
+ local list = {}
+ local full_pattern = '(.-)' .. pattern
+ local last_end = 1
+ local start_index, end_index, capture = str:find(full_pattern, 1)
+ while start_index do
+ list[#list +1] = capture
+ last_end = end_index + 1
+ start_index, end_index, capture = str:find(full_pattern, last_end)
+ end
+ if last_end <= (#str + 1) then
+ capture = str:sub(last_end)
+ list[#list +1] = capture
+ end
+ return list
+end
+
+function itable_find(haystack, needle)
+ local is_needle = type(needle) == 'function' and needle or function(index, value)
+ return value == needle
+ end
+ for index, value in ipairs(haystack) do
+ if is_needle(index, value) then return index, value end
+ end
+end
+
+function itable_filter(haystack, needle)
+ local is_needle = type(needle) == 'function' and needle or function(index, value)
+ return value == needle
+ end
+ local filtered = {}
+ for index, value in ipairs(haystack) do
+ if is_needle(index, value) then filtered[#filtered + 1] = value end
+ end
+ return filtered
+end
+
+function itable_remove(haystack, needle)
+ local should_remove = type(needle) == 'function' and needle or function(value)
+ return value == needle
+ end
+ local new_table = {}
+ for _, value in ipairs(haystack) do
+ if not should_remove(value) then
+ new_table[#new_table + 1] = value
+ end
+ end
+ return new_table
+end
+
+function itable_slice(haystack, start_pos, end_pos)
+ start_pos = start_pos and start_pos or 1
+ end_pos = end_pos and end_pos or #haystack
+
+ if end_pos < 0 then end_pos = #haystack + end_pos + 1 end
+ if start_pos < 0 then start_pos = #haystack + start_pos + 1 end
+
+ local new_table = {}
+ for index, value in ipairs(haystack) do
+ if index >= start_pos and index <= end_pos then
+ new_table[#new_table + 1] = value
+ end
+ end
+ return new_table
+end
+
+function table_copy(table)
+ local new_table = {}
+ for key, value in pairs(table) do new_table[key] = value end
+ return new_table
+end
+
+-- Sorting comparator close to (but not exactly) how file explorers sort files
+local word_order_comparator = (function()
+ local symbol_order
+ local default_order
+
+ if state.os == 'win' then
+ symbol_order = {
+ ['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7,
+ ['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14,
+ ['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20,
+ }
+ default_order = 21
+ else
+ symbol_order = {
+ ['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8,
+ ['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14,
+ ['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23,
+ }
+ default_order = 21
+ end
+
+ return function (a, b)
+ a = a:lower()
+ b = b:lower()
+ for i = 1, math.max(#a, #b) do
+ local ai = a:sub(i, i)
+ local bi = b:sub(i, i)
+ if ai == nil and bi then return true end
+ if bi == nil and ai then return false end
+ local a_order = symbol_order[ai] or default_order
+ local b_order = symbol_order[bi] or default_order
+ if a_order == b_order then
+ return a < b
+ else
+ return a_order < b_order
+ end
+ end
+ end
+end)()
+
+-- Creates in-between frames to animate value from `from` to `to` numbers.
+-- Returns function that terminates animation.
+-- `to` can be a function that returns target value, useful for movable targets.
+-- `speed` is an optional float between 1-instant and 0-infinite duration
+-- `callback` is called either on animation end, or when animation is canceled
+function tween(from, to, setter, speed, callback)
+ if type(speed) ~= 'number' then
+ callback = speed
+ speed = 0.3
+ end
+ local timeout
+ local getTo = type(to) == 'function' and to or function() return to end
+ local cutoff = math.abs(getTo() - from) * 0.01
+ function tick()
+ from = from + ((getTo() - from) * speed)
+ local is_end = math.abs(getTo() - from) <= cutoff
+ setter(is_end and getTo() or from)
+ request_render()
+ if is_end then
+ call_me_maybe(callback)
+ else
+ timeout:resume()
+ end
+ end
+ timeout = mp.add_timeout(0.016, tick)
+ tick()
+ return function()
+ timeout:kill()
+ call_me_maybe(callback)
+ end
+end
+
+-- Kills ongoing animation if one is already running on this element.
+-- Killed animation will not get its `on_end` called.
+function tween_element(element, from, to, setter, speed, callback)
+ if type(speed) ~= 'number' then
+ callback = speed
+ speed = 0.3
+ end
+
+ tween_element_stop(element)
+
+ element.stop_current_animation = tween(
+ from, to,
+ function(value) setter(element, value) end,
+ speed,
+ function()
+ element.stop_current_animation = nil
+ call_me_maybe(callback, element)
+ end
+ )
+end
+
+-- Stopped animation will not get its on_end called.
+function tween_element_is_tweening(element)
+ return element and element.stop_current_animation
+end
+
+-- Stopped animation will not get its on_end called.
+function tween_element_stop(element)
+ call_me_maybe(element and element.stop_current_animation)
+end
+
+-- Helper to automatically use an element property setter
+function tween_element_property(element, prop, from, to, speed, callback)
+ tween_element(element, from, to, function(_, value) element[prop] = value end, speed, callback)
+end
+
+function get_point_to_rectangle_proximity(point, rect)
+ local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx + 1)
+ local dy = math.max(rect.ay - point.y, 0, point.y - rect.by + 1)
+ return math.sqrt(dx*dx + dy*dy);
+end
+
+function text_width_estimate(letters, font_size)
+ return letters and letters * font_size * options.font_height_to_letter_width_ratio or 0
+end
+
+function opacity_to_alpha(opacity)
+ return 255 - math.ceil(255 * opacity)
+end
+
+function ass_opacity(opacity, fraction)
+ fraction = fraction ~= nil and fraction or 1
+ if type(opacity) == 'number' then
+ return string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction))
+ else
+ return string.format(
+ '{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
+ opacity_to_alpha((opacity[1] or 0) * fraction),
+ opacity_to_alpha((opacity[2] or 0) * fraction),
+ opacity_to_alpha((opacity[3] or 0) * fraction),
+ opacity_to_alpha((opacity[4] or 0) * fraction)
+ )
+ end
+end
+
+-- Ensures path is absolute and normalizes slashes to the current platform
+function normalize_path(path)
+ if not path or is_protocol(path) then return path end
+
+ -- Ensure path is absolute
+ if not (path:match('^/') or path:match('^%a+:') or path:match('^\\\\')) then
+ path = utils.join_path(state.cwd, path)
+ end
+
+ -- Use proper slashes
+ if state.os == 'windows' then
+ return path:gsub('/', '\\')
+ else
+ return path:gsub('\\', '/')
+ end
+end
+
+-- Check if path is a protocol, such as `http://...`
+function is_protocol(path)
+ return path:match('^%a[%a%d-_]+://')
+end
+
+function get_extension(path)
+ local parts = split(path, '%.')
+ return parts and #parts > 1 and parts[#parts] or nil
+end
+
+-- Serializes path into its semantic parts
+function serialize_path(path)
+ if not path or is_protocol(path) then return end
+ path = normalize_path(path)
+ local parts = split(path, '[\\/]+')
+ if parts[#parts] == '' then table.remove(parts, #parts) end -- remove trailing separator
+ local basename = parts and parts[#parts] or path
+ local dirname = #parts > 1 and table.concat(itable_slice(parts, 1, #parts - 1), state.os == 'windows' and '\\' or '/') or nil
+ local dot_split = split(basename, '%.')
+ return {
+ path = path:sub(-1) == ':' and state.os == 'windows' and path..'\\' or path,
+ is_root = dirname == nil,
+ dirname = dirname,
+ basename = basename,
+ filename = #dot_split > 1 and table.concat(itable_slice(dot_split, 1, #dot_split - 1), '.') or basename,
+ extension = #dot_split > 1 and dot_split[#dot_split] or nil,
+ }
+end
+
+function get_files_in_directory(directory, allowed_types)
+ local files, error = utils.readdir(directory, 'files')
+
+ if not files then
+ msg.error('Retrieving files failed: '..(error or ''))
+ return
+ end
+
+ -- Filter only requested file types
+ if allowed_types then
+ files = itable_filter(files, function(_, file)
+ local extension = get_extension(file)
+ return extension and itable_find(allowed_types, extension:lower())
+ end)
+ end
+
+ table.sort(files, word_order_comparator)
+
+ return files
+end
+
+function get_adjacent_file(file_path, direction, allowed_types)
+ local current_file = serialize_path(file_path)
+ local files = get_files_in_directory(current_file.dirname, allowed_types)
+
+ if not files then return end
+
+ for index, file in ipairs(files) do
+ if current_file.basename == file then
+ if direction == 'forward' then
+ if files[index + 1] then return utils.join_path(current_file.dirname, files[index + 1]) end
+ if options.directory_navigation_loops and files[1] then return utils.join_path(current_file.dirname, files[1]) end
+ else
+ if files[index - 1] then return utils.join_path(current_file.dirname, files[index - 1]) end
+ if options.directory_navigation_loops and files[#files] then return utils.join_path(current_file.dirname, files[#files]) end
+ end
+
+ -- This is the only file in directory
+ return nil
+ end
+ end
+end
+
+-- Ensures chapters are in chronological order
+function get_normalized_chapters()
+ local chapters = mp.get_property_native('chapter-list')
+
+ if not chapters then return end
+
+ -- Copy table
+ chapters = itable_slice(chapters)
+
+ -- Ensure chronological order of chapters
+ table.sort(chapters, function(a, b) return a.time < b.time end)
+
+ return chapters
+end
+
+-- Element
+--[[
+Signature:
+{
+ -- enables capturing button groups for this element
+ captures = {mouse_buttons = true, wheel = true},
+ -- element rectangle coordinates
+ ax = 0, ay = 0, bx = 0, by = 0,
+ -- cursor<>element relative proximity as a 0-1 floating number
+ -- where 0 = completely away, and 1 = touching/hovering
+ -- so it's easy to work with and throw into equations
+ proximity = 0,
+ -- raw cursor<>element proximity in pixels
+ proximity_raw = infinity,
+ -- called when element is created
+ ?init = function(this),
+ -- called manually when disposing of element
+ ?destroy = function(this),
+ -- triggered when event happens and cursor is above element
+ ?on_{event_name} = function(this),
+ -- triggered when any event happens anywhere on a page
+ ?on_global_{event_name} = function(this),
+ -- object
+ ?render = function(this_element),
+}
+]]
+local Element = {
+ captures = nil,
+ ax = 0, ay = 0, bx = 0, by = 0,
+ proximity = 0, proximity_raw = infinity,
+}
+Element.__index = Element
+
+function Element.new(props)
+ local element = setmetatable(props, Element)
+ element._eventListeners = {}
+
+ -- Flash timer
+ element._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
+ local getTo = function() return element.proximity end
+ element:tween_property('forced_proximity', 1, getTo, function()
+ element.forced_proximity = nil
+ end)
+ end)
+ element._flash_out_timer:kill()
+
+ element:init()
+
+ return element
+end
+
+function Element:init() end
+function Element:destroy() end
+
+-- Call method if it exists
+function Element:maybe(name, ...)
+ if self[name] then return self[name](self, ...) end
+end
+
+-- Tween helpers
+function Element:tween(...) tween_element(self, ...) end
+function Element:tween_property(...) tween_element_property(self, ...) end
+function Element:tween_stop() tween_element_stop(self) end
+function Element:is_tweening() tween_element_is_tweening(self) end
+
+-- Event listeners
+function Element:on(name, handler)
+ if self._eventListeners[name] == nil then self._eventListeners[name] = {} end
+ local preexistingIndex = itable_find(self._eventListeners[name], handler)
+ if preexistingIndex then
+ return
+ else
+ self._eventListeners[name][#self._eventListeners[name] + 1] = handler
+ end
+end
+function Element:off(name, handler)
+ if self._eventListeners[name] == nil then return end
+ local index = itable_find(self._eventListeners, handler)
+ if index then table.remove(self._eventListeners, index) end
+end
+function Element:trigger(name, ...)
+ self:maybe('on_'..name, ...)
+ if self._eventListeners[name] == nil then return end
+ for _, handler in ipairs(self._eventListeners[name]) do handler(...) end
+end
+
+-- Briefly flashes the element for `options.flash_duration` milliseconds.
+-- Useful to visualize changes of volume and timeline when changed via hotkeys.
+-- Implemented by briefly adding animated `forced_proximity` property to the element.
+function Element:flash()
+ if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
+ self:tween_stop()
+ self.forced_proximity = 1
+ self._flash_out_timer:kill()
+ self._flash_out_timer:resume()
+ end
+end
+
+-- ELEMENTS
+
+local Elements = {itable = {}}
+Elements.__index = Elements
+local elements = setmetatable({}, Elements)
+
+function Elements:add(name, element)
+ local insert_index = #Elements.itable + 1
+
+ -- Replace if element already exists
+ if self:has(name) then
+ insert_index = itable_find(Elements.itable, function(_, element)
+ return element.name == name
+ end)
+ end
+
+ element.name = name
+ Elements.itable[insert_index] = element
+ self[name] = element
+
+ request_render()
+end
+
+function Elements:remove(name, props)
+ Elements.itable = itable_remove(Elements.itable, self[name])
+ self[name] = nil
+ request_render()
+end
+
+function Elements:has(name) return self[name] ~= nil end
+function Elements:ipairs() return ipairs(Elements.itable) end
+function Elements:pairs(elements) return pairs(self) end
+
+-- MENU
+--[[
+Usage:
+```
+local items = {
+ {title = 'Foo title', hint = 'Ctrl+F', value = 'foo'},
+ {title = 'Bar title', hint = 'Ctrl+B', value = 'bar'},
+ {
+ title = 'Submenu',
+ items = {
+ {title = 'Sub item 1', value = 'sub1'},
+ {title = 'Sub item 2', value = 'sub2'}
+ }
+ }
+}
+
+function open_item(value)
+ value -- value from `item.value`
+end
+
+menu:open(items, open_item)
+```
+]]
+local Menu = {}
+Menu.__index = Menu
+local menu = setmetatable({key_bindings = {}, is_closing = false}, Menu)
+
+function Menu:is_open(menu_type)
+ return elements.menu ~= nil and (not menu_type or elements.menu.type == menu_type)
+end
+
+function Menu:open(items, open_item, opts)
+ opts = opts or {}
+
+ if menu:is_open() then
+ if not opts.parent_menu then
+ menu:close(true, function()
+ menu:open(items, open_item, opts)
+ end)
+ return
+ end
+ else
+ menu:enable_key_bindings()
+ elements.curtain:fadein()
+ end
+
+ elements:add('menu', Element.new({
+ captures = {mouse_buttons = true},
+ type = nil, -- menu type such as `menu`, `chapters`, ...
+ title = nil,
+ width = nil,
+ height = nil,
+ offset_x = 0, -- used to animated from/to left when submenu
+ item_height = nil,
+ item_spacing = 1,
+ item_content_spacing = nil,
+ font_size = nil,
+ scroll_step = nil,
+ scroll_height = nil,
+ scroll_y = 0,
+ opacity = 0,
+ relative_parent_opacity = 0.4,
+ items = items,
+ active_item = nil,
+ selected_item = nil,
+ open_item = open_item,
+ parent_menu = nil,
+ init = function(this)
+ -- Already initialized
+ if this.width ~= nil then return end
+
+ -- Apply options
+ for key, value in pairs(opts) do this[key] = value end
+ this.selected_item = this.active_item
+
+ -- Set initial dimensions
+ this:on_display_resize()
+
+ -- Scroll to active item
+ this:scroll_to_item(this.active_item)
+
+ -- Transition in animation
+ menu.transition = {to = 'child', target = this}
+ local start_offset = this.parent_menu and (this.parent_menu.width + this.width) / 2 or 0
+
+ tween_element(menu.transition.target, 0, 1, function(_, pos)
+ this:set_offset_x(round(start_offset * (1 - pos)))
+ this.opacity = pos
+ this:set_parent_opacity(1 - ((1 - config.menu_parent_opacity) * pos))
+ end, function()
+ menu.transition = nil
+ update_proximities()
+ end)
+ end,
+ destroy = function(this)
+ request_render()
+ end,
+ on_display_resize = function(this)
+ this.item_height = (state.fullscreen or state.maximized) and options.menu_item_height_fullscreen or options.menu_item_height
+ this.font_size = round(this.item_height * 0.48 * options.menu_font_scale)
+ this.item_content_spacing = round((this.item_height - this.font_size) * 0.6)
+ this.scroll_step = this.item_height + this.item_spacing
+
+ -- Estimate width of a widest item
+ local estimated_max_width = 0
+ for _, item in ipairs(items) do
+ local item_text_length = ((item.title and item.title:len() or 0) + (item.hint and item.hint:len() or 0))
+ local spacings_in_item = item.hint and 3 or 2
+ local estimated_width = text_width_estimate(item_text_length, this.font_size) + (this.item_content_spacing * spacings_in_item)
+ if estimated_width > estimated_max_width then
+ estimated_max_width = estimated_width
+ end
+ end
+
+ -- Also check menu title
+ local menu_title_length = this.title and this.title:len() or 0
+ local estimated_menu_title_width = text_width_estimate(menu_title_length, this.font_size)
+ if estimated_menu_title_width > estimated_max_width then
+ estimated_max_width = estimated_menu_title_width
+ end
+
+ -- Coordinates and sizes are of the scrollable area to make
+ -- consuming values in rendering easier. Title drawn above this, so
+ -- we need to account for that in max_height and ay position.
+ this.width = round(math.min(math.max(estimated_max_width, config.menu_min_width), display.width * 0.9))
+ local title_height = this.title and this.scroll_step or 0
+ local max_height = round(display.height * 0.9) - title_height
+ this.height = math.min(round(this.scroll_step * #items) - this.item_spacing, max_height)
+ this.scroll_height = math.max((this.scroll_step * #this.items) - this.height - this.item_spacing, 0)
+ this.ax = round((display.width - this.width) / 2) + this.offset_x
+ this.ay = round((display.height - this.height) / 2 + (title_height / 2))
+ this.bx = round(this.ax + this.width)
+ this.by = round(this.ay + this.height)
+
+ if this.parent_menu then
+ this.parent_menu:on_display_resize()
+ end
+ end,
+ set_items = function(this, items, props)
+ this.items = items
+ this.selected_item = nil
+ this.active_item = nil
+ if props then
+ for key, value in pairs(props) do this[key] = value end
+ end
+ this:on_display_resize()
+ request_render()
+ end,
+ set_offset_x = function(this, offset)
+ local delta = offset - this.offset_x
+ this.offset_x = offset
+ this.ax = this.ax + delta
+ this.bx = this.bx + delta
+ if this.parent_menu then
+ this.parent_menu:set_offset_x(offset - ((this.width + this.parent_menu.width) / 2) - this.item_spacing)
+ else
+ update_proximities()
+ end
+ end,
+ fadeout = function(this, callback)
+ this:tween(1, 0, function(this, pos)
+ this.opacity = pos
+ this:set_parent_opacity(pos * config.menu_parent_opacity)
+ end, callback)
+ end,
+ set_parent_opacity = function(this, opacity)
+ if this.parent_menu then
+ this.parent_menu.opacity = opacity
+ this.parent_menu:set_parent_opacity(opacity * config.menu_parent_opacity)
+ end
+ end,
+ get_item_index_below_cursor = function(this)
+ return math.ceil((cursor.y - this.ay + this.scroll_y) / this.scroll_step)
+ end,
+ get_first_visible_index = function(this)
+ return round(this.scroll_y / this.scroll_step) + 1
+ end,
+ get_last_visible_index = function(this)
+ return round((this.scroll_y + this.height) / this.scroll_step)
+ end,
+ get_centermost_visible_index = function(this)
+ return round((this.scroll_y + (this.height / 2)) / this.scroll_step)
+ end,
+ scroll_to = function(this, pos)
+ this.scroll_y = math.max(math.min(pos, this.scroll_height), 0)
+ request_render()
+ end,
+ scroll_to_item = function(this, index)
+ if (index and index >= 1 and index <= #this.items) then
+ this:scroll_to(round((this.scroll_step * (index - 1)) - ((this.height - this.scroll_step) / 2)))
+ end
+ end,
+ select_index = function(this, index)
+ this.selected_item = (index and index >= 1 and index <= #this.items) and index or nil
+ request_render()
+ end,
+ select_value = function(this, value)
+ this:select_index(itable_find(this.items, function(_, item) return item.value == value end))
+ end,
+ activate_index = function(this, index)
+ this.active_item = (index and index >= 1 and index <= #this.items) and index or nil
+ request_render()
+ end,
+ activate_value = function(this, value)
+ this:activate_index(itable_find(this.items, function(_, item) return item.value == value end))
+ end,
+ delete_index = function(this, index)
+ if (index and index >= 1 and index <= #this.items) then
+ local previous_active_value = this.active_index and this.items[this.active_index].value or nil
+ table.remove(this.items, index)
+ this:on_display_resize()
+ if previous_active_value then this:activate_value(previous_active_value) end
+ this:scroll_to_item(this.selected_item)
+ end
+ end,
+ delete_value = function(this, value)
+ this:delete_index(itable_find(this.items, function(_, item) return item.value == value end))
+ end,
+ prev = function(this)
+ local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_last_visible_index()
+ local current_index = this.selected_item or default_anchor + 1
+ this.selected_item = math.max(current_index - 1, 1)
+ this:scroll_to_item(this.selected_item)
+ end,
+ next = function(this)
+ local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_first_visible_index()
+ local current_index = this.selected_item or default_anchor - 1
+ this.selected_item = math.min(current_index + 1, #this.items)
+ this:scroll_to_item(this.selected_item)
+ end,
+ back = function(this)
+ if menu.transition then
+ local transition_target = menu.transition.target
+ local transition_target_type = menu.transition.target
+ tween_element_stop(transition_target)
+ if transition_target_type == 'parent' then
+ elements:add('menu', transition_target)
+ end
+ menu.transition = nil
+ transition_target:back()
+ return
+ else
+ menu.transition = {to = 'parent', target = this.parent_menu}
+ end
+
+ if menu.transition.target == nil then
+ menu:close()
+ return
+ end
+
+ local target = menu.transition.target
+ local to_offset = -target.offset_x + this.offset_x
+
+ tween_element(target, 0, 1, function(_, pos)
+ this:set_offset_x(round(to_offset * pos))
+ this.opacity = 1 - pos
+ this:set_parent_opacity(config.menu_parent_opacity + ((1 - config.menu_parent_opacity) * pos))
+ end, function()
+ menu.transition = nil
+ elements:add('menu', target)
+ update_proximities()
+ end)
+ end,
+ open_selected_item = function(this)
+ -- If there is a transition active and this method got called, it
+ -- means we are animating from this menu to parent menu, and all
+ -- calls to this method should be relayed to the parent menu.
+ if menu.transition and menu.transition.to == 'parent' then
+ local target = menu.transition.target
+ tween_element_stop(target)
+ menu.transition = nil
+ target:open_selected_item()
+ return
+ end
+
+ if this.selected_item then
+ local item = this.items[this.selected_item]
+ -- Is submenu
+ if item.items then
+ local opts = table_copy(opts)
+ opts.parent_menu = this
+ menu:open(item.items, this.open_item, opts)
+ else
+ menu:close(true)
+ this.open_item(item.value)
+ end
+ end
+ end,
+ close = function(this)
+ menu:close()
+ end,
+ on_global_mbtn_left_down = function(this)
+ if this.proximity_raw == 0 then
+ this.selected_item = this:get_item_index_below_cursor()
+ this:open_selected_item()
+ else
+ -- check if this is clicking on any parent menus
+ local parent_menu = this.parent_menu
+ repeat
+ if parent_menu then
+ if get_point_to_rectangle_proximity(cursor, parent_menu) == 0 then
+ this:back()
+ return
+ end
+ parent_menu = parent_menu.parent_menu
+ end
+ until parent_menu == nil
+
+ menu:close()
+ end
+ end,
+ on_global_mouse_move = function(this)
+ if this.proximity_raw == 0 then
+ this.selected_item = this:get_item_index_below_cursor()
+ else
+ if this.selected_item then this.selected_item = nil end
+ end
+ request_render()
+ end,
+ on_wheel_up = function(this)
+ this.selected_item = nil
+ this:scroll_to(this.scroll_y - this.scroll_step)
+ -- Selects item below cursor
+ this:on_global_mouse_move()
+ request_render()
+ end,
+ on_wheel_down = function(this)
+ this.selected_item = nil
+ this:scroll_to(this.scroll_y + this.scroll_step)
+ -- Selects item below cursor
+ this:on_global_mouse_move()
+ request_render()
+ end,
+ on_pgup = function(this)
+ this.selected_item = nil
+ this:scroll_to(this.scroll_y - this.height)
+ end,
+ on_pgdwn = function(this)
+ this.selected_item = nil
+ this:scroll_to(this.scroll_y + this.height)
+ end,
+ on_home = function(this)
+ this.selected_item = nil
+ this:scroll_to(0)
+ end,
+ on_end = function(this)
+ this.selected_item = nil
+ this:scroll_to(this.scroll_height)
+ end,
+ render = render_menu,
+ }))
+
+ elements.menu:maybe('on_open')
+end
+
+function Menu:add_key_binding(key, name, fn, flags)
+ menu.key_bindings[#menu.key_bindings + 1] = name
+ mp.add_forced_key_binding(key, name, fn, flags)
+end
+
+function Menu:enable_key_bindings()
+ menu.key_bindings = {}
+ -- The `mp.set_key_bindings()` method would be easier here, but that
+ -- doesn't support 'repeatable' flag, so we are stuck with this monster.
+ menu:add_key_binding('up', 'menu-prev', self:create_action('prev'), 'repeatable')
+ menu:add_key_binding('down', 'menu-next', self:create_action('next'), 'repeatable')
+ menu:add_key_binding('left', 'menu-back', self:create_action('back'))
+ menu:add_key_binding('right', 'menu-select', self:create_action('open_selected_item'))
+
+ if options.menu_wasd_navigation then
+ menu:add_key_binding('w', 'menu-prev-alt', self:create_action('prev'), 'repeatable')
+ menu:add_key_binding('a', 'menu-back-alt', self:create_action('back'))
+ menu:add_key_binding('s', 'menu-next-alt', self:create_action('next'), 'repeatable')
+ menu:add_key_binding('d', 'menu-select-alt', self:create_action('open_selected_item'))
+ end
+
+ if options.menu_hjkl_navigation then
+ menu:add_key_binding('h', 'menu-back-alt2', self:create_action('back'))
+ menu:add_key_binding('j', 'menu-next-alt2', self:create_action('next'), 'repeatable')
+ menu:add_key_binding('k', 'menu-prev-alt2', self:create_action('prev'), 'repeatable')
+ menu:add_key_binding('l', 'menu-select-alt2', self:create_action('open_selected_item'))
+ end
+
+ menu:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_action('back'))
+ menu:add_key_binding('bs', 'menu-back-alt4', self:create_action('back'))
+ menu:add_key_binding('enter', 'menu-select-alt3', self:create_action('open_selected_item'))
+ menu:add_key_binding('kp_enter', 'menu-select-alt4', self:create_action('open_selected_item'))
+ menu:add_key_binding('esc', 'menu-close', self:create_action('close'))
+ menu:add_key_binding('pgup', 'menu-page-up', self:create_action('on_pgup'))
+ menu:add_key_binding('pgdwn', 'menu-page-down', self:create_action('on_pgdwn'))
+ menu:add_key_binding('home', 'menu-home', self:create_action('on_home'))
+ menu:add_key_binding('end', 'menu-end', self:create_action('on_end'))
+end
+
+function Menu:disable_key_bindings()
+ for _, name in ipairs(menu.key_bindings) do mp.remove_key_binding(name) end
+ menu.key_bindings = {}
+end
+
+function Menu:create_action(name)
+ return function(...)
+ if elements.menu then elements.menu:maybe(name, ...) end
+ end
+end
+
+function Menu:close(immediate, callback)
+ if type(immediate) ~= 'boolean' then callback = immediate end
+
+ if elements:has('menu') and not menu.is_closing then
+ function close()
+ elements.menu:maybe('on_close')
+ elements.menu:destroy()
+ elements:remove('menu')
+ menu.is_closing = false
+ update_proximities()
+ menu:disable_key_bindings()
+ call_me_maybe(callback)
+ end
+
+ menu.is_closing = true
+ elements.curtain:fadeout()
+
+ if immediate then
+ close()
+ else
+ elements.menu:fadeout(close)
+ end
+ end
+end
+
+-- ICONS
+--[[
+ASS \shadN shadows are drawn also below the element, which when there is an
+opacity in play, blends icon colors into ugly greys. The mess below is an
+attempt to fix it by rendering shadows for icons with clipping.
+
+Add icons by adding functions to render them to `icons` table.
+
+Signature: function(pos_x, pos_y, size) => string
+
+Function has to return ass path coordinates to draw the icon centered at pox_x
+and pos_y of passed size.
+]]
+local icons = {}
+function icon(name, icon_x, icon_y, icon_size, shad_x, shad_y, shad_size, backdrop, opacity, clip)
+ local ass = assdraw.ass_new()
+ local icon_path = icons[name](icon_x, icon_y, icon_size)
+ local icon_color = options['color_'..backdrop..'_text']
+ local shad_color = options['color_'..backdrop]
+ local use_border = (shad_x + shad_y) == 0
+ local icon_border = use_border and shad_size or 0
+
+ -- clip can't clip out shadows, a very annoying limitation I can't work
+ -- around without going back to ugly default ass shadows, but atm I actually
+ -- don't need clipping of icons with shadows, so I'm choosing to ignore this
+ if not clip then
+ clip = ''
+ end
+
+ if not use_border then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad0\\1c&H'..shad_color..'\\iclip('..ass.scale..', '..icon_path..')}')
+ ass:append(ass_opacity(opacity))
+ ass:pos(shad_x + shad_size, shad_y + shad_size)
+ ass:draw_start()
+ ass:append(icon_path)
+ ass:draw_stop()
+ end
+
+ ass:new_event()
+ ass:append('{\\blur0\\bord'..icon_border..'\\shad0\\1c&H'..icon_color..'\\3c&H'..shad_color..clip..'}')
+ ass:append(ass_opacity(opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:append(icon_path)
+ ass:draw_stop()
+
+ return ass.text
+end
+
+function icons._volume(muted, pos_x, pos_y, size)
+ local ass = assdraw.ass_new()
+ local scale = size / 200
+ function x(number) return pos_x + (number * scale) end
+ function y(number) return pos_y + (number * scale) end
+ ass:move_to(x(-85), y(-35))
+ ass:line_to(x(-50), y(-35))
+ ass:line_to(x(-5), y(-75))
+ ass:line_to(x(-5), y(75))
+ ass:line_to(x(-50), y(35))
+ ass:line_to(x(-85), y(35))
+ if muted then
+ ass:move_to(x(76), y(-35)) ass:line_to(x(50), y(-9)) ass:line_to(x(24), y(-35))
+ ass:line_to(x(15), y(-26)) ass:line_to(x(41), y(0)) ass:line_to(x(15), y(26))
+ ass:line_to(x(24), y(35)) ass:line_to(x(50), y(9)) ass:line_to(x(76), y(35))
+ ass:line_to(x(85), y(26)) ass:line_to(x(59), y(0)) ass:line_to(x(85), y(-26))
+ else
+ ass:move_to(x(20), y(-30)) ass:line_to(x(20), y(30))
+ ass:line_to(x(35), y(30)) ass:line_to(x(35), y(-30))
+
+ ass:move_to(x(55), y(-60)) ass:line_to(x(55), y(60))
+ ass:line_to(x(70), y(60)) ass:line_to(x(70), y(-60))
+ end
+ return ass.text
+end
+function icons.volume(pos_x, pos_y, size) return icons._volume(false, pos_x, pos_y, size) end
+function icons.volume_muted(pos_x, pos_y, size) return icons._volume(true, pos_x, pos_y, size) end
+
+function icons.arrow_right(pos_x, pos_y, size)
+ local ass = assdraw.ass_new()
+ local scale = size / 200
+ function x(number) return pos_x + (number * scale) end
+ function y(number) return pos_y + (number * scale) end
+ ass:move_to(x(-22), y(-80))
+ ass:line_to(x(-45), y(-57))
+ ass:line_to(x(12), y(0))
+ ass:line_to(x(-45), y(57))
+ ass:line_to(x(-22), y(80))
+ ass:line_to(x(58), y(0))
+ return ass.text
+end
+
+-- STATE UPDATES
+
+function update_display_dimensions()
+ local o = mp.get_property_native('osd-dimensions')
+ display.width = o.w
+ display.height = o.h
+ display.aspect = o.aspect
+
+ -- Tell elements about this
+ for _, element in elements:ipairs() do
+ if element.on_display_resize ~= nil then
+ element.on_display_resize(element)
+ end
+ end
+end
+
+function update_element_cursor_proximity(element)
+ if cursor.hidden then
+ element.proximity_raw = infinity
+ element.proximity = 0
+ else
+ local range = options.proximity_out - options.proximity_in
+ element.proximity_raw = get_point_to_rectangle_proximity(cursor, element)
+ element.proximity = menu:is_open() and 0 or 1 - (math.min(math.max(element.proximity_raw - options.proximity_in, 0), range) / range)
+ end
+end
+
+function update_proximities()
+ local capture_mouse_buttons = false
+ local capture_wheel = false
+ local menu_only = menu:is_open()
+ local mouse_left_elements = {}
+ local mouse_entered_elements = {}
+
+ -- Calculates proximities and opacities for defined elements
+ for _, element in elements:ipairs() do
+ local previous_proximity_raw = element.proximity_raw
+
+ -- If menu is open, all other elements have to be disabled
+ if menu_only then
+ if element.name == 'menu' then
+ capture_mouse_buttons = true
+ capture_wheel = true
+ update_element_cursor_proximity(element)
+ else
+ element.proximity_raw = infinity
+ element.proximity = 0
+ end
+ else
+ update_element_cursor_proximity(element)
+ end
+
+ if element.proximity_raw == 0 then
+ -- Mouse is over element
+ if element.captures and element.captures.mouse_buttons then capture_mouse_buttons = true end
+ if element.captures and element.captures.wheel then capture_wheel = true end
+
+ -- Mouse entered element area
+ if previous_proximity_raw ~= 0 then
+ mouse_entered_elements[#mouse_entered_elements + 1] = element
+ end
+ else
+ -- Mouse left element area
+ if previous_proximity_raw == 0 then
+ mouse_left_elements[#mouse_left_elements + 1] = element
+ end
+ end
+ end
+
+ -- Enable key group captures elements request.
+ if capture_mouse_buttons then
+ forced_key_bindings.mouse_buttons:enable()
+ else
+ forced_key_bindings.mouse_buttons:disable()
+ end
+ if capture_wheel then
+ forced_key_bindings.wheel:enable()
+ else
+ forced_key_bindings.wheel:disable()
+ end
+
+ -- Trigger `mouse_leave` and `mouse_enter` events
+ for _, element in ipairs(mouse_left_elements) do element:trigger('mouse_leave') end
+ for _, element in ipairs(mouse_entered_elements) do element:trigger('mouse_enter') end
+end
+
+-- ELEMENT RENDERERS
+
+function render_timeline(this)
+ if this.size_max == 0 or state.duration == nil or state.duration == 0 or state.position == nil then return end
+
+ local size_min = this:get_effective_size_min()
+ local size = this:get_effective_size()
+
+ if size < 1 then return end
+
+ local ass = assdraw.ass_new()
+
+ -- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
+ local hide_text_below = math.max(this.font_size * 0.7, size_min * 2)
+ local hide_text_ramp = hide_text_below / 2
+ local text_opacity = math.max(math.min(size - hide_text_below, hide_text_ramp), 0) / hide_text_ramp
+
+ local spacing = math.max(math.floor((this.size_max - this.font_size) / 2.5), 4)
+ local progress = state.position / state.duration
+
+ -- Background bar coordinates
+ local bax = 0
+ local bay = display.height - size - this.bottom_border - this.top_border
+ local bbx = display.width
+ local bby = display.height
+
+ -- Foreground bar coordinates
+ local fax = bax
+ local fay = bay + this.top_border
+ local fbx = bbx * progress
+ local fby = bby - this.bottom_border
+ local foreground_size = bby - bay
+ local foreground_coordinates = fax..','..fay..','..fbx..','..fby -- for clipping
+
+ -- Background
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..foreground_coordinates..')}')
+ ass:append(ass_opacity(math.max(options.timeline_opacity - 0.1, 0)))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(bax, bay, bbx, bby)
+ ass:draw_stop()
+
+ -- Foreground
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
+ ass:append(ass_opacity(options.timeline_opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(fax, fay, fbx, fby)
+ ass:draw_stop()
+
+ -- Seekable ranges
+ if options.timeline_cached_ranges and state.cached_ranges then
+ local range_height = math.max(foreground_size / 8, size_min)
+ local range_ay = fby - range_height
+ for _, range in ipairs(state.cached_ranges) do
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.timeline_cached_ranges.color..'}')
+ ass:append(ass_opacity(options.timeline_cached_ranges.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(
+ bbx * (range['start'] / state.duration), range_ay,
+ bbx * (range['end'] / state.duration), range_ay + range_height
+ )
+ ass:draw_stop()
+ end
+ end
+
+ -- Custom ranges
+ if state.chapter_ranges ~= nil then
+ for i, chapter_range in ipairs(state.chapter_ranges) do
+ for i, range in ipairs(chapter_range.ranges) do
+ local rax = display.width * (range['start'].time / state.duration)
+ local rbx = display.width * (range['end'].time / state.duration)
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..chapter_range.color..'}')
+ ass:append(ass_opacity(chapter_range.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ -- for 1px chapter size, use the whole size of the bar including padding
+ if size <= 1 then
+ ass:rect_cw(rax, bay, rbx, bby)
+ else
+ ass:rect_cw(rax, fay, rbx, fby)
+ end
+ ass:draw_stop()
+ end
+ end
+ end
+
+ -- Chapters
+ if options.chapters ~= 'none' and state.chapters ~= nil and #state.chapters > 0 then
+ local half_size = size / 2
+ local dots = false
+ local chapter_size, chapter_y
+ if options.chapters == 'dots' then
+ dots = true
+ chapter_size = math.min(6, (foreground_size / 2) + 2)
+ chapter_y = math.min(fay + chapter_size, fay + half_size)
+ elseif options.chapters == 'lines' then
+ chapter_size = size
+ chapter_y = fay + (chapter_size / 2)
+ elseif options.chapters == 'lines-top' then
+ chapter_size = math.min(this.size_max / 3.5, size)
+ chapter_y = fay + (chapter_size / 2)
+ elseif options.chapters == 'lines-bottom' then
+ chapter_size = math.min(this.size_max / 3.5, size)
+ chapter_y = fay + size - (chapter_size / 2)
+ end
+
+ if chapter_size ~= nil then
+ -- for 1px chapter size, use the whole size of the bar including padding
+ chapter_size = size <= 1 and foreground_size or chapter_size
+ local chapter_half_size = chapter_size / 2
+
+ for i, chapter in ipairs(state.chapters) do
+ local chapter_x = display.width * (chapter.time / state.duration)
+ local color = chapter_x > fbx and options.color_foreground or options.color_background
+
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..color..'}')
+ ass:append(ass_opacity(options.chapters_opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+
+ if dots then
+ local bezier_stretch = chapter_size * 0.67
+ ass:move_to(chapter_x - chapter_half_size, chapter_y)
+ ass:bezier_curve(
+ chapter_x - chapter_half_size, chapter_y - bezier_stretch,
+ chapter_x + chapter_half_size, chapter_y - bezier_stretch,
+ chapter_x + chapter_half_size, chapter_y
+ )
+ ass:bezier_curve(
+ chapter_x + chapter_half_size, chapter_y + bezier_stretch,
+ chapter_x - chapter_half_size, chapter_y + bezier_stretch,
+ chapter_x - chapter_half_size, chapter_y
+ )
+ else
+ ass:rect_cw(chapter_x, chapter_y - chapter_half_size, chapter_x + 1, chapter_y + chapter_half_size)
+ end
+
+ ass:draw_stop()
+ end
+ end
+ end
+
+ if text_opacity > 0 then
+ -- Elapsed time
+ if state.elapsed_seconds then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
+ ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
+ ass:pos(spacing, fay + (size / 2))
+ ass:an(4)
+ ass:append(state.elapsed_time)
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
+ ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
+ ass:pos(spacing, fay + (size / 2))
+ ass:an(4)
+ ass:append(state.elapsed_time)
+ end
+
+ -- Remaining time
+ if state.remaining_seconds then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
+ ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
+ ass:pos(display.width - spacing, fay + (size / 2))
+ ass:an(6)
+ ass:append('-'..state.remaining_time)
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
+ ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
+ ass:pos(display.width - spacing, fay + (size / 2))
+ ass:an(6)
+ ass:append('-'..state.remaining_time)
+ end
+ end
+
+ if (this.proximity_raw == 0 or this.pressed) and not (elements.speed and elements.speed.dragging) then
+ -- Hovered time
+ local hovered_seconds = state.duration * (cursor.x / display.width)
+ local box_half_width_guesstimate = (this.font_size * 4.2) / 2
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'')
+ ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1)))
+ ass:pos(math.min(math.max(cursor.x, box_half_width_guesstimate), display.width - box_half_width_guesstimate), fay)
+ ass:an(2)
+ ass:append(mp.format_time(hovered_seconds))
+
+ -- Cursor line
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\xshad-1\\yshad0\\1c&H'..options.color_foreground..'\\4c&H'..options.color_background..'}')
+ ass:append(ass_opacity(0.2))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(cursor.x, fay, cursor.x + 1, fby)
+ ass:draw_stop()
+ end
+
+ return ass
+end
+
+function render_top_bar(this)
+ local opacity = this:get_effective_proximity()
+
+ if not this.enabled or opacity == 0 then return end
+
+ local ass = assdraw.ass_new()
+
+ if options.top_bar_controls then
+ -- Close button
+ local close = elements.window_controls_close
+ if close.proximity_raw == 0 then
+ -- Background on hover
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H2311e8}')
+ ass:append(ass_opacity(this.button_opacity, opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(close.ax, close.ay, close.bx, close.by)
+ ass:draw_stop()
+ end
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
+ ass:append(ass_opacity(this.button_opacity, opacity))
+ ass:pos(close.ax + (this.button_width / 2), (this.size / 2))
+ ass:draw_start()
+ ass:move_to(-this.icon_size, this.icon_size)
+ ass:line_to(this.icon_size, -this.icon_size)
+ ass:move_to(-this.icon_size, -this.icon_size)
+ ass:line_to(this.icon_size, this.icon_size)
+ ass:draw_stop()
+
+ -- Maximize button
+ local maximize = elements.window_controls_maximize
+ if maximize.proximity_raw == 0 then
+ -- Background on hover
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H222222}')
+ ass:append(ass_opacity(this.button_opacity, opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(maximize.ax, maximize.ay, maximize.bx, maximize.by)
+ ass:draw_stop()
+ end
+ ass:new_event()
+ ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&H000000}')
+ ass:append(ass_opacity({[3] = this.button_opacity}, opacity))
+ ass:pos(maximize.ax + (this.button_width / 2), (this.size / 2))
+ ass:draw_start()
+ ass:rect_cw(-this.icon_size + 1, -this.icon_size + 1, this.icon_size + 1, this.icon_size + 1)
+ ass:draw_stop()
+ ass:new_event()
+ ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&HFFFFFF}')
+ ass:append(ass_opacity({[3] = this.button_opacity}, opacity))
+ ass:pos(maximize.ax + (this.button_width / 2), (this.size / 2))
+ ass:draw_start()
+ ass:rect_cw(-this.icon_size, -this.icon_size, this.icon_size, this.icon_size)
+ ass:draw_stop()
+
+ -- Minimize button
+ local minimize = elements.window_controls_minimize
+ if minimize.proximity_raw == 0 then
+ -- Background on hover
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H222222}')
+ ass:append(ass_opacity(this.button_opacity, opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(minimize.ax, minimize.ay, minimize.bx, minimize.by)
+ ass:draw_stop()
+ end
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
+ ass:append(ass_opacity(this.button_opacity, opacity))
+ ass:append('{\\1a&HFF&}')
+ ass:pos(minimize.ax + (this.button_width / 2), (this.size / 2))
+ ass:draw_start()
+ ass:move_to(-this.icon_size, 0)
+ ass:line_to(this.icon_size, 0)
+ ass:draw_stop()
+ end
+
+ -- Window title
+ if options.top_bar_title and state.media_title then
+ local clip_coordinates = '0,0,'..(this.title_bx - this.spacing)..','..this.size
+
+ ass:new_event()
+ ass:append('{\\q2\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..clip_coordinates..')')
+ ass:append(ass_opacity(1, opacity))
+ ass:pos(0 + this.spacing, this.size / 2)
+ ass:an(4)
+ ass:append(state.media_title)
+ end
+
+ return ass
+end
+
+function render_volume(this)
+ local slider = elements.volume_slider
+ local opacity = this:get_effective_proximity()
+
+ if this.width == 0 or opacity == 0 then return end
+
+ local ass = assdraw.ass_new()
+
+ if slider.height > 0 then
+ -- Background bar coordinates
+ local bax = slider.ax
+ local bay = slider.ay
+ local bbx = slider.bx
+ local bby = slider.by
+
+ -- Foreground bar coordinates
+ local height_without_border = slider.height - (options.volume_border * 2)
+ local fax = slider.ax + options.volume_border
+ local fay = slider.ay + (height_without_border * (1 - math.min(state.volume / state.volume_max, 1))) + options.volume_border
+ local fbx = slider.bx - options.volume_border
+ local fby = slider.by - options.volume_border
+
+ -- Path to draw a foreground bar with a 100% volume indicator, already
+ -- clipped by volume level. Can't just clip it with rectangle, as it itself
+ -- also needs to be used as a path to clip the background bar and volume
+ -- number.
+ local fpath = assdraw.ass_new()
+ fpath:move_to(fbx, fby)
+ fpath:line_to(fax, fby)
+ local nudge_bottom_y = slider.nudge_y + slider.nudge_size
+ if fay <= nudge_bottom_y and slider.draw_nudge then
+ fpath:line_to(fax, math.min(nudge_bottom_y))
+ if fay <= slider.nudge_y then
+ fpath:line_to((fax + slider.nudge_size), slider.nudge_y)
+ local nudge_top_y = slider.nudge_y - slider.nudge_size
+ if fay <= nudge_top_y then
+ fpath:line_to(fax, nudge_top_y)
+ fpath:line_to(fax, fay)
+ fpath:line_to(fbx, fay)
+ fpath:line_to(fbx, nudge_top_y)
+ else
+ local triangle_side = fay - nudge_top_y
+ fpath:line_to((fax + triangle_side), fay)
+ fpath:line_to((fbx - triangle_side), fay)
+ end
+ fpath:line_to((fbx - slider.nudge_size), slider.nudge_y)
+ else
+ local triangle_side = nudge_bottom_y - fay
+ fpath:line_to((fax + triangle_side), fay)
+ fpath:line_to((fbx - triangle_side), fay)
+ end
+ fpath:line_to(fbx, nudge_bottom_y)
+ else
+ fpath:line_to(fax, fay)
+ fpath:line_to(fbx, fay)
+ end
+ fpath:line_to(fbx, fby)
+
+ -- Background
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..fpath.scale..', '..fpath.text..')}')
+ ass:append(ass_opacity(math.max(options.volume_opacity - 0.1, 0), opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:move_to(bax, bay)
+ ass:line_to(bbx, bay)
+ local half_border = options.volume_border / 2
+ if slider.draw_nudge then
+ ass:line_to(bbx, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
+ ass:line_to(bbx - slider.nudge_size + half_border, slider.nudge_y)
+ ass:line_to(bbx, slider.nudge_y + slider.nudge_size - half_border)
+ end
+ ass:line_to(bbx, bby)
+ ass:line_to(bax, bby)
+ if slider.draw_nudge then
+ ass:line_to(bax, slider.nudge_y + slider.nudge_size - half_border)
+ ass:line_to(bax + slider.nudge_size - half_border, slider.nudge_y)
+ ass:line_to(bax, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
+ end
+ ass:line_to(bax, bay)
+ ass:draw_stop()
+
+ -- Foreground
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
+ ass:append(ass_opacity(options.volume_opacity, opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:append(fpath.text)
+ ass:draw_stop()
+
+ -- Current volume value
+ local volume_string = tostring(round(state.volume * 10) / 10)
+ local font_size = round(((this.width * 0.6) - (#volume_string * (this.width / 20))) * options.volume_font_scale)
+ if fay < slider.by - slider.spacing then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\clip('..fpath.scale..', '..fpath.text..')}')
+ ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
+ ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
+ ass:an(2)
+ ass:append(volume_string)
+ end
+ if fay > slider.by - slider.spacing - font_size then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\iclip('..fpath.scale..', '..fpath.text..')}')
+ ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
+ ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
+ ass:an(2)
+ ass:append(volume_string)
+ end
+ end
+
+ -- Mute button
+ local mute = elements.volume_mute
+ local icon_name = state.mute and 'volume_muted' or 'volume'
+ ass:new_event()
+ ass:append(icon(
+ icon_name,
+ mute.ax + (mute.width / 2), mute.ay + (mute.height / 2), mute.width * 0.7, -- x, y, size
+ 0, 0, options.volume_border, -- shadow_x, shadow_y, shadow_size
+ 'background', options.volume_opacity * opacity -- backdrop, opacity
+ ))
+ return ass
+end
+
+function render_speed(this)
+ if not this.dragging and (elements.curtain.opacity > 0) then return end
+
+ local timeline = elements.timeline
+ local proximity = timeline:get_effective_proximity()
+ local opacity = this.forced_proximity and this.forced_proximity or (this.dragging and 1 or proximity)
+
+ if opacity == 0 then return end
+
+ local ass = assdraw.ass_new()
+
+ -- Coordinates
+ local ax = this.ax
+ local ay = this.ay + timeline.size_max - timeline:get_effective_size() - timeline.top_border - timeline.bottom_border
+ local bx = this.bx
+ local by = ay + this.height
+ local half_width = (this.width / 2)
+ local half_x = ax + half_width
+
+ -- Notches
+ local speed_at_center = state.speed
+ if this.dragging then
+ speed_at_center = this.dragging.start_speed + ((-this.dragging.distance / this.step_distance) * options.speed_step)
+ speed_at_center = math.min(math.max(speed_at_center, 0.01), 100)
+ end
+ local nearest_notch_speed = round(speed_at_center / this.notch_every) * this.notch_every
+ local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / this.notch_every) * this.notch_spacing)
+ local guide_size = math.floor(this.height / 7.5)
+ local notch_by = by - guide_size
+ local notch_ay_big = ay + round(this.font_size * 1.1)
+ local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
+ local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
+ local from_to_index = math.floor(this.notches / 2)
+
+ for i = -from_to_index, from_to_index do
+ local notch_speed = nearest_notch_speed + (i * this.notch_every)
+
+ if notch_speed < 0 or notch_speed > 100 then goto continue end
+
+ local notch_x = nearest_notch_x + (i * this.notch_spacing)
+ local notch_thickness = 1
+ local notch_ay = notch_ay_small
+ if (notch_speed % (this.notch_every * 10)) < 0.00000001 then
+ notch_ay = notch_ay_big
+ notch_thickness = 1
+ elseif (notch_speed % (this.notch_every * 5)) < 0.00000001 then
+ notch_ay = notch_ay_medium
+ end
+
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
+ ass:append(ass_opacity(math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1), opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:move_to(notch_x - notch_thickness, notch_ay)
+ ass:line_to(notch_x + notch_thickness, notch_ay)
+ ass:line_to(notch_x + notch_thickness, notch_by)
+ ass:line_to(notch_x - notch_thickness, notch_by)
+ ass:draw_stop()
+
+ ::continue::
+ end
+
+ -- Center guide
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
+ ass:append(ass_opacity(options.speed_opacity, opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:move_to(half_x, by - 2 - guide_size)
+ ass:line_to(half_x + guide_size, by - 2)
+ ass:line_to(half_x - guide_size, by - 2)
+ ass:draw_stop()
+
+ -- Speed value
+ local speed_text = (round(state.speed * 100) / 100)..'x'
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'}')
+ ass:append(ass_opacity(options.speed_opacity, opacity))
+ ass:pos(half_x, ay)
+ ass:an(8)
+ ass:append(speed_text)
+
+ return ass
+end
+
+function render_menu(this)
+ local ass = assdraw.ass_new()
+
+ if this.parent_menu then
+ ass:merge(this.parent_menu:render())
+ end
+
+ -- Menu title
+ if this.title then
+ -- Background
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
+ ass:append(ass_opacity(options.menu_opacity, this.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(this.ax, this.ay - this.item_height, this.bx, this.ay - 1)
+ ass:draw_stop()
+
+ -- Title
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad1\\b1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..'\\q2\\clip('..this.ax..','..this.ay - this.item_height..','..this.bx..','..this.ay..')}')
+ ass:append(ass_opacity(options.menu_opacity, this.opacity))
+ ass:pos(display.width / 2, this.ay - (this.item_height * 0.5))
+ ass:an(5)
+ ass:append(this.title)
+ end
+
+ local scroll_area_clip = '\\clip('..this.ax..','..this.ay..','..this.bx..','..this.by..')'
+
+ for index, item in ipairs(this.items) do
+ local item_ay = this.ay - this.scroll_y + (this.item_height * (index - 1) + this.item_spacing * (index - 1))
+ local item_by = item_ay + this.item_height
+ local item_clip = ''
+
+ -- Clip items overflowing scroll area
+ if item_ay <= this.ay or item_by >= this.by then
+ item_clip = scroll_area_clip
+ end
+
+ if item_by < this.ay or item_ay > this.by then goto continue end
+
+ local is_active = this.active_item == index
+ local font_color, background_color, ass_shadow, ass_shadow_color
+ local icon_size = this.font_size
+
+ if is_active then
+ font_color, background_color = options.color_foreground_text, options.color_foreground
+ ass_shadow, ass_shadow_color = '\\shad0', ''
+ else
+ font_color, background_color = options.color_background_text, options.color_background
+ ass_shadow, ass_shadow_color = '\\shad1', '\\4c&H'..background_color
+ end
+
+ local has_submenu = item.items ~= nil
+ local hint_width = 0
+ if item.hint then
+ hint_width = text_width_estimate(item.hint:len(), this.font_size) + this.item_content_spacing
+ elseif has_submenu then
+ hint_width = icon_size + this.item_content_spacing
+ end
+
+ -- Background
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..background_color..item_clip..'}')
+ ass:append(ass_opacity(options.menu_opacity, this.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(this.ax, item_ay, this.bx, item_by)
+ ass:draw_stop()
+
+ -- Selected highlight
+ if this.selected_item == index then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..item_clip..'}')
+ ass:append(ass_opacity(0.1, this.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(this.ax, item_ay, this.bx, item_by)
+ ass:draw_stop()
+ end
+
+ -- Title
+ if item.title then
+ item.ass_save_title = item.ass_save_title or item.title:gsub("([{}])","\\%1")
+ local title_clip_x = (this.bx - hint_width - this.item_content_spacing)
+ local title_clip = '\\clip('..this.ax..','..math.max(item_ay, this.ay)..','..title_clip_x..','..math.min(item_by, this.by)..')'
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\shad1\\1c&H'..font_color..'\\4c&H'..background_color..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..title_clip..'\\q2}')
+ ass:append(ass_opacity(options.menu_opacity, this.opacity))
+ ass:pos(this.ax + this.item_content_spacing, item_ay + (this.item_height / 2))
+ ass:an(4)
+ ass:append(item.ass_save_title)
+ end
+
+ -- Hint
+ if item.hint then
+ item.ass_save_hint = item.ass_save_hint or item.hint:gsub("([{}])","\\%1")
+ ass:new_event()
+ ass:append('{\\blur0\\bord0'..ass_shadow..'\\1c&H'..font_color..''..ass_shadow_color..'\\fn'..config.font..'\\fs'..(this.font_size - 1)..bold_tag..item_clip..'}')
+ ass:append(ass_opacity(options.menu_opacity * (has_submenu and 1 or 0.5), this.opacity))
+ ass:pos(this.bx - this.item_content_spacing, item_ay + (this.item_height / 2))
+ ass:an(6)
+ ass:append(item.ass_save_hint)
+ elseif has_submenu then
+ ass:new_event()
+ ass:append(icon(
+ 'arrow_right',
+ this.bx - this.item_content_spacing - (icon_size / 2), -- x
+ item_ay + (this.item_height / 2), -- y
+ icon_size, -- size
+ 0, 0, 1, -- shadow_x, shadow_y, shadow_size
+ is_active and 'foreground' or 'background', this.opacity, -- backdrop, opacity
+ item_clip
+ ))
+ end
+
+ ::continue::
+ end
+
+ -- Scrollbar
+ if this.scroll_height > 0 then
+ local groove_height = this.height - 2
+ local thumb_height = math.max((this.height / (this.scroll_height + this.height)) * groove_height, 40)
+ local thumb_y = this.ay + 1 + ((this.scroll_y / this.scroll_height) * (groove_height - thumb_height))
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
+ ass:append(ass_opacity(options.menu_opacity, this.opacity * 0.8))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(this.bx - 3, thumb_y, this.bx - 1, thumb_y + thumb_height)
+ ass:draw_stop()
+ end
+
+ return ass
+end
+
+-- MAIN RENDERING
+
+-- Request that render() is called.
+-- The render is then either executed immediately, or rate-limited if it was
+-- called a small time ago.
+function request_render()
+ if state.render_timer == nil then
+ state.render_timer = mp.add_timeout(0, render)
+ end
+
+ if not state.render_timer:is_enabled() then
+ local now = mp.get_time()
+ local timeout = config.render_delay - (now - state.render_last_time)
+ if timeout < 0 then
+ timeout = 0
+ end
+ state.render_timer.timeout = timeout
+ state.render_timer:resume()
+ end
+end
+
+function render()
+ state.render_last_time = mp.get_time()
+
+ -- Actual rendering
+ local ass = assdraw.ass_new()
+
+ for _, element in elements.ipairs() do
+ local result = element:maybe('render')
+ if result then
+ ass:new_event()
+ ass:merge(result)
+ end
+ end
+
+ -- submit
+ if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then
+ return
+ end
+
+ osd.res_x = display.width
+ osd.res_y = display.height
+ osd.data = ass.text
+ osd.z = 2000
+ osd:update()
+end
+
+-- STATIC ELEMENTS
+
+if itable_find({'flash', 'static'}, options.pause_indicator) then
+ elements:add('pause_indicator', Element.new({
+ base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8,
+ paused = false,
+ is_flash = options.pause_indicator == 'flash',
+ is_static = options.pause_indicator == 'static',
+ opacity = 0,
+ init = function(this)
+ local initial_call = true
+ mp.observe_property('pause', 'bool', function(_, paused)
+ if initial_call then
+ initial_call = false
+ return
+ end
+
+ this.paused = paused
+
+ if options.pause_indicator == 'flash' then
+ this.opacity = 1
+ this:tween_property('opacity', 1, 0, 0.15)
+ else
+ this.opacity = paused and 1 or 0
+ request_render()
+ end
+
+ end)
+ end,
+ render = function(this)
+ if this.opacity == 0 then return end
+
+ local ass = assdraw.ass_new()
+
+ -- Background fadeout
+ if this.is_static then
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
+ ass:append(ass_opacity(0.3, this.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(0, 0, display.width, display.height)
+ ass:draw_stop()
+ end
+
+ -- Icon
+ local size = round((math.min(display.width, display.height) * (this.is_static and 0.20 or 0.15)) / 2)
+
+ size = size + size * (1 - this.opacity)
+
+ if this.paused then
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
+ ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
+ ass:pos(display.width / 2, display.height / 2)
+ ass:draw_start()
+ ass:rect_cw(-size, -size, -size / 3, size)
+ ass:draw_stop()
+
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
+ ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
+ ass:pos(display.width / 2, display.height / 2)
+ ass:draw_start()
+ ass:rect_cw(size / 3, -size, size, size)
+ ass:draw_stop()
+ elseif this.is_flash then
+ ass:new_event()
+ ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
+ ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
+ ass:pos(display.width / 2, display.height / 2)
+ ass:draw_start()
+ ass:move_to(-size * 0.6, -size)
+ ass:line_to(size, 0)
+ ass:line_to(-size * 0.6, size)
+ ass:draw_stop()
+ end
+
+ return ass
+ end
+ }))
+end
+elements:add('timeline', Element.new({
+ captures = {mouse_buttons = true, wheel = true},
+ pressed = false,
+ size_max = 0, size_min = 0, -- set in `on_display_resize` handler based on `state.fullscreen`
+ size_min_override = options.timeline_start_hidden and 0 or nil, -- used for toggle-progress command
+ font_size = 0, -- calculated in on_display_resize
+ top_border = options.timeline_border,
+ bottom_border = 0, -- set dynamically in `border` property observer
+ init = function(this)
+ -- Toggle 1px bottom border for timeline in no-border mode
+ mp.observe_property('border', 'bool', function(_, border)
+ this.bottom_border = not border and options.timeline_border or 0
+ request_render()
+ end)
+ end,
+ get_effective_proximity = function(this)
+ if (elements.volume_slider and elements.volume_slider.pressed) then return 0 end
+ if this.pressed then return 1 end
+ return this.forced_proximity and this.forced_proximity or this.proximity
+ end,
+ get_effective_size_min = function(this)
+ return this.size_min_override or this.size_min
+ end,
+ get_effective_size = function(this)
+ if elements.speed and elements.speed.dragging then return this.size_max end
+ local size_min = this:get_effective_size_min()
+ return size_min + math.ceil((this.size_max - size_min) * this:get_effective_proximity())
+ end,
+ on_display_resize = function(this)
+ if state.fullscreen or state.maximized then
+ this.size_min = options.timeline_size_min_fullscreen
+ this.size_max = options.timeline_size_max_fullscreen
+ else
+ this.size_min = options.timeline_size_min
+ this.size_max = options.timeline_size_max
+ end
+ this.font_size = math.floor(math.min((this.size_max + 60) * 0.2, this.size_max * 0.96) * options.timeline_font_scale)
+ this.ax = 0
+ this.ay = display.height - this.size_max - this.top_border - this.bottom_border
+ this.bx = display.width
+ this.by = display.height
+ end,
+ set_from_cursor = function(this)
+ mp.commandv('seek', ((cursor.x / display.width) * 100), 'absolute-percent+exact')
+ end,
+ on_mbtn_left_down = function(this)
+ this.pressed = true
+ this:set_from_cursor()
+ end,
+ on_global_mbtn_left_up = function(this) this.pressed = false end,
+ on_global_mouse_leave = function(this) this.pressed = false end,
+ on_global_mouse_move = function(this)
+ if this.pressed then this:set_from_cursor() end
+ end,
+ on_wheel_up = function(this)
+ if options.timeline_step > 0 then mp.commandv('seek', -options.timeline_step) end
+ end,
+ on_wheel_down = function(this)
+ if options.timeline_step > 0 then mp.commandv('seek', options.timeline_step) end
+ end,
+ render = render_timeline,
+}))
+if options.top_bar_controls or options.top_bar_title then
+ elements:add('top_bar', Element.new({
+ button_opacity = 0.8,
+ enabled = false,
+ init = function(this)
+ mp.observe_property('border', 'bool', function(_, border)
+ this.enabled = not border
+ end)
+ end,
+ get_effective_proximity = function(this)
+ if (elements.volume_slider and elements.volume_slider.pressed) or elements.curtain.opacity > 0 then return 0 end
+ return this.forced_proximity and this.forced_proximity or this.proximity
+ end,
+ on_display_resize = function(this)
+ this.size = (state.fullscreen or state.maximized) and options.top_bar_size_fullscreen or options.top_bar_size
+ this.icon_size = round(this.size / 8)
+ this.spacing = math.ceil(this.size * 0.25)
+ this.font_size = math.floor(this.size - (this.spacing * 2))
+ this.button_width = round(this.size * 1.15)
+ this.title_bx = display.width - (options.top_bar_controls and (this.button_width * 3) or 0)
+ this.ax = options.top_bar_title and 0 or this.title_bx
+ this.ay = 0
+ this.bx = display.width
+ this.by = this.size
+ end,
+ render = render_top_bar,
+ }))
+end
+if options.top_bar_controls then
+ elements:add('window_controls_minimize', Element.new({
+ captures = {mouse_buttons = true},
+ on_display_resize = function(this)
+ this.ax = display.width - (elements.top_bar.button_width * 3)
+ this.ay = 0
+ this.bx = this.ax + elements.top_bar.button_width
+ this.by = elements.top_bar.size
+ end,
+ on_mbtn_left_down = function() mp.commandv('cycle', 'window-minimized') end
+ }))
+ elements:add('window_controls_maximize', Element.new({
+ captures = {mouse_buttons = true},
+ on_display_resize = function(this)
+ this.ax = display.width - (elements.top_bar.button_width * 2)
+ this.ay = 0
+ this.bx = this.ax + elements.top_bar.button_width
+ this.by = elements.top_bar.size
+ end,
+ on_mbtn_left_down = function() mp.commandv('cycle', 'window-maximized') end
+ }))
+ elements:add('window_controls_close', Element.new({
+ captures = {mouse_buttons = true},
+ on_display_resize = function(this)
+ this.ax = display.width - elements.top_bar.button_width
+ this.ay = 0
+ this.bx = this.ax + elements.top_bar.button_width
+ this.by = elements.top_bar.size
+ end,
+ on_mbtn_left_down = function() mp.commandv('quit') end
+ }))
+end
+if itable_find({'left', 'right'}, options.volume) then
+ elements:add('volume', Element.new({
+ width = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
+ height = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
+ margin = nil, -- set in `on_display_resize` handler based on `state.fullscreen`
+ get_effective_proximity = function(this)
+ if elements.volume_slider.pressed then return 1 end
+ if elements.timeline.proximity_raw == 0 or elements.curtain.opacity > 0 then return 0 end
+ return this.forced_proximity and this.forced_proximity or this.proximity
+ end,
+ on_display_resize = function(this)
+ this.width = (state.fullscreen or state.maximized) and options.volume_size_fullscreen or options.volume_size
+ this.height = round(math.min(this.width * 8, (elements.timeline.ay - elements.top_bar.size) * 0.8))
+ -- Don't bother rendering this if too small
+ if this.height < (this.width * 2) then
+ this.height = 0
+ end
+ this.margin = this.width / 2
+ this.ax = round(options.volume == 'left' and this.margin or display.width - this.margin - this.width)
+ this.ay = round((display.height - this.height) / 2)
+ this.bx = round(this.ax + this.width)
+ this.by = round(this.ay + this.height)
+ end,
+ render = render_volume,
+ }))
+ elements:add('volume_mute', Element.new({
+ captures = {mouse_buttons = true},
+ width = 0,
+ height = 0,
+ on_display_resize = function(this)
+ this.width = elements.volume.width
+ this.height = this.width
+ this.ax = elements.volume.ax
+ this.ay = elements.volume.by - this.height
+ this.bx = elements.volume.bx
+ this.by = elements.volume.by
+ end,
+ on_mbtn_left_down = function(this) mp.commandv('cycle', 'mute') end
+ }))
+ elements:add('volume_slider', Element.new({
+ captures = {mouse_buttons = true, wheel = true},
+ pressed = false,
+ width = 0,
+ height = 0,
+ nudge_y = 0, -- vertical position where volume overflows 100
+ nudge_size = nil, -- set on resize
+ font_size = nil,
+ spacing = nil,
+ on_display_resize = function(this)
+ this.ax = elements.volume.ax
+ this.ay = elements.volume.ay
+ this.bx = elements.volume.bx
+ this.by = elements.volume_mute.ay
+ this.width = this.bx - this.ax
+ this.height = this.by - this.ay
+ this.nudge_y = this.by - round(this.height * (100 / state.volume_max))
+ this.nudge_size = round(elements.volume.width * 0.18)
+ this.draw_nudge = this.ay < this.nudge_y
+ this.spacing = round(this.width * 0.2)
+ end,
+ set_from_cursor = function(this)
+ local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
+ local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
+ new_volume = round(new_volume / options.volume_step) * options.volume_step
+ if state.volume ~= new_volume then mp.commandv('set', 'volume', math.min(new_volume, state.volume_max)) end
+ end,
+ on_mbtn_left_down = function(this)
+ this.pressed = true
+ this:set_from_cursor()
+ end,
+ on_global_mbtn_left_up = function(this) this.pressed = false end,
+ on_global_mouse_leave = function(this) this.pressed = false end,
+ on_global_mouse_move = function(this)
+ if this.pressed then this:set_from_cursor() end
+ end,
+ on_wheel_up = function(this)
+ local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
+ mp.commandv('set', 'volume', math.min(current_rounded_volume + options.volume_step, state.volume_max))
+ end,
+ on_wheel_down = function(this)
+ local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
+ mp.commandv('set', 'volume', math.min(current_rounded_volume - options.volume_step, state.volume_max))
+ end,
+ }))
+end
+if options.speed then
+ elements:add('speed', Element.new({
+ captures = {mouse_buttons = true, wheel = true},
+ dragging = nil,
+ width = 0,
+ height = 0,
+ notches = 10,
+ notch_every = 0.1,
+ step_distance = nil,
+ font_size = nil,
+ init = function(this)
+ -- Fade out/in on timeline mouse enter/leave
+ elements.timeline:on('mouse_enter', function()
+ if not this.dragging then this:fadeout() end
+ end)
+ elements.timeline:on('mouse_leave', function()
+ if not this.dragging then this:fadein() end
+ end)
+ end,
+ fadeout = function(this)
+ this:tween_property('forced_proximity', 1, 0, function(this)
+ this.forced_proximity = 0
+ end)
+ end,
+ fadein = function(this)
+ local get_current_proximity = function() return this.proximity end
+ this:tween_property('forced_proximity', 0, get_current_proximity, function(this)
+ this.forced_proximity = nil
+ end)
+ end,
+ on_display_resize = function(this)
+ this.height = (state.fullscreen or state.maximized) and options.speed_size_fullscreen or options.speed_size
+ this.width = round(this.height * 3.6)
+ this.notch_spacing = this.width / this.notches
+ this.step_distance = this.notch_spacing * (options.speed_step / this.notch_every)
+ this.ax = (display.width - this.width) / 2
+ this.by = display.height - elements.timeline.size_max
+ this.ay = this.by - this.height
+ this.bx = this.ax + this.width
+ this.font_size = round(this.height * 0.48 * options.speed_font_scale)
+ end,
+ set_from_cursor = function(this)
+ local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
+ local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
+ new_volume = round(new_volume / options.volume_step) * options.volume_step
+ if state.volume ~= new_volume then mp.commandv('set', 'volume', new_volume) end
+ end,
+ on_mbtn_left_down = function(this)
+ this:tween_stop() -- Stop and cleanup possible ongoing animations
+ this.dragging = {
+ start_time = mp.get_time(),
+ start_x = cursor.x,
+ distance = 0,
+ start_speed = state.speed
+ }
+ end,
+ on_global_mouse_move = function(this)
+ if not this.dragging then return end
+
+ this.dragging.distance = cursor.x - this.dragging.start_x
+ local steps_dragged = round(-this.dragging.distance / this.step_distance)
+ local new_speed = this.dragging.start_speed + (steps_dragged * options.speed_step)
+ mp.set_property_native('speed', round(new_speed * 100) / 100)
+ end,
+ on_mbtn_left_up = function(this)
+ -- Reset speed on short clicks
+ if this.dragging and math.abs(this.dragging.distance) < 6 and mp.get_time() - this.dragging.start_time < 0.15 then
+ mp.set_property_native('speed', 1)
+ end
+ end,
+ on_global_mbtn_left_up = function(this)
+ if this.dragging and elements.timeline.proximity_raw == 0 then
+ this:fadeout()
+ end
+ this.dragging = nil
+ request_render()
+ end,
+ on_global_mouse_leave = function(this)
+ this.dragging = nil
+ request_render()
+ end,
+ on_wheel_up = function(this)
+ mp.set_property_native('speed', state.speed - options.speed_step)
+ end,
+ on_wheel_down = function(this)
+ mp.set_property_native('speed', state.speed + options.speed_step)
+ end,
+ render = render_speed,
+ }))
+end
+elements:add('curtain', Element.new({
+ opacity = 0,
+ fadeout = function(this)
+ this:tween_property('opacity', this.opacity, 0);
+ end,
+ fadein = function(this)
+ this:tween_property('opacity', this.opacity, 1);
+ end,
+ render = function(this)
+ if this.opacity > 0 then
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
+ ass:append(ass_opacity(0.4, this.opacity))
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(0, 0, display.width, display.height)
+ ass:draw_stop()
+ return ass
+ end
+ end
+}))
+
+-- CHAPTERS SERIALIZATION
+
+-- Parse `chapter_ranges` option into workable data structure
+for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do
+ local start_patterns, color, opacity, end_patterns = string.match(definition, '([^<]+)<(%x%x%x%x%x%x):(%d?%.?%d*)>([^>]+)')
+
+ -- Invalid definition
+ if start_patterns == nil then goto continue end
+
+ start_patterns = start_patterns:lower()
+ end_patterns = end_patterns:lower()
+ local uses_bof = start_patterns:find('{bof}') ~= nil
+ local uses_eof = end_patterns:find('{eof}') ~= nil
+ local chapter_range = {
+ start_patterns = split(start_patterns, '|'),
+ end_patterns = split(end_patterns, '|'),
+ color = color,
+ opacity = tonumber(opacity),
+ ranges = {}
+ }
+
+ -- Filter out special keywords so we don't use them when matching titles
+ if uses_bof then
+ chapter_range.start_patterns = itable_remove(chapter_range.start_patterns, '{bof}')
+ end
+ if uses_eof and chapter_range.end_patterns then
+ chapter_range.end_patterns = itable_remove(chapter_range.end_patterns, '{eof}')
+ end
+
+ chapter_range['serialize'] = function (chapters)
+ chapter_range.ranges = {}
+ local current_range = nil
+ -- bof and eof should be used only once per timeline
+ -- eof is only used when last range is missing end
+ local bof_used = false
+
+ function start_range(chapter)
+ -- If there is already a range started, should we append or overwrite?
+ -- I chose overwrite here.
+ current_range = {['start'] = chapter}
+ end
+
+ function end_range(chapter)
+ current_range['end'] = chapter
+ chapter_range.ranges[#chapter_range.ranges + 1] = current_range
+ -- Mark both chapter objects
+ current_range['start']._uosc_used_as_range_point = true
+ current_range['end']._uosc_used_as_range_point = true
+ -- Clear for next range
+ current_range = nil
+ end
+
+ for _, chapter in ipairs(chapters) do
+ if type(chapter.title) == 'string' then
+ local lowercase_title = chapter.title:lower()
+ local is_end = false
+ local is_start = false
+
+ -- Is ending check and handling
+ if chapter_range.end_patterns then
+ for _, end_pattern in ipairs(chapter_range.end_patterns) do
+ is_end = is_end or lowercase_title:find(end_pattern) ~= nil
+ end
+
+ if is_end then
+ if current_range == nil and uses_bof and not bof_used then
+ bof_used = true
+ start_range({time = 0})
+ end
+ if current_range ~= nil then
+ end_range(chapter)
+ else
+ is_end = false
+ end
+ end
+ end
+
+ -- Is start check and handling
+ for _, start_pattern in ipairs(chapter_range.start_patterns) do
+ is_start = is_start or lowercase_title:find(start_pattern) ~= nil
+ end
+
+ if is_start then start_range(chapter) end
+ end
+ end
+
+ -- If there is an unfinished range and range type accepts eof, use it
+ if current_range ~= nil and uses_eof then
+ end_range({time = state.duration or infinity})
+ end
+ end
+
+ state.chapter_ranges = state.chapter_ranges or {}
+ state.chapter_ranges[#state.chapter_ranges + 1] = chapter_range
+
+ ::continue::
+end
+
+function parse_chapters()
+ -- Sometimes state.duration is not initialized yet for some reason
+ state.duration = mp.get_property_native('duration')
+
+ local chapters = get_normalized_chapters()
+
+ if not chapters or not state.duration then return end
+
+ -- Reset custom ranges
+ for _, chapter_range in ipairs(state.chapter_ranges or {}) do
+ chapter_range.serialize(chapters)
+ end
+
+ -- Filter out chapters that were used as ranges
+ state.chapters = itable_remove(chapters, function(chapter)
+ return chapter._uosc_used_as_range_point == true
+ end)
+
+ request_render()
+end
+
+-- CONTEXT MENU SERIALIZATION
+
+state.context_menu_items = (function()
+ local input_conf_path = mp.command_native({'expand-path', '~~/input.conf'})
+ local input_conf_meta, meta_error = utils.file_info(input_conf_path)
+
+ -- File doesn't exist
+ if not input_conf_meta or not input_conf_meta.is_file then return end
+
+ local items = {}
+ local items_by_command = {}
+ local submenus_by_id = {}
+
+ for line in io.lines(input_conf_path) do
+ local key, command, title = string.match(line, ' *([%S]+) +(.*) #! *(.*)')
+ if key then
+ local is_dummy = key:sub(1, 1) == '#'
+ local submenu_id = ''
+ local target_menu = items
+ local title_parts = split(title or '', ' *> *')
+
+ for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do
+ if index < #title_parts then
+ submenu_id = submenu_id .. title_part
+
+ if not submenus_by_id[submenu_id] then
+ submenus_by_id[submenu_id] = {title = title_part, items = {}}
+ target_menu[#target_menu + 1] = submenus_by_id[submenu_id]
+ end
+
+ target_menu = submenus_by_id[submenu_id].items
+ else
+ -- If command is already in menu, just append the key to it
+ if items_by_command[command] then
+ items_by_command[command].hint = items_by_command[command].hint..', '..key
+ else
+ items_by_command[command] = {
+ title = title_part,
+ hint = not is_dummy and key or nil,
+ value = command
+ }
+ target_menu[#target_menu + 1] = items_by_command[command]
+ end
+ end
+ end
+ end
+ end
+
+ if #items > 0 then return items end
+end)()
+
+-- EVENT HANDLERS
+
+function create_state_setter(name)
+ return function(_, value)
+ state[name] = value
+ dispatch_event_to_elements('prop_'..name, value)
+ request_render()
+ end
+end
+
+function dispatch_event_to_elements(name, ...)
+ for _, element in pairs(elements) do
+ if element.proximity_raw == 0 then
+ element:maybe('on_'..name, ...)
+ end
+ element:maybe('on_global_'..name, ...)
+ end
+end
+
+function create_event_to_elements_dispatcher(name, ...)
+ return function(...) dispatch_event_to_elements(name, ...) end
+end
+
+function handle_mouse_leave()
+ -- Slowly fadeout elements that are currently visible
+ for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do
+ local element = elements[element_name]
+ if element and element.proximity > 0 then
+ element:tween_property('forced_proximity', element:get_effective_proximity(), 0, function()
+ element.forced_proximity = nil
+ end)
+ end
+ end
+
+ cursor.hidden = true
+ update_proximities()
+ dispatch_event_to_elements('mouse_leave')
+end
+
+function handle_mouse_enter()
+ cursor.hidden = false
+ cursor.x, cursor.y = mp.get_mouse_pos()
+ tween_element_stop(state)
+ dispatch_event_to_elements('mouse_enter')
+end
+
+function handle_mouse_move()
+ -- Handle case when we are in cursor hidden state but not left the actual
+ -- window (i.e. when autohide simulates mouse_leave).
+ if cursor.hidden then
+ handle_mouse_enter()
+ return
+ end
+
+ cursor.x, cursor.y = mp.get_mouse_pos()
+ update_proximities()
+ dispatch_event_to_elements('mouse_move')
+ request_render()
+
+ -- Restart timer that hides UI when mouse is autohidden
+ if options.autohide then
+ state.cursor_autohide_timer:kill()
+ state.cursor_autohide_timer:resume()
+ end
+end
+
+function navigate_directory(direction)
+ local path = mp.get_property_native("path")
+
+ if not path or is_protocol(path) then return end
+
+ local next_file = get_adjacent_file(path, direction, options.media_types)
+
+ if next_file then
+ mp.commandv("loadfile", utils.join_path(serialize_path(path).dirname, next_file))
+ end
+end
+
+function load_file_in_current_directory(index)
+ local path = mp.get_property_native("path")
+
+ if not path or is_protocol(path) then return end
+
+ local dirname = serialize_path(path).dirname
+ local files = get_files_in_directory(dirname, options.media_types)
+
+ if not files then return end
+ if index < 0 then index = #files + index + 1 end
+
+ if files[index] then
+ mp.commandv("loadfile", utils.join_path(dirname, files[index]))
+ end
+end
+
+-- MENUS
+
+function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop)
+ return function()
+ if menu:is_open(track_type) then menu:close() return end
+
+ local items = {}
+ local active_item = nil
+
+ for index, track in ipairs(mp.get_property_native('track-list')) do
+ if track.type == track_type then
+ if track.selected then active_item = track.id end
+
+ items[#items + 1] = {
+ title = (track.title and track.title or 'Track '..track.id),
+ hint = track.lang and track.lang:upper() or nil,
+ value = track.id
+ }
+ end
+ end
+
+ -- Add option to disable a subtitle track. This works for all tracks,
+ -- but why would anyone want to disable audio or video? Better to not
+ -- let people mistakenly select what is unwanted 99.999% of the time.
+ -- If I'm mistaken and there is an active need for this, feel free to
+ -- open an issue.
+ if track_type == 'sub' then
+ active_item = active_item and active_item + 1 or 1
+ table.insert(items, 1, {hint = 'disabled', value = nil})
+ end
+
+ menu:open(items, function(id)
+ mp.commandv('set', track_prop, id and id or 'no')
+
+ -- If subtitle track was selected, assume user also wants to see it
+ if id and track_type == 'sub' then
+ mp.commandv('set', 'sub-visibility', 'yes')
+ end
+
+ menu:close()
+ end, {type = track_type, title = menu_title, active_item = active_item})
+ end
+end
+
+-- `menu_options`:
+-- **allowed_types** - table with file extensions to display
+-- **active_path** - full path of a file to preselect
+-- Rest of the options are passed to `menu:open()`
+function open_file_navigation_menu(directory, handle_select, menu_options)
+ directory = serialize_path(directory)
+ local directories, error = utils.readdir(directory.path, 'dirs')
+ local files, error = get_files_in_directory(directory.path, menu_options.allowed_types)
+
+ if not files or not directories then
+ msg.error('Retrieving files from '..directory..' failed: '..(error or ''))
+ return
+ end
+
+ -- Files are already sorted
+ table.sort(directories, word_order_comparator)
+
+ -- Pre-populate items with parent directory selector if not at root
+ local items = not directory.dirname and {} or {
+ {title = '..', hint = 'parent dir', value = directory.dirname}
+ }
+
+ for _, dir in ipairs(directories) do
+ local serialized = serialize_path(utils.join_path(directory.path, dir))
+ items[#items + 1] = {title = serialized.basename, value = serialized.path, hint = '/'}
+ end
+
+ menu_options.active_item = nil
+
+ for _, file in ipairs(files) do
+ local serialized = serialize_path(utils.join_path(directory.path, file))
+ local item_index = #items + 1
+
+ items[item_index] = {
+ title = serialized.basename,
+ value = serialized.path,
+ }
+
+ if menu_options.active_path == serialized.path then
+ menu_options.active_item = item_index
+ end
+ end
+
+ menu_options.title = directory.basename..'/'
+
+ menu:open(items, function(path)
+ local meta, error = utils.file_info(path)
+
+ if not meta then
+ msg.error('Retrieving file info for '..path..' failed: '..(error or ''))
+ return
+ end
+
+ if meta.is_dir then
+ open_file_navigation_menu(path, handle_select, menu_options)
+ else
+ handle_select(path)
+ menu:close()
+ end
+ end, menu_options)
+end
+
+-- VALUE SERIALIZATION/NORMALIZATION
+
+options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
+options.chapters = itable_find({'dots', 'lines', 'lines-top', 'lines-bottom'}, options.chapters) and options.chapters or 'none'
+options.media_types = split(options.media_types, ' *, *')
+options.subtitle_types = split(options.subtitle_types, ' *, *')
+options.timeline_cached_ranges = (function()
+ if options.timeline_cached_ranges == '' or options.timeline_cached_ranges == 'no' then return nil end
+ local parts = split(options.timeline_cached_ranges, ':')
+ return parts[1] and {color = parts[1], opacity = tonumber(parts[2])} or nil
+end)()
+
+-- HOOKS
+mp.register_event('file-loaded', parse_chapters)
+mp.observe_property('chapter-list', 'native', parse_chapters)
+mp.observe_property('duration', 'number', create_state_setter('duration'))
+mp.observe_property('media-title', 'string', create_state_setter('media_title'))
+mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen'))
+mp.observe_property('window-maximized', 'bool', create_state_setter('maximized'))
+mp.observe_property('idle-active', 'bool', create_state_setter('idle'))
+mp.observe_property('speed', 'number', create_state_setter('speed'))
+mp.observe_property('pause', 'bool', create_state_setter('pause'))
+mp.observe_property('volume', 'number', create_state_setter('volume'))
+mp.observe_property('volume-max', 'number', create_state_setter('volume_max'))
+mp.observe_property('mute', 'bool', create_state_setter('mute'))
+mp.observe_property('playback-time', 'number', function(name, val)
+ -- Ignore the initial call with nil value
+ if val == nil then return end
+
+ state.position = val
+ state.elapsed_seconds = val
+ state.elapsed_time = state.elapsed_seconds and mp.format_time(state.elapsed_seconds) or nil
+ state.remaining_seconds = mp.get_property_native('playtime-remaining')
+ state.remaining_time = state.remaining_seconds and mp.format_time(state.remaining_seconds) or nil
+
+ request_render()
+end)
+mp.observe_property('osd-dimensions', 'native', function(name, val)
+ update_display_dimensions()
+ request_render()
+end)
+mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
+ if cache_state == nil then
+ state.cached_ranges = nil
+ return
+ end
+ local cache_ranges = cache_state['seekable-ranges']
+ state.cached_ranges = #cache_ranges > 0 and cache_ranges or nil
+end)
+
+-- CONTROLS
+
+-- Mouse movement key binds
+local base_keybinds = {
+ {'mouse_move', handle_mouse_move},
+ {'mouse_leave', handle_mouse_leave},
+ {'mouse_enter', handle_mouse_enter},
+}
+if options.pause_on_click_shorter_than > 0 then
+ -- Cycles pause when click is shorter than `options.pause_on_click_shorter_than`
+ -- while filtering out double clicks.
+ local duration_seconds = options.pause_on_click_shorter_than / 1000
+ local last_down_event;
+ local click_timer = mp.add_timeout(duration_seconds, function()
+ mp.command('cycle pause')
+ end);
+ click_timer:kill()
+ base_keybinds[#base_keybinds + 1] = {'mbtn_left', function()
+ if mp.get_time() - last_down_event < duration_seconds then
+ click_timer:resume()
+ end
+ end, function()
+ if click_timer:is_enabled() then
+ click_timer:kill()
+ last_down_event = 0
+ else
+ last_down_event = mp.get_time()
+ end
+ end
+ }
+end
+mp.set_key_bindings(base_keybinds, 'mouse_movement', 'force')
+mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor')
+
+-- Context based key bind groups
+
+forced_key_bindings = (function()
+ mp.set_key_bindings({
+ {'mbtn_left', create_event_to_elements_dispatcher('mbtn_left_up'), create_event_to_elements_dispatcher('mbtn_left_down')},
+ {'mbtn_left_dbl', 'ignore'},
+ }, 'mouse_buttons', 'force')
+ mp.set_key_bindings({
+ {'wheel_up', create_event_to_elements_dispatcher('wheel_up')},
+ {'wheel_down', create_event_to_elements_dispatcher('wheel_down')},
+ }, 'wheel', 'force')
+
+ local groups = {}
+ for _, group in ipairs({'mouse_buttons', 'wheel'}) do
+ groups[group] = {
+ is_enabled = false,
+ enable = function(this)
+ if this.is_enabled then return end
+ this.is_enabled = true
+ mp.enable_key_bindings(group)
+ end,
+ disable = function(this)
+ if not this.is_enabled then return end
+ this.is_enabled = false
+ mp.disable_key_bindings(group)
+ end,
+ }
+ end
+ return groups
+end)()
+
+-- KEY BINDABLE FEATURES
+
+mp.add_key_binding(nil, 'peek-timeline', function()
+ if elements.timeline.proximity > 0.5 then
+ elements.timeline:tween_property('proximity', elements.timeline.proximity, 0)
+ else
+ elements.timeline:tween_property('proximity', elements.timeline.proximity, 1)
+ end
+end)
+mp.add_key_binding(nil, 'toggle-progress', function()
+ local timeline = elements.timeline
+ if timeline.size_min_override then
+ timeline:tween_property('size_min_override', timeline.size_min_override, timeline.size_min, function()
+ timeline.size_min_override = nil
+ end)
+ else
+ timeline:tween_property('size_min_override', timeline.size_min, 0)
+ end
+end)
+mp.add_key_binding(nil, 'flash-timeline', function()
+ elements.timeline:flash()
+end)
+mp.add_key_binding(nil, 'flash-volume', function()
+ if elements.volume then elements.volume:flash() end
+end)
+mp.add_key_binding(nil, 'flash-speed', function()
+ if elements.speed then elements.speed:flash() end
+end)
+mp.add_key_binding(nil, 'menu', function()
+ if menu:is_open('menu') then
+ menu:close()
+ elseif state.context_menu_items then
+ menu:open(state.context_menu_items, function(command)
+ mp.command(command)
+ end, {type = 'menu'})
+ end
+end)
+mp.add_key_binding(nil, 'load-subtitles', function()
+ if menu:is_open('load-subtitles') then menu:close() return end
+
+ local path = mp.get_property_native('path')
+ if path and not is_protocol(path) then
+ open_file_navigation_menu(
+ serialize_path(path).dirname,
+ function(path) mp.commandv('sub-add', path) end,
+ {
+ type = 'load-subtitles',
+ allowed_types = options.subtitle_types
+ }
+ )
+ end
+end)
+mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener('Subtitles', 'sub', 'sid'))
+mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener('Audio', 'audio', 'aid'))
+mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener('Video', 'video', 'vid'))
+mp.add_key_binding(nil, 'playlist', function()
+ if menu:is_open('playlist') then menu:close() return end
+
+ function serialize_playlist()
+ local pos = mp.get_property_number('playlist-pos-1', 0)
+ local items = {}
+ local active_item
+ for index, item in ipairs(mp.get_property_native('playlist')) do
+ local is_url = item.filename:find('://')
+ items[index] = {
+ title = is_url and item.filename or serialize_path(item.filename).basename,
+ hint = tostring(index),
+ value = index
+ }
+
+ if index == pos then active_item = index end
+ end
+ return items, active_item
+ end
+
+ -- Update active index and playlist content on playlist changes
+ function handle_playlist_change()
+ if menu:is_open('playlist') then
+ local items, active_item = serialize_playlist()
+ elements.menu:set_items(items, {
+ active_item = active_item,
+ selected_item = active_item
+ })
+ end
+ end
+
+ local items, active_item = serialize_playlist()
+
+ menu:open(items, function(index)
+ mp.commandv('set', 'playlist-pos-1', tostring(index))
+ end, {
+ type = 'playlist',
+ title = 'Playlist',
+ active_item = active_item,
+ on_open = function()
+ mp.observe_property('playlist', 'native', handle_playlist_change)
+ mp.observe_property('playlist-pos-1', 'native', handle_playlist_change)
+ end,
+ on_close = function()
+ mp.unobserve_property(handle_playlist_change)
+ end,
+ })
+end)
+mp.add_key_binding(nil, 'chapters', function()
+ if menu:is_open('chapters') then menu:close() return end
+
+ local items = {}
+ local chapters = get_normalized_chapters()
+
+ for index, chapter in ipairs(chapters) do
+ items[#items + 1] = {
+ title = chapter.title or '',
+ hint = mp.format_time(chapter.time),
+ value = chapter.time
+ }
+ end
+
+ -- Select first chapter from the end with time lower
+ -- than current playing position (with 100ms leeway).
+ function get_selected_chapter_index()
+ local position = mp.get_property_native('playback-time')
+ if not position then return nil end
+ for index = #items, 1, -1 do
+ if position - 0.1 > items[index].value then return index end
+ end
+ end
+
+ -- Update selected chapter in chapter navigation menu
+ function seek_handler()
+ if menu:is_open('chapters') then
+ elements.menu:activate_index(get_selected_chapter_index())
+ end
+ end
+
+ menu:open(items, function(time)
+ mp.commandv('seek', tostring(time), 'absolute')
+ end, {
+ type = 'chapters',
+ title = 'Chapters',
+ active_item = get_selected_chapter_index(),
+ on_open = function() mp.register_event('seek', seek_handler) end,
+ on_close = function() mp.unregister_event(seek_handler) end
+ })
+end)
+mp.add_key_binding(nil, 'show-in-directory', function()
+ local path = mp.get_property_native('path')
+
+ -- Ignore URLs
+ if not path or is_protocol(path) then return end
+
+ path = normalize_path(path)
+
+ if state.os == 'windows' then
+ utils.subprocess_detached({args = {'explorer', '/select,', path}, cancellable = false})
+ elseif state.os == 'macos' then
+ utils.subprocess_detached({args = {'open', '-R', path}, cancellable = false})
+ elseif state.os == 'linux' then
+ local result = utils.subprocess({args = {'nautilus', path}, cancellable = false})
+
+ -- Fallback opens the folder with xdg-open instead
+ if result.status ~= 0 then
+ utils.subprocess({args = {'xdg-open', serialize_path(path).dirname}, cancellable = false})
+ end
+ end
+end)
+mp.add_key_binding(nil, 'open-file', function()
+ if menu:is_open('open-file') then menu:close() return end
+
+ local path = mp.get_property_native('path')
+ local directory
+ local active_file
+
+ if path == nil or is_protocol(path) then
+ local path = serialize_path(mp.command_native({'expand-path', '~/'}))
+ directory = path.path
+ active_file = nil
+ else
+ local path = serialize_path(path)
+ directory = path.dirname
+ active_file = path.path
+ end
+
+ -- Update selected file in directory navigation menu
+ function handle_file_loaded()
+ if menu:is_open('open-file') then
+ local path = normalize_path(mp.get_property_native('path'))
+ elements.menu:activate_value(path)
+ elements.menu:select_value(path)
+ end
+ end
+
+ open_file_navigation_menu(
+ directory,
+ function(path) mp.commandv('loadfile', path) end,
+ {
+ type = 'open-file',
+ allowed_types = options.media_types,
+ active_path = active_file,
+ on_open = function() mp.register_event('file-loaded', handle_file_loaded) end,
+ on_close = function() mp.unregister_event(handle_file_loaded) end,
+ }
+ )
+end)
+mp.add_key_binding(nil, 'next', function()
+ if mp.get_property_native('playlist-count') > 1 then
+ mp.command('playlist-next')
+ else
+ navigate_directory('forward')
+ end
+end)
+mp.add_key_binding(nil, 'prev', function()
+ if mp.get_property_native('playlist-count') > 1 then
+ mp.command('playlist-prev')
+ else
+ navigate_directory('backward')
+ end
+end)
+mp.add_key_binding(nil, 'next-file', function() navigate_directory('forward') end)
+mp.add_key_binding(nil, 'prev-file', function() navigate_directory('backward') end)
+mp.add_key_binding(nil, 'first', function()
+ if mp.get_property_native('playlist-count') > 1 then
+ mp.commandv('set', 'playlist-pos-1', '1')
+ else
+ load_file_in_current_directory(1)
+ end
+end)
+mp.add_key_binding(nil, 'last', function()
+ local playlist_count = mp.get_property_native('playlist-count')
+ if playlist_count > 1 then
+ mp.commandv('set', 'playlist-pos-1', tostring(playlist_count))
+ else
+ load_file_in_current_directory(-1)
+ end
+end)
+mp.add_key_binding(nil, 'first-file', function() load_file_in_current_directory(1) end)
+mp.add_key_binding(nil, 'last-file', function() load_file_in_current_directory(-1) end)
+mp.add_key_binding(nil, 'delete-file-next', function()
+ local path = mp.get_property_native('path')
+
+ if not path or is_protocol(path) then return end
+
+ path = normalize_path(path)
+ local playlist_count = mp.get_property_native('playlist-count')
+
+ if playlist_count > 1 then
+ mp.commandv('playlist-remove', 'current')
+ else
+ local next_file = get_adjacent_file(path, 'forward', options.media_types)
+
+ if menu:is_open('open-file') then
+ elements.menu:delete_value(path)
+ end
+
+ if next_file then
+ mp.commandv('loadfile', next_file)
+ else
+ mp.commandv('stop')
+ end
+ end
+
+ os.remove(path)
+end)
+mp.add_key_binding(nil, 'delete-file-quit', function()
+ local path = mp.get_property_native('path')
+ if not path or is_protocol(path) then return end
+ os.remove(normalize_path(path))
+ mp.command('quit')
+end)
+mp.add_key_binding(nil, 'open-config-directory', function()
+ local config = serialize_path(mp.command_native({'expand-path', '~~/mpv.conf'}))
+ local args
+
+ if state.os == 'windows' then
+ args = {'explorer', '/select,', config.path}
+ elseif state.os == 'macos' then
+ args = {'open', '-R', config.path}
+ elseif state.os == 'linux' then
+ args = {'xdg-open', config.dirname}
+ end
+
+ utils.subprocess_detached({args = args, cancellable = false})
+end)
diff --git a/mpv/scripts/webm.lua b/mpv/scripts/webm.lua
new file mode 100644
index 0000000..edfc164
--- /dev/null
+++ b/mpv/scripts/webm.lua
@@ -0,0 +1,2689 @@
+local mp = require("mp")
+local assdraw = require("mp.assdraw")
+local msg = require("mp.msg")
+local utils = require("mp.utils")
+local mpopts = require("mp.options")
+local options = {
+ -- Defaults to shift+w
+ keybind = "W",
+ -- If empty, saves on the same directory of the playing video.
+ -- A starting "~" will be replaced by the home dir.
+ -- This field is delimited by double-square-brackets - [[ and ]] - instead of
+ -- quotes, because Windows users might run into a issue when using
+ -- backslashes as a path separator. Examples of valid inputs for this field
+ -- would be: [[]] (the default, empty value), [[C:\Users\John]] (on Windows),
+ -- and [[/home/john]] (on Unix-like systems eg. Linux).
+ output_directory = [[]],
+ run_detached = false,
+ -- Template string for the output file
+ -- %f - Filename, with extension
+ -- %F - Filename, without extension
+ -- %T - Media title, if it exists, or filename, with extension (useful for some streams, such as YouTube).
+ -- %s, %e - Start and end time, with milliseconds
+ -- %S, %E - Start and end time, without milliseconds
+ -- %M - "-audio", if audio is enabled, empty otherwise
+ -- %R - "-(height)p", where height is the video's height, or scale_height, if it's enabled.
+ -- More specifiers are supported, see https://mpv.io/manual/master/#options-screenshot-template
+ -- Property expansion is supported (with %{} at top level, ${} when nested), see https://mpv.io/manual/master/#property-expansion
+ output_template = "%F-[%s-%e]%M",
+ -- Scale video to a certain height, keeping the aspect ratio. -1 disables it.
+ scale_height = -1,
+ -- Change the FPS of the output video, dropping or duplicating frames as needed.
+ -- -1 means the FPS will be unchanged from the source.
+ fps = -1,
+ -- Target filesize, in kB. This will be used to calculate the bitrate
+ -- used on the encode. If this is set to <= 0, the video bitrate will be set
+ -- to 0, which might enable constant quality modes, depending on the
+ -- video codec that's used (VP8 and VP9, for example).
+ target_filesize = 2500,
+ -- If true, will use stricter flags to ensure the resulting file doesn't
+ -- overshoot the target filesize. Not recommended, as constrained quality
+ -- mode should work well, unless you're really having trouble hitting
+ -- the target size.
+ strict_filesize_constraint = false,
+ strict_bitrate_multiplier = 0.95,
+ -- In kilobits.
+ strict_audio_bitrate = 64,
+ -- Sets the output format, from a few predefined ones.
+ -- Currently we have webm-vp8 (libvpx/libvorbis), webm-vp9 (libvpx-vp9/libvorbis)
+ -- and raw (rawvideo/pcm_s16le).
+ output_format = "webm-vp8",
+ twopass = false,
+ -- If set, applies the video filters currently used on the playback to the encode.
+ apply_current_filters = true,
+ -- If set, writes the video's filename to the "Title" field on the metadata.
+ write_filename_on_metadata = false,
+ -- Set the number of encoding threads, for codecs libvpx and libvpx-vp9
+ libvpx_threads = 4,
+ additional_flags = "",
+ -- Constant Rate Factor (CRF). The value meaning and limits may change,
+ -- from codec to codec. Set to -1 to disable.
+ crf = 10,
+ -- Useful for flags that may impact output filesize, such as qmin, qmax etc
+ -- Won't be applied when strict_filesize_constraint is on.
+ non_strict_additional_flags = "",
+ -- Display the encode progress, in %. Requires run_detached to be disabled.
+ -- On Windows, it shows a cmd popup. "auto" will display progress on non-Windows platforms.
+ display_progress = "auto",
+ -- The font size used in the menu. Isn't used for the notifications (started encode, finished encode etc)
+ font_size = 28,
+ margin = 10,
+ message_duration = 5
+}
+
+mpopts.read_options(options)
+local base64_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+
+-- encoding
+function base64_encode(data)
+ return ((data:gsub('.', function(x)
+ local r,b='',x:byte()
+ for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
+ return r;
+ end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
+ if (#x < 6) then return '' end
+ local c=0
+ for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
+ return base64_chars:sub(c+1,c+1)
+ end)..({ '', '==', '=' })[#data%3+1])
+end
+
+-- decoding
+function base64_decode(data)
+ data = string.gsub(data, '[^'..base64_chars..'=]', '')
+ return (data:gsub('.', function(x)
+ if (x == '=') then return '' end
+ local r,f='',(base64_chars:find(x)-1)
+ for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
+ return r;
+ end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
+ if (#x ~= 8) then return '' end
+ local c=0
+ for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
+ return string.char(c)
+ end))
+end
+local bold
+bold = function(text)
+ return "{\\b1}" .. tostring(text) .. "{\\b0}"
+end
+local message
+message = function(text, duration)
+ local ass = mp.get_property_osd("osd-ass-cc/0")
+ ass = ass .. text
+ return mp.osd_message(ass, duration or options.message_duration)
+end
+local append
+append = function(a, b)
+ for _, val in ipairs(b) do
+ a[#a + 1] = val
+ end
+ return a
+end
+local seconds_to_time_string
+seconds_to_time_string = function(seconds, no_ms, full)
+ if seconds < 0 then
+ return "unknown"
+ end
+ local ret = ""
+ if not (no_ms) then
+ ret = string.format(".%03d", seconds * 1000 % 1000)
+ end
+ ret = string.format("%02d:%02d%s", math.floor(seconds / 60) % 60, math.floor(seconds) % 60, ret)
+ if full or seconds > 3600 then
+ ret = string.format("%d:%s", math.floor(seconds / 3600), ret)
+ end
+ return ret
+end
+local seconds_to_path_element
+seconds_to_path_element = function(seconds, no_ms, full)
+ local time_string = seconds_to_time_string(seconds, no_ms, full)
+ local _
+ time_string, _ = time_string:gsub(":", ".")
+ return time_string
+end
+local file_exists
+file_exists = function(name)
+ local info, err = utils.file_info(name)
+ if info ~= nil then
+ return true
+ end
+ return false
+end
+local expand_properties
+expand_properties = function(text, magic)
+ if magic == nil then
+ magic = "$"
+ end
+ for prefix, raw, prop, colon, fallback, closing in text:gmatch("%" .. magic .. "{([?!]?)(=?)([^}:]*)(:?)([^}]*)(}*)}") do
+ local err
+ local prop_value
+ local compare_value
+ local original_prop = prop
+ local get_property = mp.get_property_osd
+ if raw == "=" then
+ get_property = mp.get_property
+ end
+ if prefix ~= "" then
+ for actual_prop, compare in prop:gmatch("(.-)==(.*)") do
+ prop = actual_prop
+ compare_value = compare
+ end
+ end
+ if colon == ":" then
+ prop_value, err = get_property(prop, fallback)
+ else
+ prop_value, err = get_property(prop, "(error)")
+ end
+ prop_value = tostring(prop_value)
+ if prefix == "?" then
+ if compare_value == nil then
+ prop_value = err == nil and fallback .. closing or ""
+ else
+ prop_value = prop_value == compare_value and fallback .. closing or ""
+ end
+ prefix = "%" .. prefix
+ elseif prefix == "!" then
+ if compare_value == nil then
+ prop_value = err ~= nil and fallback .. closing or ""
+ else
+ prop_value = prop_value ~= compare_value and fallback .. closing or ""
+ end
+ else
+ prop_value = prop_value .. closing
+ end
+ if colon == ":" then
+ local _
+ text, _ = text:gsub("%" .. magic .. "{" .. prefix .. raw .. original_prop:gsub("%W", "%%%1") .. ":" .. fallback:gsub("%W", "%%%1") .. closing .. "}", expand_properties(prop_value))
+ else
+ local _
+ text, _ = text:gsub("%" .. magic .. "{" .. prefix .. raw .. original_prop:gsub("%W", "%%%1") .. closing .. "}", prop_value)
+ end
+ end
+ return text
+end
+local format_filename
+format_filename = function(startTime, endTime, videoFormat)
+ local hasAudioCodec = videoFormat.audioCodec ~= ""
+ local replaceFirst = {
+ ["%%mp"] = "%%mH.%%mM.%%mS",
+ ["%%mP"] = "%%mH.%%mM.%%mS.%%mT",
+ ["%%p"] = "%%wH.%%wM.%%wS",
+ ["%%P"] = "%%wH.%%wM.%%wS.%%wT"
+ }
+ local replaceTable = {
+ ["%%wH"] = string.format("%02d", math.floor(startTime / (60 * 60))),
+ ["%%wh"] = string.format("%d", math.floor(startTime / (60 * 60))),
+ ["%%wM"] = string.format("%02d", math.floor(startTime / 60 % 60)),
+ ["%%wm"] = string.format("%d", math.floor(startTime / 60)),
+ ["%%wS"] = string.format("%02d", math.floor(startTime % 60)),
+ ["%%ws"] = string.format("%d", math.floor(startTime)),
+ ["%%wf"] = string.format("%s", startTime),
+ ["%%wT"] = string.sub(string.format("%.3f", startTime % 1), 3),
+ ["%%mH"] = string.format("%02d", math.floor(endTime / (60 * 60))),
+ ["%%mh"] = string.format("%d", math.floor(endTime / (60 * 60))),
+ ["%%mM"] = string.format("%02d", math.floor(endTime / 60 % 60)),
+ ["%%mm"] = string.format("%d", math.floor(endTime / 60)),
+ ["%%mS"] = string.format("%02d", math.floor(endTime % 60)),
+ ["%%ms"] = string.format("%d", math.floor(endTime)),
+ ["%%mf"] = string.format("%s", endTime),
+ ["%%mT"] = string.sub(string.format("%.3f", endTime % 1), 3),
+ ["%%f"] = mp.get_property("filename"),
+ ["%%F"] = mp.get_property("filename/no-ext"),
+ ["%%s"] = seconds_to_path_element(startTime),
+ ["%%S"] = seconds_to_path_element(startTime, true),
+ ["%%e"] = seconds_to_path_element(endTime),
+ ["%%E"] = seconds_to_path_element(endTime, true),
+ ["%%T"] = mp.get_property("media-title"),
+ ["%%M"] = (mp.get_property_native('aid') and not mp.get_property_native('mute') and hasAudioCodec) and '-audio' or '',
+ ["%%R"] = (options.scale_height ~= -1) and "-" .. tostring(options.scale_height) .. "p" or "-" .. tostring(mp.get_property_native('height')) .. "p",
+ ["%%t%%"] = "%%"
+ }
+ local filename = options.output_template
+ for format, value in pairs(replaceFirst) do
+ local _
+ filename, _ = filename:gsub(format, value)
+ end
+ for format, value in pairs(replaceTable) do
+ local _
+ filename, _ = filename:gsub(format, value)
+ end
+ if mp.get_property_bool("demuxer-via-network", false) then
+ local _
+ filename, _ = filename:gsub("%%X{([^}]*)}", "%1")
+ filename, _ = filename:gsub("%%x", "")
+ else
+ local x = string.gsub(mp.get_property("stream-open-filename", ""), string.gsub(mp.get_property("filename", ""), "%W", "%%%1") .. "$", "")
+ local _
+ filename, _ = filename:gsub("%%X{[^}]*}", x)
+ filename, _ = filename:gsub("%%x", x)
+ end
+ filename = expand_properties(filename, "%")
+ for format in filename:gmatch("%%t([aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ])") do
+ local _
+ filename, _ = filename:gsub("%%t" .. format, os.date("%" .. format))
+ end
+ local _
+ filename, _ = filename:gsub("[<>:\"/\\|?*]", "")
+ return tostring(filename) .. "." .. tostring(videoFormat.outputExtension)
+end
+local parse_directory
+parse_directory = function(dir)
+ local home_dir = os.getenv("HOME")
+ if not home_dir then
+ home_dir = os.getenv("USERPROFILE")
+ end
+ if not home_dir then
+ local drive = os.getenv("HOMEDRIVE")
+ local path = os.getenv("HOMEPATH")
+ if drive and path then
+ home_dir = utils.join_path(drive, path)
+ else
+ msg.warn("Couldn't find home dir.")
+ home_dir = ""
+ end
+ end
+ local _
+ dir, _ = dir:gsub("^~", home_dir)
+ return dir
+end
+local is_windows = type(package) == "table" and type(package.config) == "string" and package.config:sub(1, 1) == "\\"
+local trim
+trim = function(s)
+ return s:match("^%s*(.-)%s*$")
+end
+local get_null_path
+get_null_path = function()
+ if file_exists("/dev/null") then
+ return "/dev/null"
+ end
+ return "NUL"
+end
+local run_subprocess
+run_subprocess = function(params)
+ local res = utils.subprocess(params)
+ msg.verbose("Command stdout: ")
+ msg.verbose(res.stdout)
+ if res.status ~= 0 then
+ msg.verbose("Command failed! Reason: ", res.error, " Killed by us? ", res.killed_by_us and "yes" or "no")
+ return false
+ end
+ return true
+end
+local shell_escape
+shell_escape = function(args)
+ local ret = { }
+ for i, a in ipairs(args) do
+ local s = tostring(a)
+ if string.match(s, "[^A-Za-z0-9_/:=-]") then
+ if is_windows then
+ s = '"' .. string.gsub(s, '"', '"\\""') .. '"'
+ else
+ s = "'" .. string.gsub(s, "'", "'\\''") .. "'"
+ end
+ end
+ table.insert(ret, s)
+ end
+ local concat = table.concat(ret, " ")
+ if is_windows then
+ concat = '"' .. concat .. '"'
+ end
+ return concat
+end
+local run_subprocess_popen
+run_subprocess_popen = function(command_line)
+ local command_line_string = shell_escape(command_line)
+ command_line_string = command_line_string .. " 2>&1"
+ msg.verbose("run_subprocess_popen: running " .. tostring(command_line_string))
+ return io.popen(command_line_string)
+end
+local calculate_scale_factor
+calculate_scale_factor = function()
+ local baseResY = 720
+ local osd_w, osd_h = mp.get_osd_size()
+ return osd_h / baseResY
+end
+local should_display_progress
+should_display_progress = function()
+ if options.display_progress == "auto" then
+ return not is_windows
+ end
+ return options.display_progress
+end
+local reverse
+reverse = function(list)
+ local _accum_0 = { }
+ local _len_0 = 1
+ local _max_0 = 1
+ for _index_0 = #list, _max_0 < 0 and #list + _max_0 or _max_0, -1 do
+ local element = list[_index_0]
+ _accum_0[_len_0] = element
+ _len_0 = _len_0 + 1
+ end
+ return _accum_0
+end
+local get_pass_logfile_path
+get_pass_logfile_path = function(encode_out_path)
+ return tostring(encode_out_path) .. "-video-pass1.log"
+end
+local dimensions_changed = true
+local _video_dimensions = { }
+local get_video_dimensions
+get_video_dimensions = function()
+ if not (dimensions_changed) then
+ return _video_dimensions
+ end
+ local video_params = mp.get_property_native("video-out-params")
+ if not video_params then
+ return nil
+ end
+ dimensions_changed = false
+ local keep_aspect = mp.get_property_bool("keepaspect")
+ local w = video_params["w"]
+ local h = video_params["h"]
+ local dw = video_params["dw"]
+ local dh = video_params["dh"]
+ if mp.get_property_number("video-rotate") % 180 == 90 then
+ w, h = h, w
+ dw, dh = dh, dw
+ end
+ _video_dimensions = {
+ top_left = { },
+ bottom_right = { },
+ ratios = { }
+ }
+ local window_w, window_h = mp.get_osd_size()
+ if keep_aspect then
+ local unscaled = mp.get_property_native("video-unscaled")
+ local panscan = mp.get_property_number("panscan")
+ local fwidth = window_w
+ local fheight = math.floor(window_w / dw * dh)
+ if fheight > window_h or fheight < h then
+ local tmpw = math.floor(window_h / dh * dw)
+ if tmpw <= window_w then
+ fheight = window_h
+ fwidth = tmpw
+ end
+ end
+ local vo_panscan_area = window_h - fheight
+ local f_w = fwidth / fheight
+ local f_h = 1
+ if vo_panscan_area == 0 then
+ vo_panscan_area = window_h - fwidth
+ f_w = 1
+ f_h = fheight / fwidth
+ end
+ if unscaled or unscaled == "downscale-big" then
+ vo_panscan_area = 0
+ if unscaled or (dw <= window_w and dh <= window_h) then
+ fwidth = dw
+ fheight = dh
+ end
+ end
+ local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w)
+ local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h)
+ local split_scaling
+ split_scaling = function(dst_size, scaled_src_size, zoom, align, pan)
+ scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom)
+ align = (align + 1) / 2
+ local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size)
+ if dst_start < 0 then
+ dst_start = dst_start + 1
+ end
+ local dst_end = dst_start + scaled_src_size
+ if dst_start >= dst_end then
+ dst_start = 0
+ dst_end = 1
+ end
+ return dst_start, dst_end
+ end
+ local zoom = mp.get_property_number("video-zoom")
+ local align_x = mp.get_property_number("video-align-x")
+ local pan_x = mp.get_property_number("video-pan-x")
+ _video_dimensions.top_left.x, _video_dimensions.bottom_right.x = split_scaling(window_w, scaled_width, zoom, align_x, pan_x)
+ local align_y = mp.get_property_number("video-align-y")
+ local pan_y = mp.get_property_number("video-pan-y")
+ _video_dimensions.top_left.y, _video_dimensions.bottom_right.y = split_scaling(window_h, scaled_height, zoom, align_y, pan_y)
+ else
+ _video_dimensions.top_left.x = 0
+ _video_dimensions.bottom_right.x = window_w
+ _video_dimensions.top_left.y = 0
+ _video_dimensions.bottom_right.y = window_h
+ end
+ _video_dimensions.ratios.w = w / (_video_dimensions.bottom_right.x - _video_dimensions.top_left.x)
+ _video_dimensions.ratios.h = h / (_video_dimensions.bottom_right.y - _video_dimensions.top_left.y)
+ return _video_dimensions
+end
+local set_dimensions_changed
+set_dimensions_changed = function()
+ dimensions_changed = true
+end
+local monitor_dimensions
+monitor_dimensions = function()
+ local properties = {
+ "keepaspect",
+ "video-out-params",
+ "video-unscaled",
+ "panscan",
+ "video-zoom",
+ "video-align-x",
+ "video-pan-x",
+ "video-align-y",
+ "video-pan-y",
+ "osd-width",
+ "osd-height"
+ }
+ for _, p in ipairs(properties) do
+ mp.observe_property(p, "native", set_dimensions_changed)
+ end
+end
+local clamp
+clamp = function(min, val, max)
+ if val <= min then
+ return min
+ end
+ if val >= max then
+ return max
+ end
+ return val
+end
+local clamp_point
+clamp_point = function(top_left, point, bottom_right)
+ return {
+ x = clamp(top_left.x, point.x, bottom_right.x),
+ y = clamp(top_left.y, point.y, bottom_right.y)
+ }
+end
+local VideoPoint
+do
+ local _class_0
+ local _base_0 = {
+ set_from_screen = function(self, sx, sy)
+ local d = get_video_dimensions()
+ local point = clamp_point(d.top_left, {
+ x = sx,
+ y = sy
+ }, d.bottom_right)
+ self.x = math.floor(d.ratios.w * (point.x - d.top_left.x) + 0.5)
+ self.y = math.floor(d.ratios.h * (point.y - d.top_left.y) + 0.5)
+ end,
+ to_screen = function(self)
+ local d = get_video_dimensions()
+ return {
+ x = math.floor(self.x / d.ratios.w + d.top_left.x + 0.5),
+ y = math.floor(self.y / d.ratios.h + d.top_left.y + 0.5)
+ }
+ end
+ }
+ _base_0.__index = _base_0
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.x = -1
+ self.y = -1
+ end,
+ __base = _base_0,
+ __name = "VideoPoint"
+ }, {
+ __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
+ VideoPoint = _class_0
+end
+local Region
+do
+ local _class_0
+ local _base_0 = {
+ is_valid = function(self)
+ return self.x > -1 and self.y > -1 and self.w > -1 and self.h > -1
+ end,
+ set_from_points = function(self, p1, p2)
+ self.x = math.min(p1.x, p2.x)
+ self.y = math.min(p1.y, p2.y)
+ self.w = math.abs(p1.x - p2.x)
+ self.h = math.abs(p1.y - p2.y)
+ end
+ }
+ _base_0.__index = _base_0
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.x = -1
+ self.y = -1
+ self.w = -1
+ self.h = -1
+ end,
+ __base = _base_0,
+ __name = "Region"
+ }, {
+ __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
+ Region = _class_0
+end
+local make_fullscreen_region
+make_fullscreen_region = function()
+ local r = Region()
+ local d = get_video_dimensions()
+ local a = VideoPoint()
+ local b = VideoPoint()
+ local xa, ya
+ do
+ local _obj_0 = d.top_left
+ xa, ya = _obj_0.x, _obj_0.y
+ end
+ a:set_from_screen(xa, ya)
+ local xb, yb
+ do
+ local _obj_0 = d.bottom_right
+ xb, yb = _obj_0.x, _obj_0.y
+ end
+ b:set_from_screen(xb, yb)
+ r:set_from_points(a, b)
+ return r
+end
+local read_double
+read_double = function(bytes)
+ local sign = 1
+ local mantissa = bytes[2] % 2 ^ 4
+ for i = 3, 8 do
+ mantissa = mantissa * 256 + bytes[i]
+ end
+ if bytes[1] > 127 then
+ sign = -1
+ end
+ local exponent = (bytes[1] % 128) * 2 ^ 4 + math.floor(bytes[2] / 2 ^ 4)
+ if exponent == 0 then
+ return 0
+ end
+ mantissa = (math.ldexp(mantissa, -52) + 1) * sign
+ return math.ldexp(mantissa, exponent - 1023)
+end
+local write_double
+write_double = function(num)
+ local bytes = {
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ }
+ if num == 0 then
+ return bytes
+ end
+ local anum = math.abs(num)
+ local mantissa, exponent = math.frexp(anum)
+ exponent = exponent - 1
+ mantissa = mantissa * 2 - 1
+ local sign = num ~= anum and 128 or 0
+ exponent = exponent + 1023
+ bytes[1] = sign + math.floor(exponent / 2 ^ 4)
+ mantissa = mantissa * 2 ^ 4
+ local currentmantissa = math.floor(mantissa)
+ mantissa = mantissa - currentmantissa
+ bytes[2] = (exponent % 2 ^ 4) * 2 ^ 4 + currentmantissa
+ for i = 3, 8 do
+ mantissa = mantissa * 2 ^ 8
+ currentmantissa = math.floor(mantissa)
+ mantissa = mantissa - currentmantissa
+ bytes[i] = currentmantissa
+ end
+ return bytes
+end
+local FirstpassStats
+do
+ local _class_0
+ local duration_multiplier, fields_before_duration, fields_after_duration
+ local _base_0 = {
+ get_duration = function(self)
+ local big_endian_binary_duration = reverse(self.binary_duration)
+ return read_double(reversed_binary_duration) / duration_multiplier
+ end,
+ set_duration = function(self, duration)
+ local big_endian_binary_duration = write_double(duration * duration_multiplier)
+ self.binary_duration = reverse(big_endian_binary_duration)
+ end,
+ _bytes_to_string = function(self, bytes)
+ return string.char(unpack(bytes))
+ end,
+ as_binary_string = function(self)
+ local before_duration_string = self:_bytes_to_string(self.binary_data_before_duration)
+ local duration_string = self:_bytes_to_string(self.binary_duration)
+ local after_duration_string = self:_bytes_to_string(self.binary_data_after_duration)
+ return before_duration_string .. duration_string .. after_duration_string
+ end
+ }
+ _base_0.__index = _base_0
+ _class_0 = setmetatable({
+ __init = function(self, before_duration, duration, after_duration)
+ self.binary_data_before_duration = before_duration
+ self.binary_duration = duration
+ self.binary_data_after_duration = after_duration
+ end,
+ __base = _base_0,
+ __name = "FirstpassStats"
+ }, {
+ __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
+ duration_multiplier = 10000000.0
+ fields_before_duration = 16
+ fields_after_duration = 1
+ self.data_before_duration_size = function(self)
+ return fields_before_duration * 8
+ end
+ self.data_after_duration_size = function(self)
+ return fields_after_duration * 8
+ end
+ self.size = function(self)
+ return (fields_before_duration + 1 + fields_after_duration) * 8
+ end
+ self.from_bytes = function(self, bytes)
+ local before_duration
+ do
+ local _accum_0 = { }
+ local _len_0 = 1
+ local _max_0 = self:data_before_duration_size()
+ for _index_0 = 1, _max_0 < 0 and #bytes + _max_0 or _max_0 do
+ local b = bytes[_index_0]
+ _accum_0[_len_0] = b
+ _len_0 = _len_0 + 1
+ end
+ before_duration = _accum_0
+ end
+ local duration
+ do
+ local _accum_0 = { }
+ local _len_0 = 1
+ local _max_0 = self:data_before_duration_size() + 8
+ for _index_0 = self:data_before_duration_size() + 1, _max_0 < 0 and #bytes + _max_0 or _max_0 do
+ local b = bytes[_index_0]
+ _accum_0[_len_0] = b
+ _len_0 = _len_0 + 1
+ end
+ duration = _accum_0
+ end
+ local after_duration
+ do
+ local _accum_0 = { }
+ local _len_0 = 1
+ for _index_0 = self:data_before_duration_size() + 8 + 1, #bytes do
+ local b = bytes[_index_0]
+ _accum_0[_len_0] = b
+ _len_0 = _len_0 + 1
+ end
+ after_duration = _accum_0
+ end
+ return self(before_duration, duration, after_duration)
+ end
+ FirstpassStats = _class_0
+end
+local read_logfile_into_stats_array
+read_logfile_into_stats_array = function(logfile_path)
+ local file = assert(io.open(logfile_path, "rb"))
+ local logfile_string = base64_decode(file:read())
+ file:close()
+ local stats_size = FirstpassStats:size()
+ assert(logfile_string:len() % stats_size == 0)
+ local stats = { }
+ for offset = 1, #logfile_string, stats_size do
+ local bytes = {
+ logfile_string:byte(offset, offset + stats_size - 1)
+ }
+ assert(#bytes == stats_size)
+ stats[#stats + 1] = FirstpassStats:from_bytes(bytes)
+ end
+ return stats
+end
+local write_stats_array_to_logfile
+write_stats_array_to_logfile = function(stats_array, logfile_path)
+ local file = assert(io.open(logfile_path, "wb"))
+ local logfile_string = ""
+ for _index_0 = 1, #stats_array do
+ local stat = stats_array[_index_0]
+ logfile_string = logfile_string .. stat:as_binary_string()
+ end
+ file:write(base64_encode(logfile_string))
+ return file:close()
+end
+local vp8_patch_logfile
+vp8_patch_logfile = function(logfile_path, encode_total_duration)
+ local stats_array = read_logfile_into_stats_array(logfile_path)
+ local average_duration = encode_total_duration / (#stats_array - 1)
+ for i = 1, #stats_array - 1 do
+ stats_array[i]:set_duration(average_duration)
+ end
+ stats_array[#stats_array]:set_duration(encode_total_duration)
+ return write_stats_array_to_logfile(stats_array, logfile_path)
+end
+local formats = { }
+local Format
+do
+ local _class_0
+ local _base_0 = {
+ getPreFilters = function(self)
+ return { }
+ end,
+ getPostFilters = function(self)
+ return { }
+ end,
+ getFlags = function(self)
+ return { }
+ end,
+ getCodecFlags = function(self)
+ local codecs = { }
+ if self.videoCodec ~= "" then
+ codecs[#codecs + 1] = "--ovc=" .. tostring(self.videoCodec)
+ end
+ if self.audioCodec ~= "" then
+ codecs[#codecs + 1] = "--oac=" .. tostring(self.audioCodec)
+ end
+ return codecs
+ end
+ }
+ _base_0.__index = _base_0
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "Basic"
+ self.supportsTwopass = true
+ self.videoCodec = ""
+ self.audioCodec = ""
+ self.outputExtension = ""
+ self.acceptsBitrate = true
+ end,
+ __base = _base_0,
+ __name = "Format"
+ }, {
+ __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
+ Format = _class_0
+end
+local RawVideo
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = {
+ getColorspace = function(self)
+ local csp = mp.get_property("colormatrix")
+ local _exp_0 = csp
+ if "bt.601" == _exp_0 then
+ return "bt601"
+ elseif "bt.709" == _exp_0 then
+ return "bt709"
+ elseif "bt.2020" == _exp_0 then
+ return "bt2020"
+ elseif "smpte-240m" == _exp_0 then
+ return "smpte240m"
+ else
+ msg.info("Warning, unknown colorspace " .. tostring(csp) .. " detected, using bt.601.")
+ return "bt601"
+ end
+ end,
+ getPostFilters = function(self)
+ return {
+ "format=yuv444p16",
+ "lavfi-scale=in_color_matrix=" .. self:getColorspace(),
+ "format=bgr24"
+ }
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "Raw"
+ self.supportsTwopass = false
+ self.videoCodec = "rawvideo"
+ self.audioCodec = "pcm_s16le"
+ self.outputExtension = "avi"
+ self.acceptsBitrate = false
+ end,
+ __base = _base_0,
+ __name = "RawVideo",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ RawVideo = _class_0
+end
+formats["raw"] = RawVideo()
+local WebmVP8
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = {
+ getPreFilters = function(self)
+ local colormatrixFilter = {
+ ["bt.709"] = "bt709",
+ ["bt.2020"] = "bt2020",
+ ["smpte-240m"] = "smpte240m"
+ }
+ local ret = { }
+ local colormatrix = mp.get_property_native("video-params/colormatrix")
+ if colormatrixFilter[colormatrix] then
+ append(ret, {
+ "lavfi-colormatrix=" .. tostring(colormatrixFilter[colormatrix]) .. ":bt601"
+ })
+ end
+ return ret
+ end,
+ getFlags = function(self)
+ return {
+ "--ovcopts-add=threads=" .. tostring(options.libvpx_threads)
+ }
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "WebM"
+ self.supportsTwopass = true
+ self.videoCodec = "libvpx"
+ self.audioCodec = "libvorbis"
+ self.outputExtension = "webm"
+ self.acceptsBitrate = true
+ end,
+ __base = _base_0,
+ __name = "WebmVP8",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ WebmVP8 = _class_0
+end
+formats["webm-vp8"] = WebmVP8()
+local WebmVP9
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = {
+ getFlags = function(self)
+ return {
+ "--ovcopts-add=threads=" .. tostring(options.libvpx_threads)
+ }
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "WebM (VP9)"
+ self.supportsTwopass = true
+ self.videoCodec = "libvpx-vp9"
+ self.audioCodec = "libvorbis"
+ self.outputExtension = "webm"
+ self.acceptsBitrate = true
+ end,
+ __base = _base_0,
+ __name = "WebmVP9",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ WebmVP9 = _class_0
+end
+formats["webm-vp9"] = WebmVP9()
+local MP4
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = { }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "MP4 (h264/AAC)"
+ self.supportsTwopass = true
+ self.videoCodec = "libx264"
+ self.audioCodec = "aac"
+ self.outputExtension = "mp4"
+ self.acceptsBitrate = true
+ end,
+ __base = _base_0,
+ __name = "MP4",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ MP4 = _class_0
+end
+formats["mp4"] = MP4()
+local MP4NVENC
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = { }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "MP4 (h264-NVENC/AAC)"
+ self.supportsTwopass = true
+ self.videoCodec = "h264_nvenc"
+ self.audioCodec = "aac"
+ self.outputExtension = "mp4"
+ self.acceptsBitrate = true
+ end,
+ __base = _base_0,
+ __name = "MP4NVENC",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ MP4NVENC = _class_0
+end
+formats["mp4-nvenc"] = MP4NVENC()
+local MP3
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = { }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "MP3 (libmp3lame)"
+ self.supportsTwopass = false
+ self.videoCodec = ""
+ self.audioCodec = "libmp3lame"
+ self.outputExtension = "mp3"
+ self.acceptsBitrate = true
+ end,
+ __base = _base_0,
+ __name = "MP3",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ MP3 = _class_0
+end
+formats["mp3"] = MP3()
+local GIF
+do
+ local _class_0
+ local _parent_0 = Format
+ local _base_0 = { }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.displayName = "GIF"
+ self.supportsTwopass = false
+ self.videoCodec = "gif"
+ self.audioCodec = ""
+ self.outputExtension = "gif"
+ self.acceptsBitrate = false
+ end,
+ __base = _base_0,
+ __name = "GIF",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ GIF = _class_0
+end
+formats["gif"] = GIF()
+local Page
+do
+ local _class_0
+ local _base_0 = {
+ add_keybinds = function(self)
+ if not self.keybinds then
+ return
+ end
+ for key, func in pairs(self.keybinds) do
+ mp.add_forced_key_binding(key, key, func, {
+ repeatable = true
+ })
+ end
+ end,
+ remove_keybinds = function(self)
+ if not self.keybinds then
+ return
+ end
+ for key, _ in pairs(self.keybinds) do
+ mp.remove_key_binding(key)
+ end
+ end,
+ observe_properties = function(self)
+ self.sizeCallback = function()
+ return self:draw()
+ end
+ local properties = {
+ "keepaspect",
+ "video-out-params",
+ "video-unscaled",
+ "panscan",
+ "video-zoom",
+ "video-align-x",
+ "video-pan-x",
+ "video-align-y",
+ "video-pan-y",
+ "osd-width",
+ "osd-height"
+ }
+ for _index_0 = 1, #properties do
+ local p = properties[_index_0]
+ mp.observe_property(p, "native", self.sizeCallback)
+ end
+ end,
+ unobserve_properties = function(self)
+ if self.sizeCallback then
+ mp.unobserve_property(self.sizeCallback)
+ self.sizeCallback = nil
+ end
+ end,
+ clear = function(self)
+ local window_w, window_h = mp.get_osd_size()
+ mp.set_osd_ass(window_w, window_h, "")
+ return mp.osd_message("", 0)
+ end,
+ prepare = function(self)
+ return nil
+ end,
+ dispose = function(self)
+ return nil
+ end,
+ show = function(self)
+ if self.visible then
+ return
+ end
+ self.visible = true
+ self:observe_properties()
+ self:add_keybinds()
+ self:prepare()
+ self:clear()
+ return self:draw()
+ end,
+ hide = function(self)
+ if not self.visible then
+ return
+ end
+ self.visible = false
+ self:unobserve_properties()
+ self:remove_keybinds()
+ self:clear()
+ return self:dispose()
+ end,
+ setup_text = function(self, ass)
+ local scale = calculate_scale_factor()
+ local margin = options.margin * scale
+ ass:append("{\\an7}")
+ ass:pos(margin, margin)
+ return ass:append("{\\fs" .. tostring(options.font_size * scale) .. "}")
+ end
+ }
+ _base_0.__index = _base_0
+ _class_0 = setmetatable({
+ __init = function() end,
+ __base = _base_0,
+ __name = "Page"
+ }, {
+ __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
+ Page = _class_0
+end
+local EncodeWithProgress
+do
+ local _class_0
+ local _parent_0 = Page
+ local _base_0 = {
+ draw = function(self)
+ local progress = 100 * ((self.currentTime - self.startTime) / self.duration)
+ local progressText = string.format("%d%%", progress)
+ local window_w, window_h = mp.get_osd_size()
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ self:setup_text(ass)
+ ass:append("Encoding (" .. tostring(bold(progressText)) .. ")\\N")
+ return mp.set_osd_ass(window_w, window_h, ass.text)
+ end,
+ parseLine = function(self, line)
+ local matchTime = string.match(line, "Encode time[-]pos: ([0-9.]+)")
+ local matchExit = string.match(line, "Exiting... [(]([%a ]+)[)]")
+ if matchTime == nil and matchExit == nil then
+ return
+ end
+ if matchTime ~= nil and tonumber(matchTime) > self.currentTime then
+ self.currentTime = tonumber(matchTime)
+ end
+ if matchExit ~= nil then
+ self.finished = true
+ self.finishedReason = matchExit
+ end
+ end,
+ startEncode = function(self, command_line)
+ local copy_command_line
+ do
+ local _accum_0 = { }
+ local _len_0 = 1
+ for _index_0 = 1, #command_line do
+ local arg = command_line[_index_0]
+ _accum_0[_len_0] = arg
+ _len_0 = _len_0 + 1
+ end
+ copy_command_line = _accum_0
+ end
+ append(copy_command_line, {
+ '--term-status-msg=Encode time-pos: ${=time-pos}\\n'
+ })
+ self:show()
+ local processFd = run_subprocess_popen(copy_command_line)
+ for line in processFd:lines() do
+ msg.verbose(string.format('%q', line))
+ self:parseLine(line)
+ self:draw()
+ end
+ processFd:close()
+ self:hide()
+ if self.finishedReason == "End of file" then
+ return true
+ end
+ return false
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self, startTime, endTime)
+ self.startTime = startTime
+ self.endTime = endTime
+ self.duration = endTime - startTime
+ self.currentTime = startTime
+ end,
+ __base = _base_0,
+ __name = "EncodeWithProgress",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ EncodeWithProgress = _class_0
+end
+local get_active_tracks
+get_active_tracks = function()
+ local accepted = {
+ video = true,
+ audio = not mp.get_property_bool("mute"),
+ sub = mp.get_property_bool("sub-visibility")
+ }
+ local active = {
+ video = { },
+ audio = { },
+ sub = { }
+ }
+ for _, track in ipairs(mp.get_property_native("track-list")) do
+ if track["selected"] and accepted[track["type"]] then
+ local count = #active[track["type"]]
+ active[track["type"]][count + 1] = track
+ end
+ end
+ return active
+end
+local filter_tracks_supported_by_format
+filter_tracks_supported_by_format = function(active_tracks, format)
+ local has_video_codec = format.videoCodec ~= ""
+ local has_audio_codec = format.audioCodec ~= ""
+ local supported = {
+ video = has_video_codec and active_tracks["video"] or { },
+ audio = has_audio_codec and active_tracks["audio"] or { },
+ sub = has_video_codec and active_tracks["sub"] or { }
+ }
+ return supported
+end
+local append_track
+append_track = function(out, track)
+ local external_flag = {
+ ["audio"] = "audio-file",
+ ["sub"] = "sub-file"
+ }
+ local internal_flag = {
+ ["video"] = "vid",
+ ["audio"] = "aid",
+ ["sub"] = "sid"
+ }
+ if track['external'] and string.len(track['external-filename']) <= 2048 then
+ return append(out, {
+ "--" .. tostring(external_flag[track['type']]) .. "=" .. tostring(track['external-filename'])
+ })
+ else
+ return append(out, {
+ "--" .. tostring(internal_flag[track['type']]) .. "=" .. tostring(track['id'])
+ })
+ end
+end
+local append_audio_tracks
+append_audio_tracks = function(out, tracks)
+ local internal_tracks = { }
+ for _index_0 = 1, #tracks do
+ local track = tracks[_index_0]
+ if track['external'] then
+ append_track(out, track)
+ else
+ append(internal_tracks, {
+ track
+ })
+ end
+ end
+ if #internal_tracks > 1 then
+ local filter_string = ""
+ for _index_0 = 1, #internal_tracks do
+ local track = internal_tracks[_index_0]
+ filter_string = filter_string .. "[aid" .. tostring(track['id']) .. "]"
+ end
+ filter_string = filter_string .. "amix[ao]"
+ return append(out, {
+ "--lavfi-complex=" .. tostring(filter_string)
+ })
+ else
+ if #internal_tracks == 1 then
+ return append_track(out, internal_tracks[1])
+ end
+ end
+end
+local get_scale_filters
+get_scale_filters = function()
+ if options.scale_height > 0 then
+ return {
+ "lavfi-scale=-2:" .. tostring(options.scale_height)
+ }
+ end
+ return { }
+end
+local get_fps_filters
+get_fps_filters = function()
+ if options.fps > 0 then
+ return {
+ "fps=" .. tostring(options.fps)
+ }
+ end
+ return { }
+end
+local append_property
+append_property = function(out, property_name, option_name)
+ option_name = option_name or property_name
+ local prop = mp.get_property(property_name)
+ if prop and prop ~= "" then
+ return append(out, {
+ "--" .. tostring(option_name) .. "=" .. tostring(prop)
+ })
+ end
+end
+local append_list_options
+append_list_options = function(out, property_name, option_prefix)
+ option_prefix = option_prefix or property_name
+ local prop = mp.get_property_native(property_name)
+ if prop then
+ for _index_0 = 1, #prop do
+ local value = prop[_index_0]
+ append(out, {
+ "--" .. tostring(option_prefix) .. "-append=" .. tostring(value)
+ })
+ end
+ end
+end
+local get_playback_options
+get_playback_options = function()
+ local ret = { }
+ append_property(ret, "sub-ass-override")
+ append_property(ret, "sub-ass-force-style")
+ append_property(ret, "sub-ass-vsfilter-aspect-compat")
+ append_property(ret, "sub-auto")
+ append_property(ret, "sub-delay")
+ append_property(ret, "video-rotate")
+ append_property(ret, "ytdl-format")
+ return ret
+end
+local get_speed_flags
+get_speed_flags = function()
+ local ret = { }
+ local speed = mp.get_property_native("speed")
+ if speed ~= 1 then
+ append(ret, {
+ "--vf-add=setpts=PTS/" .. tostring(speed),
+ "--af-add=atempo=" .. tostring(speed),
+ "--sub-speed=1/" .. tostring(speed)
+ })
+ end
+ return ret
+end
+local get_metadata_flags
+get_metadata_flags = function()
+ local title = mp.get_property("filename/no-ext")
+ return {
+ "--oset-metadata=title=%" .. tostring(string.len(title)) .. "%" .. tostring(title)
+ }
+end
+local apply_current_filters
+apply_current_filters = function(filters)
+ local vf = mp.get_property_native("vf")
+ msg.verbose("apply_current_filters: got " .. tostring(#vf) .. " currently applied.")
+ for _index_0 = 1, #vf do
+ local _continue_0 = false
+ repeat
+ local filter = vf[_index_0]
+ msg.verbose("apply_current_filters: filter name: " .. tostring(filter['name']))
+ if filter["enabled"] == false then
+ _continue_0 = true
+ break
+ end
+ local str = filter["name"]
+ local params = filter["params"] or { }
+ for k, v in pairs(params) do
+ str = str .. ":" .. tostring(k) .. "=%" .. tostring(string.len(v)) .. "%" .. tostring(v)
+ end
+ append(filters, {
+ str
+ })
+ _continue_0 = true
+ until true
+ if not _continue_0 then
+ break
+ end
+ end
+end
+local get_video_filters
+get_video_filters = function(format, region)
+ local filters = { }
+ append(filters, format:getPreFilters())
+ if options.apply_current_filters then
+ apply_current_filters(filters)
+ end
+ if region and region:is_valid() then
+ append(filters, {
+ "lavfi-crop=" .. tostring(region.w) .. ":" .. tostring(region.h) .. ":" .. tostring(region.x) .. ":" .. tostring(region.y)
+ })
+ end
+ append(filters, get_scale_filters())
+ append(filters, get_fps_filters())
+ append(filters, format:getPostFilters())
+ return filters
+end
+local get_video_encode_flags
+get_video_encode_flags = function(format, region)
+ local flags = { }
+ append(flags, get_playback_options())
+ local filters = get_video_filters(format, region)
+ for _index_0 = 1, #filters do
+ local f = filters[_index_0]
+ append(flags, {
+ "--vf-add=" .. tostring(f)
+ })
+ end
+ append(flags, get_speed_flags())
+ return flags
+end
+local calculate_bitrate
+calculate_bitrate = function(active_tracks, format, length)
+ if format.videoCodec == "" then
+ return nil, options.target_filesize * 8 / length
+ end
+ local video_kilobits = options.target_filesize * 8
+ local audio_kilobits = nil
+ local has_audio_track = #active_tracks["audio"] > 0
+ if options.strict_filesize_constraint and has_audio_track then
+ audio_kilobits = length * options.strict_audio_bitrate
+ video_kilobits = video_kilobits - audio_kilobits
+ end
+ local video_bitrate = math.floor(video_kilobits / length)
+ local audio_bitrate = audio_kilobits and math.floor(audio_kilobits / length) or nil
+ return video_bitrate, audio_bitrate
+end
+local encode
+encode = function(region, startTime, endTime)
+ local format = formats[options.output_format]
+ local path = mp.get_property("path")
+ if not path then
+ message("No file is being played")
+ return
+ end
+ local is_stream = not file_exists(path)
+ local command = {
+ "mpv",
+ path,
+ "--start=" .. seconds_to_time_string(startTime, false, true),
+ "--end=" .. seconds_to_time_string(endTime, false, true),
+ "--loop-file=no"
+ }
+ append(command, format:getCodecFlags())
+ local active_tracks = get_active_tracks()
+ local supported_active_tracks = filter_tracks_supported_by_format(active_tracks, format)
+ for track_type, tracks in pairs(supported_active_tracks) do
+ if track_type == "audio" then
+ append_audio_tracks(command, tracks)
+ else
+ for _index_0 = 1, #tracks do
+ local track = tracks[_index_0]
+ append_track(command, track)
+ end
+ end
+ end
+ for track_type, tracks in pairs(supported_active_tracks) do
+ local _continue_0 = false
+ repeat
+ if #tracks > 0 then
+ _continue_0 = true
+ break
+ end
+ local _exp_0 = track_type
+ if "video" == _exp_0 then
+ append(command, {
+ "--vid=no"
+ })
+ elseif "audio" == _exp_0 then
+ append(command, {
+ "--aid=no"
+ })
+ elseif "sub" == _exp_0 then
+ append(command, {
+ "--sid=no"
+ })
+ end
+ _continue_0 = true
+ until true
+ if not _continue_0 then
+ break
+ end
+ end
+ if format.videoCodec ~= "" then
+ append(command, get_video_encode_flags(format, region))
+ end
+ append(command, format:getFlags())
+ if options.write_filename_on_metadata then
+ append(command, get_metadata_flags())
+ end
+ if format.acceptsBitrate then
+ if options.target_filesize > 0 then
+ local length = endTime - startTime
+ local video_bitrate, audio_bitrate = calculate_bitrate(supported_active_tracks, format, length)
+ if video_bitrate then
+ append(command, {
+ "--ovcopts-add=b=" .. tostring(video_bitrate) .. "k"
+ })
+ end
+ if audio_bitrate then
+ append(command, {
+ "--oacopts-add=b=" .. tostring(audio_bitrate) .. "k"
+ })
+ end
+ if options.strict_filesize_constraint then
+ local type = format.videoCodec ~= "" and "ovc" or "oac"
+ append(command, {
+ "--" .. tostring(type) .. "opts-add=minrate=" .. tostring(bitrate) .. "k",
+ "--" .. tostring(type) .. "opts-add=maxrate=" .. tostring(bitrate) .. "k"
+ })
+ end
+ else
+ local type = format.videoCodec ~= "" and "ovc" or "oac"
+ append(command, {
+ "--" .. tostring(type) .. "opts-add=b=0"
+ })
+ end
+ end
+ for token in string.gmatch(options.additional_flags, "[^%s]+") do
+ command[#command + 1] = token
+ end
+ if not options.strict_filesize_constraint then
+ for token in string.gmatch(options.non_strict_additional_flags, "[^%s]+") do
+ command[#command + 1] = token
+ end
+ if options.crf >= 0 then
+ append(command, {
+ "--ovcopts-add=crf=" .. tostring(options.crf)
+ })
+ end
+ end
+ local dir = ""
+ if is_stream then
+ dir = parse_directory("~")
+ else
+ local _
+ dir, _ = utils.split_path(path)
+ end
+ if options.output_directory ~= "" then
+ dir = parse_directory(options.output_directory)
+ end
+ local formatted_filename = format_filename(startTime, endTime, format)
+ local out_path = utils.join_path(dir, formatted_filename)
+ append(command, {
+ "--o=" .. tostring(out_path)
+ })
+ if options.twopass and format.supportsTwopass and not is_stream then
+ local first_pass_cmdline
+ do
+ local _accum_0 = { }
+ local _len_0 = 1
+ for _index_0 = 1, #command do
+ local arg = command[_index_0]
+ _accum_0[_len_0] = arg
+ _len_0 = _len_0 + 1
+ end
+ first_pass_cmdline = _accum_0
+ end
+ append(first_pass_cmdline, {
+ "--ovcopts-add=flags=+pass1"
+ })
+ message("Starting first pass...")
+ msg.verbose("First-pass command line: ", table.concat(first_pass_cmdline, " "))
+ local res = run_subprocess({
+ args = first_pass_cmdline,
+ cancellable = false
+ })
+ if not res then
+ message("First pass failed! Check the logs for details.")
+ return
+ end
+ append(command, {
+ "--ovcopts-add=flags=+pass2"
+ })
+ if format.videoCodec == "libvpx" then
+ msg.verbose("Patching libvpx pass log file...")
+ vp8_patch_logfile(get_pass_logfile_path(out_path), endTime - startTime)
+ end
+ end
+ msg.info("Encoding to", out_path)
+ msg.verbose("Command line:", table.concat(command, " "))
+ if options.run_detached then
+ message("Started encode, process was detached.")
+ return utils.subprocess_detached({
+ args = command
+ })
+ else
+ local res = false
+ if not should_display_progress() then
+ message("Started encode...")
+ res = run_subprocess({
+ args = command,
+ cancellable = false
+ })
+ else
+ local ewp = EncodeWithProgress(startTime, endTime)
+ res = ewp:startEncode(command)
+ end
+ if res then
+ message("Encoded successfully! Saved to\\N" .. tostring(bold(out_path)))
+ else
+ message("Encode failed! Check the logs for details.")
+ end
+ return os.remove(get_pass_logfile_path(out_path))
+ end
+end
+local CropPage
+do
+ local _class_0
+ local _parent_0 = Page
+ local _base_0 = {
+ reset = function(self)
+ local dimensions = get_video_dimensions()
+ local xa, ya
+ do
+ local _obj_0 = dimensions.top_left
+ xa, ya = _obj_0.x, _obj_0.y
+ end
+ self.pointA:set_from_screen(xa, ya)
+ local xb, yb
+ do
+ local _obj_0 = dimensions.bottom_right
+ xb, yb = _obj_0.x, _obj_0.y
+ end
+ self.pointB:set_from_screen(xb, yb)
+ if self.visible then
+ return self:draw()
+ end
+ end,
+ setPointA = function(self)
+ local posX, posY = mp.get_mouse_pos()
+ self.pointA:set_from_screen(posX, posY)
+ if self.visible then
+ return self:draw()
+ end
+ end,
+ setPointB = function(self)
+ local posX, posY = mp.get_mouse_pos()
+ self.pointB:set_from_screen(posX, posY)
+ if self.visible then
+ return self:draw()
+ end
+ end,
+ cancel = function(self)
+ self:hide()
+ return self.callback(false, nil)
+ end,
+ finish = function(self)
+ local region = Region()
+ region:set_from_points(self.pointA, self.pointB)
+ self:hide()
+ return self.callback(true, region)
+ end,
+ draw_box = function(self, ass)
+ local region = Region()
+ region:set_from_points(self.pointA:to_screen(), self.pointB:to_screen())
+ local d = get_video_dimensions()
+ ass:new_event()
+ ass:append("{\\an7}")
+ ass:pos(0, 0)
+ ass:append('{\\bord0}')
+ ass:append('{\\shad0}')
+ ass:append('{\\c&H000000&}')
+ ass:append('{\\alpha&H77}')
+ ass:draw_start()
+ ass:rect_cw(d.top_left.x, d.top_left.y, region.x, region.y + region.h)
+ ass:rect_cw(region.x, d.top_left.y, d.bottom_right.x, region.y)
+ ass:rect_cw(d.top_left.x, region.y + region.h, region.x + region.w, d.bottom_right.y)
+ ass:rect_cw(region.x + region.w, region.y, d.bottom_right.x, d.bottom_right.y)
+ return ass:draw_stop()
+ end,
+ draw = function(self)
+ local window = { }
+ window.w, window.h = mp.get_osd_size()
+ local ass = assdraw.ass_new()
+ self:draw_box(ass)
+ ass:new_event()
+ self:setup_text(ass)
+ ass:append(tostring(bold('Crop:')) .. "\\N")
+ ass:append(tostring(bold('1:')) .. " change point A (" .. tostring(self.pointA.x) .. ", " .. tostring(self.pointA.y) .. ")\\N")
+ ass:append(tostring(bold('2:')) .. " change point B (" .. tostring(self.pointB.x) .. ", " .. tostring(self.pointB.y) .. ")\\N")
+ ass:append(tostring(bold('r:')) .. " reset to whole screen\\N")
+ ass:append(tostring(bold('ESC:')) .. " cancel crop\\N")
+ local width, height = math.abs(self.pointA.x - self.pointB.x), math.abs(self.pointA.y - self.pointB.y)
+ ass:append(tostring(bold('ENTER:')) .. " confirm crop (" .. tostring(width) .. "x" .. tostring(height) .. ")\\N")
+ return mp.set_osd_ass(window.w, window.h, ass.text)
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self, callback, region)
+ self.pointA = VideoPoint()
+ self.pointB = VideoPoint()
+ self.keybinds = {
+ ["1"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.setPointA
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["2"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.setPointB
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["r"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.reset
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["ESC"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.cancel
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["ENTER"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.finish
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)()
+ }
+ self:reset()
+ self.callback = callback
+ if region and region:is_valid() then
+ self.pointA.x = region.x
+ self.pointA.y = region.y
+ self.pointB.x = region.x + region.w
+ self.pointB.y = region.y + region.h
+ end
+ end,
+ __base = _base_0,
+ __name = "CropPage",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ CropPage = _class_0
+end
+local Option
+do
+ local _class_0
+ local _base_0 = {
+ hasPrevious = function(self)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ return true
+ elseif "int" == _exp_0 then
+ if self.opts.min then
+ return self.value > self.opts.min
+ else
+ return true
+ end
+ elseif "list" == _exp_0 then
+ return self.value > 1
+ end
+ end,
+ hasNext = function(self)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ return true
+ elseif "int" == _exp_0 then
+ if self.opts.max then
+ return self.value < self.opts.max
+ else
+ return true
+ end
+ elseif "list" == _exp_0 then
+ return self.value < #self.opts.possibleValues
+ end
+ end,
+ leftKey = function(self)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ self.value = not self.value
+ elseif "int" == _exp_0 then
+ self.value = self.value - self.opts.step
+ if self.opts.min and self.opts.min > self.value then
+ self.value = self.opts.min
+ end
+ elseif "list" == _exp_0 then
+ if self.value > 1 then
+ self.value = self.value - 1
+ end
+ end
+ end,
+ rightKey = function(self)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ self.value = not self.value
+ elseif "int" == _exp_0 then
+ self.value = self.value + self.opts.step
+ if self.opts.max and self.opts.max < self.value then
+ self.value = self.opts.max
+ end
+ elseif "list" == _exp_0 then
+ if self.value < #self.opts.possibleValues then
+ self.value = self.value + 1
+ end
+ end
+ end,
+ getValue = function(self)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ return self.value
+ elseif "int" == _exp_0 then
+ return self.value
+ elseif "list" == _exp_0 then
+ local value, _
+ do
+ local _obj_0 = self.opts.possibleValues[self.value]
+ value, _ = _obj_0[1], _obj_0[2]
+ end
+ return value
+ end
+ end,
+ setValue = function(self, value)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ self.value = value
+ elseif "int" == _exp_0 then
+ self.value = value
+ elseif "list" == _exp_0 then
+ local set = false
+ for i, possiblePair in ipairs(self.opts.possibleValues) do
+ local possibleValue, _
+ possibleValue, _ = possiblePair[1], possiblePair[2]
+ if possibleValue == value then
+ set = true
+ self.value = i
+ break
+ end
+ end
+ if not set then
+ return msg.warn("Tried to set invalid value " .. tostring(value) .. " to " .. tostring(self.displayText) .. " option.")
+ end
+ end
+ end,
+ getDisplayValue = function(self)
+ local _exp_0 = self.optType
+ if "bool" == _exp_0 then
+ return self.value and "yes" or "no"
+ elseif "int" == _exp_0 then
+ if self.opts.altDisplayNames and self.opts.altDisplayNames[self.value] then
+ return self.opts.altDisplayNames[self.value]
+ else
+ return tostring(self.value)
+ end
+ elseif "list" == _exp_0 then
+ local value, displayValue
+ do
+ local _obj_0 = self.opts.possibleValues[self.value]
+ value, displayValue = _obj_0[1], _obj_0[2]
+ end
+ return displayValue or value
+ end
+ end,
+ draw = function(self, ass, selected)
+ if selected then
+ ass:append(tostring(bold(self.displayText)) .. ": ")
+ else
+ ass:append(tostring(self.displayText) .. ": ")
+ end
+ if self:hasPrevious() then
+ ass:append("◀ ")
+ end
+ ass:append(self:getDisplayValue())
+ if self:hasNext() then
+ ass:append(" ▶")
+ end
+ return ass:append("\\N")
+ end
+ }
+ _base_0.__index = _base_0
+ _class_0 = setmetatable({
+ __init = function(self, optType, displayText, value, opts)
+ self.optType = optType
+ self.displayText = displayText
+ self.opts = opts
+ self.value = 1
+ return self:setValue(value)
+ end,
+ __base = _base_0,
+ __name = "Option"
+ }, {
+ __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
+ Option = _class_0
+end
+local EncodeOptionsPage
+do
+ local _class_0
+ local _parent_0 = Page
+ local _base_0 = {
+ getCurrentOption = function(self)
+ return self.options[self.currentOption][2]
+ end,
+ leftKey = function(self)
+ (self:getCurrentOption()):leftKey()
+ return self:draw()
+ end,
+ rightKey = function(self)
+ (self:getCurrentOption()):rightKey()
+ return self:draw()
+ end,
+ prevOpt = function(self)
+ self.currentOption = math.max(1, self.currentOption - 1)
+ return self:draw()
+ end,
+ nextOpt = function(self)
+ self.currentOption = math.min(#self.options, self.currentOption + 1)
+ return self:draw()
+ end,
+ confirmOpts = function(self)
+ for _, optPair in ipairs(self.options) do
+ local optName, opt
+ optName, opt = optPair[1], optPair[2]
+ options[optName] = opt:getValue()
+ end
+ self:hide()
+ return self.callback(true)
+ end,
+ cancelOpts = function(self)
+ self:hide()
+ return self.callback(false)
+ end,
+ draw = function(self)
+ local window_w, window_h = mp.get_osd_size()
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ self:setup_text(ass)
+ ass:append(tostring(bold('Options:')) .. "\\N\\N")
+ for i, optPair in ipairs(self.options) do
+ local opt = optPair[2]
+ opt:draw(ass, self.currentOption == i)
+ end
+ ass:append("\\N▲ / ▼: navigate\\N")
+ ass:append(tostring(bold('ENTER:')) .. " confirm options\\N")
+ ass:append(tostring(bold('ESC:')) .. " cancel\\N")
+ return mp.set_osd_ass(window_w, window_h, ass.text)
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self, callback)
+ self.callback = callback
+ self.currentOption = 1
+ local scaleHeightOpts = {
+ possibleValues = {
+ {
+ -1,
+ "no"
+ },
+ {
+ 240
+ },
+ {
+ 360
+ },
+ {
+ 480
+ },
+ {
+ 720
+ },
+ {
+ 1080
+ },
+ {
+ 1440
+ },
+ {
+ 2160
+ }
+ }
+ }
+ local filesizeOpts = {
+ step = 250,
+ min = 0,
+ altDisplayNames = {
+ [0] = "0 (constant quality)"
+ }
+ }
+ local crfOpts = {
+ step = 1,
+ min = -1,
+ altDisplayNames = {
+ [-1] = "disabled"
+ }
+ }
+ local fpsOpts = {
+ possibleValues = {
+ {
+ -1,
+ "source"
+ },
+ {
+ 15
+ },
+ {
+ 24
+ },
+ {
+ 30
+ },
+ {
+ 48
+ },
+ {
+ 50
+ },
+ {
+ 60
+ },
+ {
+ 120
+ },
+ {
+ 240
+ }
+ }
+ }
+ local formatIds = {
+ "webm-vp8",
+ "webm-vp9",
+ "mp4",
+ "mp4-nvenc",
+ "raw",
+ "mp3",
+ "gif"
+ }
+ local formatOpts = {
+ possibleValues = (function()
+ local _accum_0 = { }
+ local _len_0 = 1
+ for _index_0 = 1, #formatIds do
+ local fId = formatIds[_index_0]
+ _accum_0[_len_0] = {
+ fId,
+ formats[fId].displayName
+ }
+ _len_0 = _len_0 + 1
+ end
+ return _accum_0
+ end)()
+ }
+ self.options = {
+ {
+ "output_format",
+ Option("list", "Output Format", options.output_format, formatOpts)
+ },
+ {
+ "twopass",
+ Option("bool", "Two Pass", options.twopass)
+ },
+ {
+ "apply_current_filters",
+ Option("bool", "Apply Current Video Filters", options.apply_current_filters)
+ },
+ {
+ "scale_height",
+ Option("list", "Scale Height", options.scale_height, scaleHeightOpts)
+ },
+ {
+ "strict_filesize_constraint",
+ Option("bool", "Strict Filesize Constraint", options.strict_filesize_constraint)
+ },
+ {
+ "write_filename_on_metadata",
+ Option("bool", "Write Filename on Metadata", options.write_filename_on_metadata)
+ },
+ {
+ "target_filesize",
+ Option("int", "Target Filesize", options.target_filesize, filesizeOpts)
+ },
+ {
+ "crf",
+ Option("int", "CRF", options.crf, crfOpts)
+ },
+ {
+ "fps",
+ Option("list", "FPS", options.fps, fpsOpts)
+ }
+ }
+ self.keybinds = {
+ ["LEFT"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.leftKey
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["RIGHT"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.rightKey
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["UP"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.prevOpt
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["DOWN"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.nextOpt
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["ENTER"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.confirmOpts
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["ESC"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.cancelOpts
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)()
+ }
+ end,
+ __base = _base_0,
+ __name = "EncodeOptionsPage",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ EncodeOptionsPage = _class_0
+end
+local PreviewPage
+do
+ local _class_0
+ local _parent_0 = Page
+ local _base_0 = {
+ prepare = function(self)
+ local vf = mp.get_property_native("vf")
+ vf[#vf + 1] = {
+ name = "sub"
+ }
+ if self.region:is_valid() then
+ vf[#vf + 1] = {
+ name = "crop",
+ params = {
+ w = tostring(self.region.w),
+ h = tostring(self.region.h),
+ x = tostring(self.region.x),
+ y = tostring(self.region.y)
+ }
+ }
+ end
+ mp.set_property_native("vf", vf)
+ if self.startTime > -1 and self.endTime > -1 then
+ mp.set_property_native("ab-loop-a", self.startTime)
+ mp.set_property_native("ab-loop-b", self.endTime)
+ mp.set_property_native("time-pos", self.startTime)
+ end
+ return mp.set_property_native("pause", false)
+ end,
+ dispose = function(self)
+ mp.set_property("ab-loop-a", "no")
+ mp.set_property("ab-loop-b", "no")
+ for prop, value in pairs(self.originalProperties) do
+ mp.set_property_native(prop, value)
+ end
+ end,
+ draw = function(self)
+ local window_w, window_h = mp.get_osd_size()
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ self:setup_text(ass)
+ ass:append("Press " .. tostring(bold('ESC')) .. " to exit preview.\\N")
+ return mp.set_osd_ass(window_w, window_h, ass.text)
+ end,
+ cancel = function(self)
+ self:hide()
+ return self.callback()
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self, callback, region, startTime, endTime)
+ self.callback = callback
+ self.originalProperties = {
+ ["vf"] = mp.get_property_native("vf"),
+ ["time-pos"] = mp.get_property_native("time-pos"),
+ ["pause"] = mp.get_property_native("pause")
+ }
+ self.keybinds = {
+ ["ESC"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.cancel
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)()
+ }
+ self.region = region
+ self.startTime = startTime
+ self.endTime = endTime
+ self.isLoop = false
+ end,
+ __base = _base_0,
+ __name = "PreviewPage",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ PreviewPage = _class_0
+end
+local MainPage
+do
+ local _class_0
+ local _parent_0 = Page
+ local _base_0 = {
+ setStartTime = function(self)
+ self.startTime = mp.get_property_number("time-pos")
+ if self.visible then
+ self:clear()
+ return self:draw()
+ end
+ end,
+ setEndTime = function(self)
+ self.endTime = mp.get_property_number("time-pos")
+ if self.visible then
+ self:clear()
+ return self:draw()
+ end
+ end,
+ setupStartAndEndTimes = function(self)
+ if mp.get_property_native("duration") then
+ self.startTime = 0
+ self.endTime = mp.get_property_native("duration")
+ else
+ self.startTime = -1
+ self.endTime = -1
+ end
+ if self.visible then
+ self:clear()
+ return self:draw()
+ end
+ end,
+ draw = function(self)
+ local window_w, window_h = mp.get_osd_size()
+ local ass = assdraw.ass_new()
+ ass:new_event()
+ self:setup_text(ass)
+ ass:append(tostring(bold('WebM maker')) .. "\\N\\N")
+ ass:append(tostring(bold('c:')) .. " crop\\N")
+ ass:append(tostring(bold('1:')) .. " set start time (current is " .. tostring(seconds_to_time_string(self.startTime)) .. ")\\N")
+ ass:append(tostring(bold('2:')) .. " set end time (current is " .. tostring(seconds_to_time_string(self.endTime)) .. ")\\N")
+ ass:append(tostring(bold('o:')) .. " change encode options\\N")
+ ass:append(tostring(bold('p:')) .. " preview\\N")
+ ass:append(tostring(bold('e:')) .. " encode\\N\\N")
+ ass:append(tostring(bold('ESC:')) .. " close\\N")
+ return mp.set_osd_ass(window_w, window_h, ass.text)
+ end,
+ onUpdateCropRegion = function(self, updated, newRegion)
+ if updated then
+ self.region = newRegion
+ end
+ return self:show()
+ end,
+ crop = function(self)
+ self:hide()
+ local cropPage = CropPage((function()
+ local _base_1 = self
+ local _fn_0 = _base_1.onUpdateCropRegion
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(), self.region)
+ return cropPage:show()
+ end,
+ onOptionsChanged = function(self, updated)
+ return self:show()
+ end,
+ changeOptions = function(self)
+ self:hide()
+ local encodeOptsPage = EncodeOptionsPage((function()
+ local _base_1 = self
+ local _fn_0 = _base_1.onOptionsChanged
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)())
+ return encodeOptsPage:show()
+ end,
+ onPreviewEnded = function(self)
+ return self:show()
+ end,
+ preview = function(self)
+ self:hide()
+ local previewPage = PreviewPage((function()
+ local _base_1 = self
+ local _fn_0 = _base_1.onPreviewEnded
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(), self.region, self.startTime, self.endTime)
+ return previewPage:show()
+ end,
+ encode = function(self)
+ self:hide()
+ if self.startTime < 0 then
+ message("No start time, aborting")
+ return
+ end
+ if self.endTime < 0 then
+ message("No end time, aborting")
+ return
+ end
+ if self.startTime >= self.endTime then
+ message("Start time is ahead of end time, aborting")
+ return
+ end
+ return encode(self.region, self.startTime, self.endTime)
+ end
+ }
+ _base_0.__index = _base_0
+ setmetatable(_base_0, _parent_0.__base)
+ _class_0 = setmetatable({
+ __init = function(self)
+ self.keybinds = {
+ ["c"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.crop
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["1"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.setStartTime
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["2"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.setEndTime
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["o"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.changeOptions
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["p"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.preview
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["e"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.encode
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)(),
+ ["ESC"] = (function()
+ local _base_1 = self
+ local _fn_0 = _base_1.hide
+ return function(...)
+ return _fn_0(_base_1, ...)
+ end
+ end)()
+ }
+ self.startTime = -1
+ self.endTime = -1
+ self.region = Region()
+ end,
+ __base = _base_0,
+ __name = "MainPage",
+ __parent = _parent_0
+ }, {
+ __index = function(cls, name)
+ local val = rawget(_base_0, name)
+ if val == nil then
+ local parent = rawget(cls, "__parent")
+ if parent then
+ return parent[name]
+ end
+ else
+ return val
+ end
+ end,
+ __call = function(cls, ...)
+ local _self_0 = setmetatable({}, _base_0)
+ cls.__init(_self_0, ...)
+ return _self_0
+ end
+ })
+ _base_0.__class = _class_0
+ if _parent_0.__inherited then
+ _parent_0.__inherited(_parent_0, _class_0)
+ end
+ MainPage = _class_0
+end
+monitor_dimensions()
+local mainPage = MainPage()
+mp.add_key_binding(options.keybind, "display-webm-encoder", (function()
+ local _base_0 = mainPage
+ local _fn_0 = _base_0.show
+ return function(...)
+ return _fn_0(_base_0, ...)
+ end
+end)(), {
+ repeatable = false
+})
+return mp.register_event("file-loaded", (function()
+ local _base_0 = mainPage
+ local _fn_0 = _base_0.setupStartAndEndTimes
+ return function(...)
+ return _fn_0(_base_0, ...)
+ end
+end)())
diff --git a/mpv/scripts/youtube-quality.lua b/mpv/scripts/youtube-quality.lua
new file mode 100644
index 0000000..ba95fef
--- /dev/null
+++ b/mpv/scripts/youtube-quality.lua
@@ -0,0 +1,275 @@
+-- youtube-quality.lua
+--
+-- Change youtube video quality on the fly.
+--
+-- Diplays a menu that lets you switch to different ytdl-format settings while
+-- you're in the middle of a video (just like you were using the web player).
+--
+-- Bound to ctrl-f by default.
+
+local mp = require 'mp'
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local opts = {
+ --key bindings
+ toggle_menu_binding = "ctrl+f",
+ up_binding = "UP",
+ down_binding = "DOWN",
+ select_binding = "ENTER",
+
+ --formatting / cursors
+ selected_and_active = "▶ - ",
+ selected_and_inactive = "● - ",
+ unselected_and_active = "▷ - ",
+ unselected_and_inactive = "○ - ",
+
+ --font size scales by window, if false requires larger font and padding sizes
+ scale_playlist_by_window=false,
+
+ --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
+ --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
+ --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
+ --undeclared tags will use default osd settings
+ --these styles will be used for the whole playlist. More specific styling will need to be hacked in
+ --
+ --(a monospaced font is recommended but not required)
+ style_ass_tags = "{\\fnmonospace}",
+
+ --paddings for top left corner
+ text_padding_x = 5,
+ text_padding_y = 5,
+
+ --other
+ menu_timeout = 10,
+
+ --use youtube-dl to fetch a list of available formats (overrides quality_strings)
+ fetch_formats = true,
+
+ --default menu entries
+ quality_strings=[[
+ [
+ {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"},
+ {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"},
+ {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"},
+ {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"},
+ {"720p" : "bestvideo[height<=?720]+bestaudio/best"},
+ {"480p" : "bestvideo[height<=?480]+bestaudio/best"},
+ {"360p" : "bestvideo[height<=?360]+bestaudio/best"},
+ {"240p" : "bestvideo[height<=?240]+bestaudio/best"},
+ {"144p" : "bestvideo[height<=?144]+bestaudio/best"}
+ ]
+ ]],
+}
+(require 'mp.options').read_options(opts, "youtube-quality")
+opts.quality_strings = utils.parse_json(opts.quality_strings)
+
+local destroyer = nil
+
+
+function show_menu()
+ local selected = 1
+ local active = 0
+ local current_ytdl_format = mp.get_property("ytdl-format")
+ msg.verbose("current ytdl-format: "..current_ytdl_format)
+ local num_options = 0
+ local options = {}
+
+
+ if opts.fetch_formats then
+ options, num_options = download_formats()
+ end
+
+ if next(options) == nil then
+ for i,v in ipairs(opts.quality_strings) do
+ num_options = num_options + 1
+ for k,v2 in pairs(v) do
+ options[i] = {label = k, format=v2}
+ if v2 == current_ytdl_format then
+ active = i
+ selected = active
+ end
+ end
+ end
+ end
+
+ --set the cursor to the currently format
+ for i,v in ipairs(options) do
+ if v.format == current_ytdl_format then
+ active = i
+ selected = active
+ break
+ end
+ end
+
+ function selected_move(amt)
+ selected = selected + amt
+ if selected < 1 then selected = num_options
+ elseif selected > num_options then selected = 1 end
+ timeout:kill()
+ timeout:resume()
+ draw_menu()
+ end
+ function choose_prefix(i)
+ if i == selected and i == active then return opts.selected_and_active
+ elseif i == selected then return opts.selected_and_inactive end
+
+ if i ~= selected and i == active then return opts.unselected_and_active
+ elseif i ~= selected then return opts.unselected_and_inactive end
+ return "> " --shouldn't get here.
+ end
+
+ function draw_menu()
+ local ass = assdraw.ass_new()
+
+ ass:pos(opts.text_padding_x, opts.text_padding_y)
+ ass:append(opts.style_ass_tags)
+
+ for i,v in ipairs(options) do
+ ass:append(choose_prefix(i)..v.label.."\\N")
+ end
+
+ local w, h = mp.get_osd_size()
+ if opts.scale_playlist_by_window then w,h = 0, 0 end
+ mp.set_osd_ass(w, h, ass.text)
+ end
+
+ function destroy()
+ timeout:kill()
+ mp.set_osd_ass(0,0,"")
+ mp.remove_key_binding("move_up")
+ mp.remove_key_binding("move_down")
+ mp.remove_key_binding("select")
+ mp.remove_key_binding("escape")
+ destroyer = nil
+ end
+ timeout = mp.add_periodic_timer(opts.menu_timeout, destroy)
+ destroyer = destroy
+
+ mp.add_forced_key_binding(opts.up_binding, "move_up", function() selected_move(-1) end, {repeatable=true})
+ mp.add_forced_key_binding(opts.down_binding, "move_down", function() selected_move(1) end, {repeatable=true})
+ mp.add_forced_key_binding(opts.select_binding, "select", function()
+ destroy()
+ mp.set_property("ytdl-format", options[selected].format)
+ reload_resume()
+ end)
+ mp.add_forced_key_binding(opts.toggle_menu_binding, "escape", destroy)
+
+ draw_menu()
+ return
+end
+
+local ytdl = {
+ path = "youtube-dl",
+ searched = false,
+ blacklisted = {}
+}
+
+format_cache={}
+function download_formats()
+ local function exec(args)
+ local ret = utils.subprocess({args = args})
+ return ret.status, ret.stdout, ret
+ end
+
+ local function table_size(t)
+ s = 0
+ for i,v in ipairs(t) do
+ s = s+1
+ end
+ return s
+ end
+
+ local url = mp.get_property("path")
+
+ url = string.gsub(url, "ytdl://", "") -- Strip possible ytdl:// prefix.
+
+ -- don't fetch the format list if we already have it
+ if format_cache[url] ~= nil then
+ local res = format_cache[url]
+ return res, table_size(res)
+ end
+ mp.osd_message("fetching available formats with youtube-dl...", 60)
+
+ if not (ytdl.searched) then
+ local ytdl_mcd = mp.find_config_file("youtube-dl")
+ if not (ytdl_mcd == nil) then
+ msg.verbose("found youtube-dl at: " .. ytdl_mcd)
+ ytdl.path = ytdl_mcd
+ end
+ ytdl.searched = true
+ end
+
+ local command = {ytdl.path, "--no-warnings", "--no-playlist", "-J"}
+ table.insert(command, url)
+ local es, json, result = exec(command)
+
+ if (es < 0) or (json == nil) or (json == "") then
+ mp.osd_message("fetching formats failed...", 1)
+ msg.error("failed to get format list: " .. err)
+ return {}, 0
+ end
+
+ local json, err = utils.parse_json(json)
+
+ if (json == nil) then
+ mp.osd_message("fetching formats failed...", 1)
+ msg.error("failed to parse JSON data: " .. err)
+ return {}, 0
+ end
+
+ res = {}
+ msg.verbose("youtube-dl succeeded!")
+ for i,v in ipairs(json.formats) do
+ if v.vcodec ~= "none" then
+ local fps = v.fps and v.fps.."fps" or ""
+ local resolution = string.format("%sx%s", v.width, v.height)
+ local l = string.format("%-9s %-5s (%-4s / %s)", resolution, fps, v.ext, v.vcodec)
+ local f = string.format("%s+bestaudio/best", v.format_id)
+ table.insert(res, {label=l, format=f, width=v.width })
+ end
+ end
+
+ table.sort(res, function(a, b) return a.width > b.width end)
+
+ mp.osd_message("", 0)
+ format_cache[url] = res
+ return res, table_size(res)
+end
+
+
+-- register script message to show menu
+mp.register_script_message("toggle-quality-menu",
+function()
+ if destroyer ~= nil then
+ destroyer()
+ else
+ show_menu()
+ end
+end)
+
+-- keybind to launch menu
+mp.add_forced_key_binding(opts.toggle_menu_binding, "quality-menu", show_menu)
+
+-- special thanks to reload.lua (https://github.com/4e6/mpv-reload/)
+function reload_resume()
+ local playlist_pos = mp.get_property_number("playlist-pos")
+ local reload_duration = mp.get_property_native("duration")
+ local time_pos = mp.get_property("time-pos")
+
+ mp.set_property_number("playlist-pos", playlist_pos)
+
+ -- 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
+ local function seeker()
+ mp.commandv("seek", time_pos, "absolute")
+ mp.unregister_event(seeker)
+ end
+ mp.register_event("file-loaded", seeker)
+ end
+end