From 30a3d9d4393b66ca1945771c3b80deb53e1744aa Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Tue, 15 Jun 2021 12:59:34 +0200 Subject: [PATCH] mpv updated --- .config/mpv/script-opts/youtube-quality.conf | 41 + .config/mpv/script-opts/youtube-upnext.conf | 57 + .config/mpv/scripts/mpv-webm.lua | 2696 ++++++++++++++++++ .config/mpv/scripts/mpvSockets.lua | 36 + .config/mpv/scripts/youtube-quality.lua | 275 ++ .config/mpv/scripts/youtube-upnext.lua | 531 ++++ 6 files changed, 3636 insertions(+) create mode 100644 .config/mpv/script-opts/youtube-quality.conf create mode 100644 .config/mpv/script-opts/youtube-upnext.conf create mode 100644 .config/mpv/scripts/mpv-webm.lua create mode 100644 .config/mpv/scripts/mpvSockets.lua create mode 100644 .config/mpv/scripts/youtube-quality.lua create mode 100644 .config/mpv/scripts/youtube-upnext.lua diff --git a/.config/mpv/script-opts/youtube-quality.conf b/.config/mpv/script-opts/youtube-quality.conf new file mode 100644 index 0000000..9c1a8c9 --- /dev/null +++ b/.config/mpv/script-opts/youtube-quality.conf @@ -0,0 +1,41 @@ +# KEY BINDINGS + +# invoke or dismiss the quality menu +toggle_menu_binding=ctrl+f +# move the menu cursor up +up_binding=UP +# move the menu cursor down +down_binding=DOWN +# select menu entry +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=no + +# 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 + +# how many seconds until the quality menu times out +menu_timeout=10 + +#use youtube-dl to fetch a list of available formats (overrides quality_strings) +fetch_formats=yes + +# list of ytdl-format strings to choose from +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"} ] diff --git a/.config/mpv/script-opts/youtube-upnext.conf b/.config/mpv/script-opts/youtube-upnext.conf new file mode 100644 index 0000000..3c56ce0 --- /dev/null +++ b/.config/mpv/script-opts/youtube-upnext.conf @@ -0,0 +1,57 @@ +# KEY BINDINGS + +# invoke or dismiss the quality menu +toggle_menu_binding=ctrl+u +# move the menu cursor up +up_binding=UP +# move the menu cursor down +down_binding=DOWN +# select menu entry +select_binding=ENTER + +# auto load and add the "upnext" video to the playlist +auto_add=yes + +# formatting / cursors +cursor_selected=● - +cursor_unselected=○ - + +# font size scales by window, if false requires larger font and padding sizes +scale_playlist_by_window=no + +# 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 + +# how many seconds until the quality menu times out +menu_timeout=10 + +# base url for loading new urls, %s will be replaced with video id +youtube_url=https://www.youtube.com/watch?v=%s + +# Fallback Invidious instance. Used if "upnext" could not be retrieved from the normal youtube website +# See https://instances.invidio.us/ for alternatives e.g. https://invidious.snopyta.org +invidious_instance=https://invidious.xyz + +# Keep the width of the window the same when the next video is played +restore_window_width=no + +# On Windows wget.exe may not be able to check SSL certificates for HTTPS, so you can disable it here +check_certificate=yes + + +# Use a cookies file +# Same as youtube-dl --cookies or wget --load-cookies +# If you don't set this, the script may create a cookie file for you +# For example "C:\\Users\\Username\\cookies.txt" +# Or "C:/Users/Username/cookies.txt" +#cookies=cookies.txt \ No newline at end of file diff --git a/.config/mpv/scripts/mpv-webm.lua b/.config/mpv/scripts/mpv-webm.lua new file mode 100644 index 0000000..f441481 --- /dev/null +++ b/.config/mpv/scripts/mpv-webm.lua @@ -0,0 +1,2696 @@ +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 = "D", + -- 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", + output_template = "%T", + -- 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!") + 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" + }, + { + 2 + }, + { + 5 + }, + { + 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/.config/mpv/scripts/mpvSockets.lua b/.config/mpv/scripts/mpvSockets.lua new file mode 100644 index 0000000..df8d078 --- /dev/null +++ b/.config/mpv/scripts/mpvSockets.lua @@ -0,0 +1,36 @@ +-- mpvSockets, one socket per instance, removes socket on exit + +local utils = require 'mp.utils' + +local function get_temp_path() + local directory_seperator = package.config:match("([^\n]*)\n?") + local example_temp_file_path = os.tmpname() + + -- remove generated temp file + pcall(os.remove, example_temp_file_path) + + local seperator_idx = example_temp_file_path:reverse():find(directory_seperator) + local temp_path_length = #example_temp_file_path - seperator_idx + + return example_temp_file_path:sub(1, temp_path_length) +end + +tempDir = get_temp_path() + +function join_paths(...) + local arg={...} + path = "" + for i,v in ipairs(arg) do + path = utils.join_path(path, tostring(v)) + end + return path; +end + +ppid = utils.getpid() +os.execute("mkdir " .. join_paths(tempDir, "mpvSockets") .. " 2>/dev/null") +mp.set_property("options/input-ipc-server", join_paths(tempDir, "mpvSockets", ppid)) + +function shutdown_handler() + os.remove(join_paths(tempDir, "mpvSockets", ppid)) +end +mp.register_event("shutdown", shutdown_handler) diff --git a/.config/mpv/scripts/youtube-quality.lua b/.config/mpv/scripts/youtube-quality.lua new file mode 100644 index 0000000..b587f37 --- /dev/null +++ b/.config/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_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 diff --git a/.config/mpv/scripts/youtube-upnext.lua b/.config/mpv/scripts/youtube-upnext.lua new file mode 100644 index 0000000..15e7f8c --- /dev/null +++ b/.config/mpv/scripts/youtube-upnext.lua @@ -0,0 +1,531 @@ +-- youtube-upnext.lua +-- +-- Fetch upnext/recommended videos from youtube +-- This is forked/based on https://github.com/jgreco/mpv-youtube-quality +-- +-- Diplays a menu that lets you load the upnext/recommended video from youtube +-- that appear on the right side on the youtube website. +-- If auto_add is set to true (default), the 'up next' video is automatically +-- appended to the current playlist +-- +-- Bound to ctrl-u by default. +-- +-- Requires wget/wget.exe in PATH. On Windows you may need to set check_certificate +-- to false, otherwise wget.exe might not be able to download the youtube website. + +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+u", + up_binding = "UP", + down_binding = "DOWN", + select_binding = "ENTER", + + --auto load and add the "upnext" video to the playlist + auto_add = true, + + --formatting / cursors + cursor_selected = "● ", + cursor_unselected = "○ ", + + --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, + youtube_url = "https://www.youtube.com/watch?v=%s", + + -- Fallback Invidious instance, see https://instances.invidio.us/ for alternatives e.g. https://invidious.snopyta.org + invidious_instance = "https://invidious.xyz", + + -- Keep the width of the window the same when the next video is played + restore_window_width = false, + + -- On Windows wget.exe may not be able to check SSL certificates for HTTPS, so you can disable checking here + check_certificate = true, + + -- Use a cookies file + -- Same as youtube-dl --cookies or wget --load-cookies + -- If you don't set this, the script may create a cookie file for you + -- On Windows you need to use a double blackslash or a single fordwardslash + -- For example "C:\\Users\\Username\\cookies.txt" + -- Or "C:/Users/Username/cookies.txt" + cookies = "" +} +(require 'mp.options').read_options(opts, "youtube-upnext") + +-- Command line options +if opts.cookies == nil or opts.cookies == "" then + local raw_options = mp.get_property_native("options/ytdl-raw-options") + for param, arg in pairs(raw_options) do + if (param == "cookies") and (arg ~= "") then + opts.cookies = arg + end + end +end + +local destroyer = nil +local upnext_cache={} +local prefered_win_width = nil +local last_dheight = nil +local last_dwidth = nil + +local function table_size(t) + local s = 0 + for _, _ in ipairs(t) do + s = s+1 + end + return s +end + +local function exec(args) + local ret = utils.subprocess({args = args}) + return ret.status, ret.stdout, ret +end + +local function url_encode(s) + local function repl(x) + return string.format("%%%02X", string.byte(x)) + end + return string.gsub(s, "([^0-9a-zA-Z!'()*._~-])", repl) + end + +local function download_upnext(url, post_data) + local command = {"wget", "-q", "-O", "-"} + if not opts.check_certificate then + table.insert(command, "--no-check-certificate") + end + if post_data then + table.insert(command, "--post-data") + table.insert(command, post_data) + end + if opts.cookies then + table.insert(command, "--load-cookies") + table.insert(command, opts.cookies) + table.insert(command, "--save-cookies") + table.insert(command, opts.cookies) + table.insert(command, "--keep-session-cookies") + end + table.insert(command, url) + + local es, s, _ = exec(command) + + if (es ~= 0) or (s == nil) or (s == "") then + if es == 5 then + mp.osd_message("upnext failed: wget does not support HTTPS", 10) + msg.error("wget is missing certificates, disable check-certificate in userscript options") + elseif es == -1 or es == 127 or es == 9009 then + mp.osd_message("upnext failed: wget not found", 10) + msg.error("wget/ wget.exe is missing. Please install it or put an executable in your PATH") + else + mp.osd_message("upnext failed: error=" .. tostring(es), 10) + msg.error("failed to get upnext list: error=" .. tostring(es)) + end + return "{}" + end + + local consent_pos = s:find('action="https://consent.youtube.com/s"') + if consent_pos ~= nil then + -- Accept cookie consent form + msg.debug("Need to accept cookie consent form") + s = s:sub(s:find(">", consent_pos + 1, true), s:find("", pos1 + 1) + if pos2 ~= nil then + s = string.sub(s, pos1 + 15, pos2 - 1) + return s + else + msg.error("failed to find json position 02") + end + + mp.osd_message("upnext failed, no upnext data found err03", 10) + msg.error("failed to get upnext data: pos1=" .. tostring(pos1) .. " pos2=" ..tostring(pos2)) + return "{}" +end + +local function get_invidious(url) + -- convert to invidious API call + url = string.gsub(url, "https://youtube%.com/watch%?v=", opts.invidious_instance .. "/api/v1/videos/") + url = string.gsub(url, "https://www%.youtube%.com/watch%?v=", opts.invidious_instance .. "/api/v1/videos/") + url = string.gsub(url, "https://youtu%.be/", opts.invidious_instance .. "/api/v1/videos/") + msg.debug("Invidious url:" .. url) + + local command = {"wget", "-q", "-O", "-"} + if not opts.check_certificate then + table.insert(command, "--no-check-certificate") + end + table.insert(command, url) + + local es, s, _ = exec(command) + + if (es ~= 0) or (s == nil) or (s == "") then + if es == 5 then + mp.osd_message("upnext failed: wget does not support HTTPS", 10) + msg.error("wget is missing certificates, disable check-certificate in userscript options") + elseif es == -1 or es == 127 or es == 9009 then + mp.osd_message("upnext failed: wget not found", 10) + msg.error("wget/ wget.exe is missing. Please install it or put an executable in your PATH") + else + mp.osd_message("upnext failed: error=" .. tostring(es), 10) + msg.error("failed to get invidious: error=" .. tostring(es)) + end + return {} + end + + local data, err = utils.parse_json(s) + if data == nil then + mp.osd_message("upnext fetch failed (Invidious): JSON decode failed", 10) + msg.error("parse_json failed (Invidious): " .. err) + return {} + end + + if data.recommendedVideos then + local res = {} + msg.verbose("wget and json decode succeeded! (Invidious)") + for i, v in ipairs(data.recommendedVideos) do + table.insert(res, { + index=i, + label=v.title .. " - " .. v.author, + file=string.format(opts.youtube_url, v.videoId) + }) + end + mp.osd_message("upnext fetch from Invidious succeeded", 10) + return res + elseif data.error then + mp.osd_message("upnext fetch failed (Invidious): " .. data.error, 10) + msg.error("Invidious error: " .. data.error) + else + mp.osd_message("upnext: No recommended videos! (Invidious)", 10) + msg.error("No recommended videos! (Invidious)") + end + + return {} +end + + +local function parse_upnext(json_str, current_video_url) + if json_str == "{}" then + return {}, 0 + end + + local data, err = utils.parse_json(json_str) + + if data == nil then + mp.osd_message("upnext failed: JSON decode failed", 10) + msg.error("parse_json failed: " .. tostring(err)) + msg.debug("Corrupted JSON:\n" .. json_str .. "\n") + return {}, 0 + end + + local res = {} + msg.verbose("wget and json decode succeeded!") + + local index = 1 + local autoplay_id = nil + if data.playerOverlays + and data.playerOverlays.playerOverlayRenderer + and data.playerOverlays.playerOverlayRenderer.autoplay + and data.playerOverlays.playerOverlayRenderer.autoplay.playerOverlayAutoplayRenderer then + local title = data.playerOverlays.playerOverlayRenderer.autoplay.playerOverlayAutoplayRenderer.videoTitle.simpleText + local video_id = data.playerOverlays.playerOverlayRenderer.autoplay.playerOverlayAutoplayRenderer.videoId + autoplay_id = video_id + msg.debug("Found autoplay video") + table.insert(res, { + index=index, + label=title, + file=string.format(opts.youtube_url, video_id) + }) + index = index + 1 + end + + if data.playerOverlays + and data.playerOverlays.playerOverlayRenderer + and data.playerOverlays.playerOverlayRenderer.endScreen + and data.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer + and data.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer.results + then + local n = table_size(data.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer.results) + msg.debug("Found " .. tostring(n) .. " endScreen videos") + for i, v in ipairs(data.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer.results) do + if v.endScreenVideoRenderer + and v.endScreenVideoRenderer.title + and v.endScreenVideoRenderer.title.simpleText then + local title = v.endScreenVideoRenderer.title.simpleText + local video_id = v.endScreenVideoRenderer.videoId + if video_id ~= autoplay_id then + table.insert(res, { + index=index + i, + label=title, + file=string.format(opts.youtube_url, video_id) + }) + end + end + end + index = index + n + end + + if data.contents + and data.contents.twoColumnWatchNextResults + and data.contents.twoColumnWatchNextResults.secondaryResults + then + local secondaryResults = data.contents.twoColumnWatchNextResults.secondaryResults + if secondaryResults.secondaryResults then + secondaryResults = secondaryResults.secondaryResults + end + local n = table_size(secondaryResults.results) + msg.debug("Found " .. tostring(n) .. " watchNextResults videos") + for i, v in ipairs(secondaryResults.results) do + local compactVideoRenderer = nil + local watchnextindex = index + if v.compactAutoplayRenderer + and v.compactAutoplayRenderer + and v.compactAutoplayRenderer.contents + and v.compactAutoplayRenderer.contents.compactVideoRenderer then + compactVideoRenderer = v.compactAutoplayRenderer.contents.compactVideoRenderer + watchnextindex = 0 + elseif v.compactVideoRenderer then + compactVideoRenderer = v.compactVideoRenderer + end + if compactVideoRenderer + and compactVideoRenderer.videoId + and compactVideoRenderer.title + and compactVideoRenderer.title.simpleText + then + local title = compactVideoRenderer.title.simpleText + local video_id = compactVideoRenderer.videoId + local video_url = string.format(opts.youtube_url, video_id) + local duplicate = false + for _, entry in ipairs(res) do + if video_url == entry.file then + duplicate = true + end + end + if not duplicate then + table.insert(res, { + index=watchnextindex + i, + label=title, + file=video_url + }) + end + end + end + end + + table.sort(res, function(a, b) return a.index < b.index end) + + upnext_cache[current_video_url] = res + return res, table_size(res) +end + + +local function load_upnext() + local url = mp.get_property("path") + + url = string.gsub(url, "ytdl://", "") -- Strip possible ytdl:// prefix. + + if string.find(url, "//youtu.be/") == nil + and string.find(url, "//www.youtube.co.uk/") == nil + and string.find(url, "//youtube.com/") == nil + and string.find(url, "//www.youtube.com/") == nil + then + return {}, 0 + end + + -- don't fetch the website if it's already cached + if upnext_cache[url] ~= nil then + local res = upnext_cache[url] + return res, table_size(res) + end + + local res, n = parse_upnext(download_upnext(url, nil), url) + + -- Fallback to Invidious API + if n == 0 and opts.invidious_instance and opts.invidious_instance ~= "" then + res = get_invidious(url) + n = table_size(res) + end + + return res, n +end + +local function on_file_loaded(_) + local url = mp.get_property("path") + url = string.gsub(url, "ytdl://", "") -- Strip possible ytdl:// prefix. + if string.find(url, "youtu") ~= nil then + local upnext, num_upnext = load_upnext() + if num_upnext > 0 then + mp.commandv("loadfile", upnext[1].file, "append") + end + end +end + +local function show_menu() + mp.osd_message("fetching 'up next' with wget...", 60) + + local upnext, num_upnext = load_upnext() + if num_upnext == 0 then + return + end + mp.osd_message("", 1) + + local timeout + local selected = 1 + local function choose_prefix(i) + if i == selected then + return opts.cursor_selected + else + return opts.cursor_unselected + end + end + local 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(upnext) 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 + local function selected_move(amt) + selected = selected + amt + if selected < 1 then + selected = num_upnext + elseif selected > num_upnext then + selected = 1 + end + timeout:kill() + timeout:resume() + draw_menu() + end + + local 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.commandv("loadfile", upnext[selected].file, "replace") + end) + mp.add_forced_key_binding(opts.toggle_menu_binding, "escape", destroy) + + draw_menu() + return +end + +local function on_window_scale_changed(_, value) + if value == nil then + return + end + local dwidth = mp.get_property("dwidth") + local dheight = mp.get_property("dheight") + if dwidth ~= nil and dheight ~= nil and dwidth == last_dwidth and dheight == last_dheight then + -- If video size stayed the same, then the scaling was probably done by the user to we save it + local current_window_scale = mp.get_property("current-window-scale") + prefered_win_width = dwidth * current_window_scale + end +end + +local function on_dwidth_change(_, value) + if value == nil then + return + end + local dwidth = mp.get_property("dwidth") + local dheight = mp.get_property("dheight") + if dwidth == nil or dheight == nil then + return + end + + -- Save new video size + last_dwidth = dwidth + last_dheight = dheight + + if prefered_win_width == nil then + return + end + -- Scale window to prefered width + local current_window_scale = mp.get_property("current-window-scale") + local window_width = dwidth * current_window_scale + local new_scale = current_window_scale + if prefered_win_width ~= nil and math.abs(prefered_win_width - window_width) > 2 then + new_scale = prefered_win_width / dwidth + end + + if new_scale ~= current_window_scale then + mp.set_property("window-scale", new_scale) + end +end + + +-- register script message to show menu +mp.register_script_message("toggle-upnext-menu", +function() + if destroyer ~= nil then + destroyer() + else + show_menu() + end +end) + +-- keybind to launch menu +mp.add_key_binding(opts.toggle_menu_binding, "upnext-menu", show_menu) + +if opts.auto_add then + mp.register_event("file-loaded", on_file_loaded) +end + +if opts.restore_window_width then + mp.observe_property("current-window-scale", "number", on_window_scale_changed) + mp.observe_property("dwidth", "number", on_dwidth_change) +end