From 644fee20e454ce9d9c82adac49d4e98bef82a73d Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Thu, 14 May 2026 17:35:07 +0200 Subject: [PATCH] mpv settings --- input.conf | 4 + mpv.conf | 11 + scripts/freezer.lua | 35 + scripts/playlistmanager.lua | 1442 ++++++++++++++++++ scripts/webm.lua | 2763 +++++++++++++++++++++++++++++++++++ webm.lua | 2719 ++++++++++++++++++++++++++++++++++ 6 files changed, 6974 insertions(+) create mode 100644 input.conf create mode 100644 mpv.conf create mode 100644 scripts/freezer.lua create mode 100644 scripts/playlistmanager.lua create mode 100644 scripts/webm.lua create mode 100644 webm.lua diff --git a/input.conf b/input.conf new file mode 100644 index 0000000..a659321 --- /dev/null +++ b/input.conf @@ -0,0 +1,4 @@ +WHEEL_UP seek 10 # seek 10 seconds forward +WHEEL_DOWN seek -10 # seek 10 seconds backward +k playlist-shuffle +MBTN_MID mute diff --git a/mpv.conf b/mpv.conf new file mode 100644 index 0000000..998c63f --- /dev/null +++ b/mpv.conf @@ -0,0 +1,11 @@ +profile=gpu-hq +ontop +loop-file=inf +no-keepaspect-window +volume=50 +window-scale=1 +geometry=50%:50% +#geometry=740x480+740+480 +script-opts-append=ytdl_hook-ytdl_path=yt-dlp +ytdl-format="bestvideo[ext=mp4][vcodec^=avc][height<=?1080]+bestaudio/best" +##ytdl-format=bestvideo[height<=1080]+bestaudio diff --git a/scripts/freezer.lua b/scripts/freezer.lua new file mode 100644 index 0000000..bfe1e3b --- /dev/null +++ b/scripts/freezer.lua @@ -0,0 +1,35 @@ +--[[ + mpv_geometry_freezer.lua + Sets the geometry property when window size changes, + avoiding Windows' maximized windows detaching. + - AMM +]]-- +local msg = require 'mp.msg' + +local UPDATE_INTERVAL = 0.5 +local screen_w, screen_h = mp.get_osd_size() + +mp.add_periodic_timer(UPDATE_INTERVAL, function() + local new_screen_w, new_screen_h = mp.get_osd_size() + if new_screen_w ~= screen_w or new_screen_h ~= screen_h then + screen_w = new_screen_w + screen_h = new_screen_h + + local geom_string = ("%dx%d"):format(screen_w, screen_h) + msg.debug("OSD resized: " .. geom_string .. ", setting geometry property") + mp.set_property_native("geometry", geom_string) + end +end) +# +local size_changed = false + +mp.register_idle(function() + if not size_changed then return end + local ww, wh = mp.get_osd_size() + if not ww or ww <= 0 or not wh or wh <= 0 then return end + mp.set_property("geometry", string.format("%dx%d", ww, wh)) + size_changed = false +end) + +mp.observe_property("osd-width", "native", function() size_changed = true end) +mp.observe_property("osd-height", "native", function() size_changed = true end) diff --git a/scripts/playlistmanager.lua b/scripts/playlistmanager.lua new file mode 100644 index 0000000..262c07a --- /dev/null +++ b/scripts/playlistmanager.lua @@ -0,0 +1,1442 @@ +local settings = { + + -- #### FUNCTIONALITY SETTINGS + + --navigation keybindings force override only while playlist is visible + --if "no" then you can display the playlist by any of the navigation keys + dynamic_binds = true, + + -- to bind multiple keys separate them by a space + + -- main key to show playlist + key_showplaylist = "SHIFT+ENTER", + + -- display playlist while key is held down + key_peek_at_playlist = "", + + -- dynamic keys + key_moveup = "UP", + key_movedown = "DOWN", + key_movepageup = "PGUP", + key_movepagedown = "PGDWN", + key_movebegin = "HOME", + key_moveend = "END", + key_selectfile = "RIGHT LEFT", + key_unselectfile = "", + key_playfile = "ENTER", + key_removefile = "BS", + key_closeplaylist = "ESC SHIFT+ENTER", + + -- extra functionality keys + key_sortplaylist = "", + key_shuffleplaylist = "", + key_reverseplaylist = "", + key_loadfiles = "", + key_saveplaylist = "", + + --replaces matches on filenames based on extension, put as empty string to not replace anything + --replace rules are executed in provided order + --replace rule key is the pattern and value is the replace value + --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial + --'all' will match any extension or protocol if it has one + --uses json and parses it into a lua table to be able to support .conf file + + filename_replace = [[ + [ + { + "protocol": { "all": true }, + "rules": [ + { "%%(%x%x)": "hex_to_char" } + ] + } + ] + ]], + +--[=====[ START OF SAMPLE REPLACE - Remove this line to use it + --Sample replace: replaces underscore to space on all files + --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space + filename_replace = [[ + [ + { + "ext": { "all": true}, + "rules": [ + { "_" : " " } + ] + },{ + "ext": { "mp4": true, "mkv": true }, + "rules": [ + { "^(.+)%..+$": "%1" }, + { "%s*[%[%(].-[%]%)]%s*": "" }, + { "(%w)%.(%w)": "%1 %2" } + ] + },{ + "protocol": { "http": true, "https": true }, + "rules": [ + { "^%a+://w*%.?": "" } + ] + } + ] + ]], +--END OF SAMPLE REPLACE ]=====] + + --json array of filetypes to search from directory + loadfiles_filetypes = [[ + [ + "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp", + "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus", + "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp" + ] + ]], + + --loadfiles at startup if 1 or more items in playlist + loadfiles_on_start = false, + -- loadfiles from working directory on idle startup + loadfiles_on_idle_start = false, + --always put loaded files after currently playing file + loadfiles_always_append = false, + + --sort playlist when files are added to playlist + sortplaylist_on_file_add = false, + + --default sorting method, must be one of: "name", "date-asc", "date-desc", "size-asc", "size-desc". + default_sort = "name", + + --"linux | windows | auto" + system = "auto", + + --Use ~ for home directory. Leave as empty to use mpv/playlists + playlist_savepath = "", + + -- constant filename to save playlist as. Note that it will override existing playlist. Leave empty for generated name. + playlist_save_filename = "", + + --save playlist automatically after current file was unloaded + save_playlist_on_file_end = false, + + + --show playlist or filename every time a new file is loaded + --2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing + --instead of using this you can also call script-message playlistmanager show playlist/filename + --ex. KEY playlist-next ; script-message playlistmanager show playlist + show_playlist_on_fileload = 0, + + --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.) + --has the sideeffect of moving cursor if file happens to change when navigating + --good side is cursor always following current file when going back and forth files with playlist-next/prev + sync_cursor_on_load = true, + + --allow the playlist cursor to loop from end to start and vice versa + loop_cursor = true, + + --youtube-dl executable for title resolving if enabled, probably "youtube-dl" or "yt-dlp", can be absolute path + youtube_dl_executable = "youtube-dl", + + -- allow playlistmanager to write watch later config when navigating between files + allow_write_watch_later_config = true, + + -- reset cursor navigation when closing or opening playlist + reset_cursor_on_close = true, + reset_cursor_on_open = true, + + --#### VISUAL SETTINGS + + --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename. + prefer_titles = "url", + + --call youtube-dl to resolve the titles of urls in the playlist + resolve_url_titles = false, + + --call ffprobe to resolve the titles of local files in the playlist (if they exist in the metadata) + resolve_local_titles = false, + + -- timeout in seconds for url title resolving + resolve_title_timeout = 15, + + -- how many url titles can be resolved at a time. Higher number might lead to stutters. + concurrent_title_resolve_limit = 10, + + --osd timeout on inactivity in seconds, use 0 for no timeout + playlist_display_timeout = 0, + + -- when peeking at playlist, show playlist at the very least for display timeout + peek_respect_display_timeout = false, + + -- the maximum amount of lines playlist will render. Optimal value depends on font/video size etc. + showamount = 9, + + --font size scales by window, if false requires larger font and padding sizes + scale_playlist_by_window=true, + --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 + style_ass_tags = "{}", + --paddings from top left corner + text_padding_x = 10, + text_padding_y = 30, + + --screen dim when menu is open 0.0 - 1.0 (0 is no dim, 1 is black) + curtain_opacity=0, + + --set title of window with stripped name + set_title_stripped = false, + title_prefix = "", + title_suffix = " - mpv", + + --slice long filenames, and how many chars to show + slice_longfilenames = false, + slice_longfilenames_amount = 70, + + --Playlist header template + --%mediatitle or %filename = title or name of playing file + --%pos = position of playing file + --%cursor = position of navigation + --%plen = playlist length + --%N = newline + playlist_header = "[%cursor/%plen]", + + --Playlist file templates + --%pos = position of file with leading zeros + --%name = title or name of file + --%N = newline + --you can also use the ass tags mentioned above. For example: + -- selected_file="{\\c&HFF00FF&}➔ %name" | to add a color for selected file. However, if you + -- use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20) + normal_file = "○ %name", + hovered_file = "● %name", + selected_file = "➔ %name", + playing_file = "▷ %name", + playing_hovered_file = "▶ %name", + playing_selected_file = "➤ %name", + + + -- what to show when playlist is truncated + playlist_sliced_prefix = "...", + playlist_sliced_suffix = "...", + + --output visual feedback to OSD for tasks + display_osd_feedback = true, +} +local opts = require("mp.options") +opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end) + +local utils = require("mp.utils") +local msg = require("mp.msg") +local assdraw = require("mp.assdraw") + + +--check os +if settings.system=="auto" then + local o = {} + if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then + settings.system = "windows" + else + settings.system = "linux" + end +end + +--global variables +local playlist_visible = false +local strippedname = nil +local path = nil +local directory = nil +local filename = nil +local pos = 0 +local plen = 0 +local cursor = 0 +--table for saved media titles for later if we prefer them +local title_table = {} +-- table for urls and local file paths that we have requested to be resolved to titles +local requested_titles = {} + +local filetype_lookup = {} + +function update_opts(changelog) + msg.verbose('updating options') + + --parse filename json + if changelog.filename_replace then + if(settings.filename_replace~="") then + settings.filename_replace = utils.parse_json(settings.filename_replace) + else + settings.filename_replace = false + end + end + + --parse loadfiles json + if changelog.loadfiles_filetypes then + settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes) + + filetype_lookup = {} + --create loadfiles set + for _, ext in ipairs(settings.loadfiles_filetypes) do + filetype_lookup[ext] = true + end + end + + if changelog.resolve_url_titles then + resolve_titles() + end + + if changelog.resolve_local_titles then + resolve_titles() + end + + if changelog.playlist_display_timeout then + keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) + keybindstimer:kill() + end + + if playlist_visible then showplaylist() end +end + +update_opts({filename_replace = true, loadfiles_filetypes = true}) + +local sort_mode = 0 +if settings.default_sort == 'name' then + sort_mode = 1 +elseif settings.default_sort == 'date-asc' then + sort_mode = 2 +elseif settings.default_sort == 'date-desc' then + sort_mode = 3 +elseif settings.default_sort == 'size-asc' then + sort_mode = 4 +elseif settings.default_sort == 'size-desc' then + sort_mode = 5 +end + +function is_protocol(path) + return type(path) == 'string' and path:match('^%a[%a%d-_]+://') ~= nil +end + +function on_file_loaded() + refresh_globals() + filename = mp.get_property("filename") + path = mp.get_property('path') + local media_title = mp.get_property("media-title") + if is_protocol(path) and not title_table[path] and path ~= media_title then + title_table[path] = media_title + end + + if settings.sync_cursor_on_load then + cursor=pos + --refresh playlist if cursor moved + if playlist_visible then draw_playlist() end + end + + strippedname = stripfilename(mp.get_property('media-title')) + if settings.show_playlist_on_fileload == 2 then + showplaylist() + elseif settings.show_playlist_on_fileload == 1 then + mp.commandv('show-text', strippedname) + end + if settings.set_title_stripped then + mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix) + end +end + +function on_start_file() + refresh_globals() + filename = mp.get_property("filename") + path = mp.get_property('path') + --if not a url then join path with working directory + if not is_protocol(path) then + path = utils.join_path(mp.get_property('working-directory'), path) + directory = utils.split_path(path) + else + directory = nil + end + + if settings.loadfiles_on_start and plen == 1 then + local ext = filename:match("%.([^%.]+)$") + -- a directory or playlist has been loaded, let's not do anything as mpv will expand it into files + if ext and filetype_lookup[ext:lower()] then + msg.info("Loading files from playing files directory") + playlist() + end + end +end + +function on_end_file() + if settings.save_playlist_on_file_end then save_playlist() end + strippedname = nil + path = nil + directory = nil + filename = nil + if playlist_visible then showplaylist() end +end + +function refresh_globals() + pos = mp.get_property_number('playlist-pos', 0) + plen = mp.get_property_number('playlist-count', 0) +end + +function escapepath(dir, escapechar) + return string.gsub(dir, escapechar, '\\'..escapechar) +end + +function replace_table_has_value(value, valid_values) + if value == nil or valid_values == nil then + return false + end + return valid_values['all'] or valid_values[value] +end + +local filename_replace_functions = { + --decode special characters in url + hex_to_char = function(x) return string.char(tonumber(x, 16)) end +} + +--strip a filename based on its extension or protocol according to rules in settings +function stripfilename(pathfile, media_title) + if pathfile == nil then return '' end + local ext = pathfile:match("%.([^%.]+)$") + local protocol = pathfile:match("^(%a%a+)://") + if not ext then ext = "" end + local tmp = pathfile + if settings.filename_replace and not media_title then + for k,v in ipairs(settings.filename_replace) do + if replace_table_has_value(ext, v['ext']) or replace_table_has_value(protocol, v['protocol']) then + for ruleindex, indexrules in ipairs(v['rules']) do + for rule, override in pairs(indexrules) do + override = filename_replace_functions[override] or override + tmp = tmp:gsub(rule, override) + end + end + end + end + end + if settings.slice_longfilenames and tmp:len()>settings.slice_longfilenames_amount+5 then + tmp = tmp:sub(1, settings.slice_longfilenames_amount).." ..." + end + return tmp +end + +--gets the file info of an item +function get_file_info(item) + local path = mp.get_property('playlist/' .. item - 1 .. '/filename') + if is_protocol(path) then return {} end + local file_info = utils.file_info(path) + if not file_info then + msg.warn('failed to read file info for', path) + return {} + end + + return file_info +end + +--gets a nicename of playlist entry at 0-based position i +function get_name_from_index(i, notitle) + refresh_globals() + if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end + local _, name = nil + local title = mp.get_property('playlist/'..i..'/title') + local name = mp.get_property('playlist/'..i..'/filename') + + local should_use_title = settings.prefer_titles == 'all' or is_protocol(name) and settings.prefer_titles == 'url' + --check if file has a media title stored or as property + if not title and should_use_title then + local mtitle = mp.get_property('media-title') + if i == pos and mp.get_property('filename') ~= mtitle then + if not title_table[name] then + title_table[name] = mtitle + end + title = mtitle + elseif title_table[name] then + title = title_table[name] + end + end + + --if we have media title use a more conservative strip + if title and not notitle and should_use_title then + -- Escape a string for verbatim display on the OSD + -- Ref: https://github.com/mpv-player/mpv/blob/94677723624fb84756e65c8f1377956667244bc9/player/lua/stats.lua#L145 + return stripfilename(title, true):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h") + end + + --remove paths if they exist, keeping protocols for stripping + if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then + _, name = utils.split_path(name) + end + return stripfilename(name):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h") +end + +function parse_header(string) + local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%") + local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%") + return string:gsub("%%N", "\\N") + :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1) + :gsub("%%plen", mp.get_property("playlist-count")) + :gsub("%%cursor", cursor+1) + :gsub("%%mediatitle", esc_title) + :gsub("%%filename", esc_file) + -- undo name escape + :gsub("%%%%", "%%") +end + +function parse_filename(string, name, index) + local base = tostring(plen):len() + local esc_name = stripfilename(name):gsub("%%", "%%%%") + return string:gsub("%%N", "\\N") + :gsub("%%pos", string.format("%0"..base.."d", index+1)) + :gsub("%%name", esc_name) + -- undo name escape + :gsub("%%%%", "%%") +end + +function parse_filename_by_index(index) + local template = settings.normal_file + + local is_idle = mp.get_property_native('idle-active') + local position = is_idle and -1 or pos + + if index == position then + if index == cursor then + if selection then + template = settings.playing_selected_file + else + template = settings.playing_hovered_file + end + else + template = settings.playing_file + end + elseif index == cursor then + if selection then + template = settings.selected_file + else + template = settings.hovered_file + end + end + + return parse_filename(template, get_name_from_index(index), index) +end + + +function draw_playlist() + refresh_globals() + local ass = assdraw.ass_new() + + local _, _, a = mp.get_osd_size() + local h = 360 + local w = h * a + + if settings.curtain_opacity ~= nil and settings.curtain_opacity ~= 0 and settings.curtain_opacity < 1.0 then + -- curtain dim from https://github.com/christoph-heinrich/mpv-quality-menu/blob/501794bfbef468ee6a61e54fc8821fe5cd72c4ed/quality-menu.lua#L699-L707 + local alpha = 255 - math.ceil(255 * settings.curtain_opacity) + ass.text = string.format('{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}', alpha) + ass:draw_start() + ass:rect_cw(0, 0, w, h) + ass:draw_stop() + ass:new_event() + end + + ass:append(settings.style_ass_tags) + + -- TODO: padding should work even on different osd alignments + if mp.get_property("osd-align-x") == "left" and mp.get_property("osd-align-y") == "top" then + ass:pos(settings.text_padding_x, settings.text_padding_y) + end + + if settings.playlist_header ~= "" then + ass:append(parse_header(settings.playlist_header).."\\N") + end + + -- (visible index, playlist index) pairs of playlist entries that should be rendered + local visible_indices = {} + + local one_based_cursor = cursor + 1 + table.insert(visible_indices, one_based_cursor) + + local offset = 1; + local visible_indices_length = 1; + while visible_indices_length < settings.showamount and visible_indices_length < plen do + -- add entry for offset steps below the cursor + local below = one_based_cursor + offset + if below <= plen then + table.insert(visible_indices, below) + visible_indices_length = visible_indices_length + 1; + end + + -- add entry for offset steps above the cursor + -- also need to double check that there is still space, this happens if we have even numbered limit + local above = one_based_cursor - offset + if above >= 1 and visible_indices_length < settings.showamount and visible_indices_length < plen then + table.insert(visible_indices, 1, above) + visible_indices_length = visible_indices_length + 1; + end + + offset = offset + 1 + end + + -- both indices are 1 based + for display_index, playlist_index in pairs(visible_indices) do + if display_index == 1 and playlist_index ~= 1 then + ass:append(settings.playlist_sliced_prefix.."\\N") + elseif display_index == settings.showamount and playlist_index ~= plen then + ass:append(settings.playlist_sliced_suffix) + else + -- parse_filename_by_index expects 0 based index + ass:append(parse_filename_by_index(playlist_index - 1).."\\N") + end + end + + if settings.scale_playlist_by_window then w,h = 0, 0 end + mp.set_osd_ass(w, h, ass.text) +end + +local peek_display_timer = nil +local peek_button_pressed = false + +function peek_timeout() + peek_display_timer:kill() + if not peek_button_pressed and not playlist_visible then + remove_keybinds() + end +end + +function handle_complex_playlist_toggle(table) + local event = table["event"] + if event == "press" then + msg.error("Complex key event not supported. Falling back to normal playlist display.") + showplaylist() + elseif event == "down" then + showplaylist(1000000) + if settings.peek_respect_display_timeout then + peek_button_pressed = true + peek_display_timer = mp.add_periodic_timer(settings.playlist_display_timeout, peek_timeout) + end + elseif event == "up" then + -- set playlist state to not visible, doesn't actually hide playlist yet + -- this will allow us to check if other functionality has rendered playlist before removing binds + playlist_visible = false + + function remove_keybinds_after_timeout() + -- if playlist is still not visible then lets actually hide it + -- this lets other keys that interupt the peek to render playlist without peek up event closing it + if not playlist_visible then + remove_keybinds() + end + end + + if settings.peek_respect_display_timeout then + peek_button_pressed = false + if not peek_display_timer:is_enabled() then + mp.add_timeout(0.01, remove_keybinds_after_timeout) + end + else + -- use small delay to let dynamic binds run before keys are potentially unbound + mp.add_timeout(0.01, remove_keybinds_after_timeout) + end + end +end + +function toggle_playlist(show_function) + local show = show_function or showplaylist + if playlist_visible then + remove_keybinds() + else + -- toggle always shows without timeout + show(0) + end +end + +function showplaylist(duration) + refresh_globals() + if plen == 0 then return end + if not playlist_visible and settings.reset_cursor_on_open then + resetcursor() + end + + playlist_visible = true + add_keybinds() + + draw_playlist() + keybindstimer:kill() + + local dur = duration or settings.playlist_display_timeout + if dur > 0 then + keybindstimer = mp.add_periodic_timer(dur, remove_keybinds) + end +end + +function showplaylist_non_interactive(duration) + refresh_globals() + if plen == 0 then return end + if not playlist_visible and settings.reset_cursor_on_open then + resetcursor() + end + playlist_visible = true + draw_playlist() + keybindstimer:kill() + + local dur = duration or settings.playlist_display_timeout + if dur > 0 then + keybindstimer = mp.add_periodic_timer(dur, remove_keybinds) + end +end + +selection=nil +function selectfile() + refresh_globals() + if plen == 0 then return end + if not selection then + selection=cursor + else + selection=nil + end + showplaylist() +end + +function unselectfile() + selection=nil + showplaylist() +end + +function resetcursor() + selection = nil + cursor = mp.get_property_number('playlist-pos', 1) +end + +function removefile() + refresh_globals() + if plen == 0 then return end + selection = nil + if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end + mp.commandv("playlist-remove", cursor) + if cursor==plen-1 then cursor = cursor - 1 end + if plen == 1 then + remove_keybinds() + else + showplaylist() + end +end + +function moveup() + refresh_globals() + if plen == 0 then return end + if cursor~=0 then + if selection then mp.commandv("playlist-move", cursor,cursor-1) end + cursor = cursor-1 + elseif settings.loop_cursor then + if selection then mp.commandv("playlist-move", cursor,plen) end + cursor = plen-1 + end + showplaylist() +end + +function movedown() + refresh_globals() + if plen == 0 then return end + if cursor ~= plen-1 then + if selection then mp.commandv("playlist-move", cursor,cursor+2) end + cursor = cursor + 1 + elseif settings.loop_cursor then + if selection then mp.commandv("playlist-move", cursor,0) end + cursor = 0 + end + showplaylist() +end + +function movepageup() + refresh_globals() + if plen == 0 or cursor == 0 then return end + local prev_cursor = cursor + cursor = cursor - settings.showamount + if cursor < 0 then cursor = 0 end + if selection then mp.commandv("playlist-move", prev_cursor, cursor) end + showplaylist() +end + +function movepagedown() + refresh_globals() + if plen == 0 or cursor == plen-1 then return end + local prev_cursor = cursor + cursor = cursor + settings.showamount + if cursor >= plen then cursor = plen-1 end + if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end + showplaylist() +end + +function movebegin() + refresh_globals() + if plen == 0 or cursor == 0 then return end + local prev_cursor = cursor + cursor = 0 + if selection then mp.commandv("playlist-move", prev_cursor, cursor) end + showplaylist() +end + +function moveend() + refresh_globals() + if plen == 0 or cursor == plen-1 then return end + local prev_cursor = cursor + cursor = plen-1 + if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end + showplaylist() +end + +function write_watch_later(force_write) + if settings.allow_write_watch_later_config then + if mp.get_property_bool("save-position-on-quit") or force_write then + mp.command("write-watch-later-config") + end + end +end + +function playlist_next(force_write) + write_watch_later(force_write) + mp.commandv("playlist-next", "weak") +end + +function playlist_prev(force_write) + write_watch_later(force_write) + mp.commandv("playlist-prev", "weak") +end + +function playfile() + refresh_globals() + if plen == 0 then return end + selection = nil + local is_idle = mp.get_property_native('idle-active') + if cursor ~= pos or is_idle then + write_watch_later() + mp.set_property("playlist-pos", cursor) + else + if cursor~=plen-1 then + cursor = cursor + 1 + end + write_watch_later() + mp.commandv("playlist-next", "weak") + end + if settings.show_playlist_on_fileload ~= 2 then + remove_keybinds() + end +end + +function file_filter(filenames) + local files = {} + for i = 1, #filenames do + local file = filenames[i] + local ext = file:match('%.([^%.]+)$') + if ext and filetype_lookup[ext:lower()] then + table.insert(files, file) + end + end + return files +end + +function get_playlist_filenames_set() + local filenames = {} + for n=0,plen-1,1 do + local filename = mp.get_property('playlist/'..n..'/filename') + local _, file = utils.split_path(filename) + filenames[file] = true + end + return filenames +end + +--Creates a playlist of all files in directory, will keep the order and position +--For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it +function playlist(force_dir) + refresh_globals() + if not directory and plen > 0 then return end + local hasfile = true + if plen == 0 then + hasfile = false + dir = mp.get_property('working-directory') + else + dir = directory + end + + if dir == "." then dir = "" end + if force_dir then dir = force_dir end + + local files = file_filter(utils.readdir(dir, "files")) + table.sort(files, alphanumsort) + + if files == nil then + msg.verbose("no files in directory") + return + end + + local filenames = get_playlist_filenames_set() + local c, c2 = 0,0 + if files then + local cur = false + local filename = mp.get_property("filename") + for _, file in ipairs(files) do + if file == nil or file[1] == "." then + break + end + local appendstr = "append" + if not hasfile then + cur = true + appendstr = "append-play" + hasfile = true + end + if filename == file then + cur = true + elseif filenames[file] then + -- skip files already in playlist + elseif cur == true or settings.loadfiles_always_append then + mp.commandv("loadfile", utils.join_path(dir, file), appendstr) + msg.info("Appended to playlist: " .. file) + c2 = c2 + 1 + else + mp.commandv("loadfile", utils.join_path(dir, file), appendstr) + msg.info("Prepended to playlist: " .. file) + mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1, c) + c = c + 1 + end + end + if c2 > 0 or c>0 then + msg.info("Added "..c + c2.." files to playlist") + else + msg.info("No additional files found") + end + cursor = mp.get_property_number('playlist-pos', 1) + else + msg.error("Could not scan for files: "..(error or "")) + end + refresh_globals() + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + if c2 > 0 or c>0 then + mp.osd_message("Added "..c + c2.." files to playlist") + else + mp.osd_message("No additional files found") + end + end + return c + c2 +end + +function parse_home(path) + if not path:find("^~") then + return path + end + local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") + 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.error("Couldn't find home dir.") + return nil + end + end + local result = path:gsub("^~", home_dir) + return result +end + +local interactive_save = false +function activate_playlist_save() + if interactive_save then + remove_keybinds() + mp.command("script-message playlistmanager-save-interactive \"start interactive filenaming process\"") + else + save_playlist() + end +end + +--saves the current playlist into a m3u file +function save_playlist(filename) + local length = mp.get_property_number('playlist-count', 0) + if length == 0 then return end + + --get playlist save path + local savepath + if settings.playlist_savepath == nil or settings.playlist_savepath == "" then + savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists" + else + savepath = parse_home(settings.playlist_savepath) + if savepath == nil then return end + end + + --create savepath if it doesn't exist + if utils.readdir(savepath) == nil then + local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath} + local unix_args = { 'mkdir', savepath } + local args = settings.system == 'windows' and windows_args or unix_args + local res = utils.subprocess({ args = args, cancellable = false }) + if res.status ~= 0 then + msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown")) + return + end + end + + local name = filename + if name == nil then + if settings.playlist_save_filename == nil or settings.playlist_save_filename == "" then + local date = os.date("*t") + local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec) + + name = datestring.."_playlist-size_"..length..".m3u" + else + name = settings.playlist_save_filename + end + end + + local savepath = utils.join_path(savepath, name) + local file, err = io.open(savepath, "w") + if not file then + msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown")) + else + file:write("#EXTM3U\n") + local i=0 + while i < length do + local pwd = mp.get_property("working-directory") + local filename = mp.get_property('playlist/'..i..'/filename') + local fullpath = filename + if not is_protocol(filename) then + fullpath = utils.join_path(pwd, filename) + end + local title = mp.get_property('playlist/'..i..'/title') or title_table[filename] + if title then + file:write("#EXTINF:,"..title.."\n") + end + file:write(fullpath, "\n") + i=i+1 + end + local saved_msg = "Playlist written to: "..savepath + if settings.display_osd_feedback then mp.osd_message(saved_msg) end + msg.info(saved_msg) + file:close() + end +end + +function alphanumsort(a, b) + local function padnum(d) + local dec, n = string.match(d, "(%.?)0*(.+)") + return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) + end + return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b) + < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a) +end + +-- fast sort algo from https://github.com/zsugabubus/dotfiles/blob/master/.config/mpv/scripts/playlist-filtersort.lua +function sortplaylist(startover) + local playlist = mp.get_property_native('playlist') + if #playlist < 2 then return end + + local order = {} + for i=1, #playlist do + order[i] = i + playlist[i].string = get_name_from_index(i - 1, true) + end + + table.sort(order, function(a, b) + if sort_mode == 1 then + return alphanumsort(playlist[a].string, playlist[b].string) + elseif sort_mode == 2 then + return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0) + elseif sort_mode == 3 then + return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0) + elseif sort_mode == 4 then + return (get_file_info(a).size or 0) < (get_file_info(b).size or 0) + elseif sort_mode == 5 then + return (get_file_info(a).size or 0) > (get_file_info(b).size or 0) + end + end) + + for i=1, #playlist do + playlist[order[i]].new_pos = i + end + + for i=1, #playlist do + while true do + local j = playlist[i].new_pos + if i == j then + break + end + mp.commandv('playlist-move', (i) - 1, (j + 1) - 1) + mp.commandv('playlist-move', (j - 1) - 1, (i) - 1) + playlist[j], playlist[i] = playlist[i], playlist[j] + end + end + + for i = 1, #playlist do + local filename = mp.get_property('playlist/' .. i - 1 .. '/filename') + local ext = filename:match("%.([^%.]+)$") + if not ext or not filetype_lookup[ext:lower()] then + --move the directory to the end of the playlist + mp.commandv('playlist-move', i - 1, #playlist) + end + end + + cursor = mp.get_property_number('playlist-pos', 0) + if startover then + mp.set_property('playlist-pos', 0) + end + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + mp.osd_message("Playlist sorted") + end +end + +function reverseplaylist() + local length = mp.get_property_number('playlist-count', 0) + if length < 2 then return end + for outer=1, length-1, 1 do + mp.commandv('playlist-move', outer, 0) + end + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + mp.osd_message("Playlist reversed") + end +end + +function shuffleplaylist() + refresh_globals() + if plen < 2 then return end + mp.command("playlist-shuffle") + math.randomseed(os.time()) + mp.commandv("playlist-move", pos, math.random(0, plen-1)) + + local playlist = mp.get_property_native('playlist') + for i = 1, #playlist do + local filename = mp.get_property('playlist/' .. i - 1 .. '/filename') + local ext = filename:match("%.([^%.]+)$") + if not ext or not filetype_lookup[ext:lower()] then + --move the directory to the end of the playlist + mp.commandv('playlist-move', i - 1, #playlist) + end + end + + mp.set_property('playlist-pos', 0) + refresh_globals() + if playlist_visible then + showplaylist() + elseif settings.display_osd_feedback then + mp.osd_message("Playlist shuffled") + end +end + +function bind_keys(keys, name, func, opts) + if keys == nil or keys == "" then + mp.add_key_binding(keys, name, func, opts) + return + end + local i = 1 + for key in keys:gmatch("[^%s]+") do + local prefix = i == 1 and '' or i + mp.add_key_binding(key, name..prefix, func, opts) + i = i + 1 + end +end + +function bind_keys_forced(keys, name, func, opts) + if keys == nil or keys == "" then + mp.add_forced_key_binding(keys, name, func, opts) + return + end + local i = 1 + for key in keys:gmatch("[^%s]+") do + local prefix = i == 1 and '' or i + mp.add_forced_key_binding(key, name..prefix, func, opts) + i = i + 1 + end +end + +function unbind_keys(keys, name) + if keys == nil or keys == "" then + mp.remove_key_binding(name) + return + end + local i = 1 + for key in keys:gmatch("[^%s]+") do + local prefix = i == 1 and '' or i + mp.remove_key_binding(name..prefix) + i = i + 1 + end +end + +function add_keybinds() + bind_keys_forced(settings.key_moveup, 'moveup', moveup, "repeatable") + bind_keys_forced(settings.key_movedown, 'movedown', movedown, "repeatable") + bind_keys_forced(settings.key_movepageup, 'movepageup', movepageup, "repeatable") + bind_keys_forced(settings.key_movepagedown, 'movepagedown', movepagedown, "repeatable") + bind_keys_forced(settings.key_movebegin, 'movebegin', movebegin, "repeatable") + bind_keys_forced(settings.key_moveend, 'moveend', moveend, "repeatable") + bind_keys_forced(settings.key_selectfile, 'selectfile', selectfile) + bind_keys_forced(settings.key_unselectfile, 'unselectfile', unselectfile) + bind_keys_forced(settings.key_playfile, 'playfile', playfile) + bind_keys_forced(settings.key_removefile, 'removefile', removefile, "repeatable") + bind_keys_forced(settings.key_closeplaylist, 'closeplaylist', remove_keybinds) +end + +function remove_keybinds() + keybindstimer:kill() + keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) + keybindstimer:kill() + mp.set_osd_ass(0, 0, "") + playlist_visible = false + if settings.reset_cursor_on_close then + resetcursor() + end + if settings.dynamic_binds then + unbind_keys(settings.key_moveup, 'moveup') + unbind_keys(settings.key_movedown, 'movedown') + unbind_keys(settings.key_movepageup, 'movepageup') + unbind_keys(settings.key_movepagedown, 'movepagedown') + unbind_keys(settings.key_movebegin, 'movebegin') + unbind_keys(settings.key_moveend, 'moveend') + unbind_keys(settings.key_selectfile, 'selectfile') + unbind_keys(settings.key_unselectfile, 'unselectfile') + unbind_keys(settings.key_playfile, 'playfile') + unbind_keys(settings.key_removefile, 'removefile') + unbind_keys(settings.key_closeplaylist, 'closeplaylist') + end +end + +keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) +keybindstimer:kill() + +if not settings.dynamic_binds then + add_keybinds() +end + +if settings.loadfiles_on_idle_start and mp.get_property_number('playlist-count', 0) == 0 then + playlist() +end + +mp.observe_property('playlist-count', "number", function(_, plcount) + --if we promised to listen and sort on playlist size increase do it + if settings.sortplaylist_on_file_add and (plcount > plen) then + msg.info("Added files will be automatically sorted") + refresh_globals() + sortplaylist() + end + if playlist_visible then showplaylist() end + resolve_titles() +end) + + +url_request_queue = {} +function url_request_queue.push(item) table.insert(url_request_queue, item) end +function url_request_queue.pop() return table.remove(url_request_queue, 1) end +local url_titles_to_fetch = url_request_queue +local ongoing_url_requests = {} + +function url_fetching_throttler() + if #url_titles_to_fetch == 0 then + url_title_fetch_timer:kill() + end + + local ongoing_url_requests_count = 0 + for _, ongoing in pairs(ongoing_url_requests) do + if ongoing then + ongoing_url_requests_count = ongoing_url_requests_count + 1 + end + end + + -- start resolving some url titles if there is available slots + local amount_to_fetch = math.max(0, settings.concurrent_title_resolve_limit - ongoing_url_requests_count) + for index=1,amount_to_fetch,1 do + local file = url_titles_to_fetch.pop() + if file then + ongoing_url_requests[file] = true + resolve_ytdl_title(file) + end + end +end + +url_title_fetch_timer = mp.add_periodic_timer(0.1, url_fetching_throttler) +url_title_fetch_timer:kill() + +local_request_queue = {} +function local_request_queue.push(item) table.insert(local_request_queue, item) end +function local_request_queue.pop() return table.remove(local_request_queue, 1) end +local local_titles_to_fetch = local_request_queue +local ongoing_local_request = false + +-- this will only allow 1 concurrent local title resolve process +function local_fetching_throttler() + if not ongoing_local_request then + local file = local_titles_to_fetch.pop() + if file then + ongoing_local_request = true + resolve_ffprobe_title(file) + end + end +end + +function resolve_titles() + if settings.prefer_titles == 'none' then return end + if not settings.resolve_url_titles and not settings.resolve_local_titles then return end + + local length = mp.get_property_number('playlist-count', 0) + if length < 2 then return end + -- loop all items in playlist because we can't predict how it has changed + local added_urls = false + local added_local = false + for i=0,length - 1,1 do + local filename = mp.get_property('playlist/'..i..'/filename') + local title = mp.get_property('playlist/'..i..'/title') + if i ~= pos + and filename + and not title + and not title_table[filename] + and not requested_titles[filename] + then + requested_titles[filename] = true + if filename:match('^https?://') then + url_titles_to_fetch.push(filename) + added_urls = true + elseif settings.prefer_titles == "all" then + local_titles_to_fetch.push(filename) + added_local = true + end + end + end + if added_urls then + url_title_fetch_timer:resume() + end + if added_local then + local_fetching_throttler() + end +end + +function resolve_ytdl_title(filename) + local args = { + settings.youtube_dl_executable, + '--no-playlist', + '--flat-playlist', + '-sJ', + '--no-config', + filename, + } + local req = mp.command_native_async( + { + name = "subprocess", + args = args, + playback_only = false, + capture_stdout = true + }, + function (success, res) + ongoing_url_requests[filename] = false + if res.killed_by_us then + msg.verbose('Request to resolve url title ' .. filename .. ' timed out') + return + end + if res.status == 0 then + local json, err = utils.parse_json(res.stdout) + if not err then + local is_playlist = json['_type'] and json['_type'] == 'playlist' + local title = (is_playlist and '[playlist]: ' or '') .. json['title'] + msg.verbose(filename .. " resolved to '" .. title .. "'") + title_table[filename] = title + refresh_globals() + if playlist_visible then showplaylist() end + else + msg.error("Failed parsing json, reason: "..(err or "unknown")) + end + else + msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown")) + end + end + ) + + mp.add_timeout( + settings.resolve_title_timeout, + function() + mp.abort_async_command(req) + ongoing_url_requests[filename] = false + end + ) +end + +function resolve_ffprobe_title(filename) + local args = { "ffprobe", "-show_format", "-show_entries", "format=tags", "-loglevel", "quiet", filename } + local req = mp.command_native_async( + { + name = "subprocess", + args = args, + playback_only = false, + capture_stdout = true + }, + function (success, res) + ongoing_local_request = false + local_fetching_throttler() + if res.killed_by_us then + msg.verbose('Request to resolve local title ' .. filename .. ' timed out') + return + end + if res.status == 0 then + local title = string.match(res.stdout, "title=([^\n\r]+)") + if title then + msg.verbose(filename .. " resolved to '" .. title .. "'") + title_table[filename] = title + refresh_globals() + if playlist_visible then showplaylist() end + end + else + msg.error("Failed to resolve local title "..filename.." Error: "..(res.error or "unknown")) + end + end + ) +end + +--script message handler +function handlemessage(msg, value, value2) + if msg == "show" and value == "playlist" then + if value2 ~= "toggle" then + showplaylist(value2) + return + else + toggle_playlist(showplaylist) + return + end + end + if msg == "show" and value == "playlist-nokeys" then + if value2 ~= "toggle" then + showplaylist_non_interactive(value2) + return + else + toggle_playlist(showplaylist_non_interactive) + return + end + end + if msg == "show" and value == "filename" and strippedname and value2 then + mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return + end + if msg == "show" and value == "filename" and strippedname then + mp.commandv('show-text', strippedname ) ; return + end + if msg == "sort" then sortplaylist(value) ; return end + if msg == "shuffle" then shuffleplaylist() ; return end + if msg == "reverse" then reverseplaylist() ; return end + if msg == "loadfiles" then playlist(value) ; return end + if msg == "save" then save_playlist(value) ; return end + if msg == "playlist-next" then playlist_next(true) ; return end + if msg == "playlist-prev" then playlist_prev(true) ; return end + if msg == "enable-interactive-save" then interactive_save = true end + if msg == "close" then remove_keybinds() end +end + +mp.register_script_message("playlistmanager", handlemessage) + +bind_keys(settings.key_sortplaylist, "sortplaylist", function() + sort_mode = sort_mode + 1 + if sort_mode > 5 then sort_mode = 1 end + sortplaylist() +end) +bind_keys(settings.key_shuffleplaylist, "shuffleplaylist", shuffleplaylist) +bind_keys(settings.key_reverseplaylist, "reverseplaylist", reverseplaylist) +bind_keys(settings.key_loadfiles, "loadfiles", playlist) +bind_keys(settings.key_saveplaylist, "saveplaylist", activate_playlist_save) +bind_keys(settings.key_showplaylist, "showplaylist", showplaylist) +bind_keys( + settings.key_peek_at_playlist, + "peek_at_playlist", + handle_complex_playlist_toggle, + { complex=true } +) + +mp.register_event("start-file", on_start_file) +mp.register_event("file-loaded", on_file_loaded) +mp.register_event("end-file", on_end_file) diff --git a/scripts/webm.lua b/scripts/webm.lua new file mode 100644 index 0000000..d94530e --- /dev/null +++ b/scripts/webm.lua @@ -0,0 +1,2763 @@ +local mp = require("mp") +local assdraw = require("mp.assdraw") +local msg = require("mp.msg") +local utils = require("mp.utils") +local mpopts = require("mp.options") +local options = { + -- Defaults to shift+w + keybind = "W", + -- If empty, saves on the same directory of the playing video. + -- A starting "~" will be replaced by the home dir. + -- This field is delimited by double-square-brackets - [[ and ]] - instead of + -- quotes, because Windows users might run into a issue when using + -- backslashes as a path separator. Examples of valid inputs for this field + -- would be: [[]] (the default, empty value), [[C:\Users\John]] (on Windows), + -- and [[/home/john]] (on Unix-like systems eg. Linux). + output_directory = [[/home/nx/Videos/mpv-encodes]], + 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, Wwith 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 = "%T_%S-%E", + -- 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 = 9500, + -- 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/libopus) + -- 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 = 7, + -- 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 = "libopus" + 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) + ass:append("{\\bord2}{\\3c&H000000&}") + 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 find_path + find_path = function(startTime, endTime) + local path = mp.get_property('path') + if not path then + return nil, nil, nil, nil, nil + end + local is_stream = not file_exists(path) + local is_temporary = false + if is_stream then + if mp.get_property('file-format') == 'hls' then + path = utils.join_path(parse_directory('~'), 'cache_dump.ts') + mp.command_native({ + 'dump_cache', + seconds_to_time_string(startTime, false, true), + seconds_to_time_string(endTime + 5, false, true), + path + }) + endTime = endTime - startTime + startTime = 0 + is_temporary = true + end + end + return path, is_stream, is_temporary, startTime, endTime + end + local encode + encode = function(region, startTime, endTime) + local format = formats[options.output_format] + local originalStartTime = startTime + local originalEndTime = endTime + local path, is_temporary, is_stream + path, is_temporary, is_stream, startTime, endTime = find_path(startTime, endTime) + if not path then + message("No file is being played") + return + end + local command = { + "mpv", + path, + "--start=" .. seconds_to_time_string(startTime, false, true), + "--end=" .. seconds_to_time_string(endTime, false, true), + "--loop-file=no", + "--no-pause" + } + 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(originalStartTime, originalEndTime, format) + local out_path = utils.join_path(dir, formatted_filename) + append(command, { + "--o=" .. tostring(out_path) + }) + if options.twopass and format.supportsTwopass and not is_stream then + local first_pass_cmdline + do + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #command do + local arg = command[_index_0] + _accum_0[_len_0] = arg + _len_0 = _len_0 + 1 + end + first_pass_cmdline = _accum_0 + end + append(first_pass_cmdline, { + "--ovcopts-add=flags=+pass1" + }) + message("Starting first pass...") + msg.verbose("First-pass command line: ", table.concat(first_pass_cmdline, " ")) + local res = run_subprocess({ + args = first_pass_cmdline, + cancellable = false + }) + if not res then + message("First pass failed! Check the logs for details.") + return + end + append(command, { + "--ovcopts-add=flags=+pass2" + }) + if format.videoCodec == "libvpx" then + msg.verbose("Patching libvpx pass log file...") + vp8_patch_logfile(get_pass_logfile_path(out_path), endTime - startTime) + end + end + msg.info("Encoding to", out_path) + msg.verbose("Command line:", table.concat(command, " ")) + if options.run_detached then + message("Started encode, process was detached.") + return utils.subprocess_detached({ + args = command + }) + else + local res = false + if not should_display_progress() then + message("Started encode...") + res = run_subprocess({ + args = command, + cancellable = false + }) + else + local ewp = EncodeWithProgress(startTime, endTime) + res = ewp:startEncode(command) + end + if res then + message("Encoded successfully! Saved to\\N" .. tostring(bold(out_path))) + else + message("Encode failed! Check the logs for details.") + end + os.remove(get_pass_logfile_path(out_path)) + if is_temporary then + return os.remove(path) + end + 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() + -- Draw a border around the entire video frame to distinguish it from the background + ass:new_event() + ass:append("{\\an7}") + ass:pos(0, 0) + ass:append('{\\bord1}') + ass:append('{\\3c&H666666&}') + ass:append('{\\1a&HFF&}') + ass:draw_start() + ass:rect_cw(d.top_left.x, d.top_left.y, d.bottom_right.x, d.bottom_right.y) + ass:draw_stop() + -- Dim the area outside the crop + ass:new_event() + ass:append("{\\an7}") + ass:pos(0, 0) + ass:append('{\\bord0}') + ass:append('{\\shad0}') + ass:append('{\\c&H000000&}') + ass:append('{\\alpha&HAA&}') + 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) + ass:draw_stop() + -- Draw a bright border around the crop area + ass:new_event() + ass:append("{\\an7}") + ass:pos(0, 0) + ass:append('{\\bord2}') + ass:append('{\\3c&HFFFFFF&}') + ass:append('{\\1a&HFF&}') + ass:draw_start() + ass:rect_cw(region.x, region.y, region.x + region.w, region.y + region.h) + ass:draw_stop() + -- Points markers (outer corner brackets) + local pA = self.pointA:to_screen() + local pB = self.pointB:to_screen() + local centerX = (pA.x + pB.x) / 2 + local centerY = (pA.y + pB.y) / 2 + for _, p in ipairs({ + pA, + pB + }) do + ass:new_event() + local dirX = p.x < centerX and -1 or 1 + local dirY = p.y < centerY and -1 or 1 + ass:append("{\\an5}") + ass:pos(p.x, p.y) + ass:append('{\\bord1}') + ass:append('{\\3c&H000000&}') + ass:append('{\\c&H00FFFF&}') + ass:draw_start() + ass:rect_cw(0, 0, 12 * dirX, 2 * dirY) + ass:rect_cw(0, 0, 2 * dirX, 12 * dirY) + ass:draw_stop() + end + end, + draw = function(self) + local window = { } + window.w, window.h = mp.get_osd_size() + local ass = assdraw.ass_new() + self:draw_box(ass) + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('Crop:')) .. "\\N") + ass:append(tostring(bold('1:')) .. " change point A (" .. tostring(self.pointA.x) .. ", " .. tostring(self.pointA.y) .. ")\\N") + ass:append(tostring(bold('2:')) .. " change point B (" .. tostring(self.pointB.x) .. ", " .. tostring(self.pointB.y) .. ")\\N") + ass:append(tostring(bold('r:')) .. " reset to whole screen\\N") + ass:append(tostring(bold('ESC:')) .. " cancel crop\\N") + local width, height = math.abs(self.pointA.x - self.pointB.x), math.abs(self.pointA.y - self.pointB.y) + ass:append(tostring(bold('ENTER:')) .. " confirm crop (" .. tostring(width) .. "x" .. tostring(height) .. ")\\N") + return mp.set_osd_ass(window.w, window.h, ass.text) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback, region) + self.pointA = VideoPoint() + self.pointB = VideoPoint() + self.keybinds = { + ["1"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setPointA + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["2"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setPointB + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["r"] = (function() + local _base_1 = self + local _fn_0 = _base_1.reset + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancel + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ENTER"] = (function() + local _base_1 = self + local _fn_0 = _base_1.finish + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self:reset() + self.callback = callback + if region and region:is_valid() then + self.pointA.x = region.x + self.pointA.y = region.y + self.pointB.x = region.x + region.w + self.pointB.y = region.y + region.h + end + end, + __base = _base_0, + __name = "CropPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + CropPage = _class_0 + end + local Option + do + local _class_0 + local _base_0 = { + hasPrevious = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return true + elseif "int" == _exp_0 then + if self.opts.min then + return self.value > self.opts.min + else + return true + end + elseif "list" == _exp_0 then + return self.value > 1 + end + end, + hasNext = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return true + elseif "int" == _exp_0 then + if self.opts.max then + return self.value < self.opts.max + else + return true + end + elseif "list" == _exp_0 then + return self.value < #self.opts.possibleValues + end + end, + leftKey = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = not self.value + elseif "int" == _exp_0 then + self.value = self.value - self.opts.step + if self.opts.min and self.opts.min > self.value then + self.value = self.opts.min + end + elseif "list" == _exp_0 then + if self.value > 1 then + self.value = self.value - 1 + end + end + end, + rightKey = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = not self.value + elseif "int" == _exp_0 then + self.value = self.value + self.opts.step + if self.opts.max and self.opts.max < self.value then + self.value = self.opts.max + end + elseif "list" == _exp_0 then + if self.value < #self.opts.possibleValues then + self.value = self.value + 1 + end + end + end, + getValue = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return self.value + elseif "int" == _exp_0 then + return self.value + elseif "list" == _exp_0 then + local value, _ + do + local _obj_0 = self.opts.possibleValues[self.value] + value, _ = _obj_0[1], _obj_0[2] + end + return value + end + end, + setValue = function(self, value) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = value + elseif "int" == _exp_0 then + self.value = value + elseif "list" == _exp_0 then + local set = false + for i, possiblePair in ipairs(self.opts.possibleValues) do + local possibleValue, _ + possibleValue, _ = possiblePair[1], possiblePair[2] + if possibleValue == value then + set = true + self.value = i + break + end + end + if not set then + return msg.warn("Tried to set invalid value " .. tostring(value) .. " to " .. tostring(self.displayText) .. " option.") + end + end + end, + getDisplayValue = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return self.value and "yes" or "no" + elseif "int" == _exp_0 then + if self.opts.altDisplayNames and self.opts.altDisplayNames[self.value] then + return self.opts.altDisplayNames[self.value] + else + return tostring(self.value) + end + elseif "list" == _exp_0 then + local value, displayValue + do + local _obj_0 = self.opts.possibleValues[self.value] + value, displayValue = _obj_0[1], _obj_0[2] + end + return displayValue or value + end + end, + draw = function(self, ass, selected) + if selected then + ass:append(tostring(bold(self.displayText)) .. ": ") + else + ass:append(tostring(self.displayText) .. ": ") + end + if self:hasPrevious() then + ass:append("◀ ") + end + ass:append(self:getDisplayValue()) + if self:hasNext() then + ass:append(" ▶") + end + return ass:append("\\N") + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, optType, displayText, value, opts) + self.optType = optType + self.displayText = displayText + self.opts = opts + self.value = 1 + return self:setValue(value) + end, + __base = _base_0, + __name = "Option" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Option = _class_0 + end + local EncodeOptionsPage + do + local _class_0 + local _parent_0 = Page + local _base_0 = { + getCurrentOption = function(self) + return self.options[self.currentOption][2] + end, + leftKey = function(self) + (self:getCurrentOption()):leftKey() + return self:draw() + end, + rightKey = function(self) + (self:getCurrentOption()):rightKey() + return self:draw() + end, + prevOpt = function(self) + self.currentOption = math.max(1, self.currentOption - 1) + return self:draw() + end, + nextOpt = function(self) + self.currentOption = math.min(#self.options, self.currentOption + 1) + return self:draw() + end, + confirmOpts = function(self) + for _, optPair in ipairs(self.options) do + local optName, opt + optName, opt = optPair[1], optPair[2] + options[optName] = opt:getValue() + end + self:hide() + return self.callback(true) + end, + cancelOpts = function(self) + self:hide() + return self.callback(false) + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('Options:')) .. "\\N\\N") + for i, optPair in ipairs(self.options) do + local opt = optPair[2] + opt:draw(ass, self.currentOption == i) + end + ass:append("\\N▲ / ▼: navigate\\N") + ass:append(tostring(bold('ENTER:')) .. " confirm options\\N") + ass:append(tostring(bold('ESC:')) .. " cancel\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback) + self.callback = callback + self.currentOption = 1 + local scaleHeightOpts = { + possibleValues = { + { + -1, + "no" + }, + { + 240 + }, + { + 360 + }, + { + 480 + }, + { + 720 + }, + { + 1080 + }, + { + 1440 + }, + { + 2160 + } + } + } + local filesizeOpts = { + step = 250, + min = 0, + altDisplayNames = { + [0] = "0 (constant quality)" + } + } + local crfOpts = { + step = 1, + min = -1, + altDisplayNames = { + [-1] = "disabled" + } + } + local fpsOpts = { + possibleValues = { + { + -1, + "source" + }, + { + 15 + }, + { + 24 + }, + { + 30 + }, + { + 48 + }, + { + 50 + }, + { + 60 + }, + { + 120 + }, + { + 240 + } + } + } + local formatIds = { + "webm-vp8", + "webm-vp9", + "mp4", + "mp4-nvenc", + "raw", + "mp3", + "gif" + } + local formatOpts = { + possibleValues = (function() + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #formatIds do + local fId = formatIds[_index_0] + _accum_0[_len_0] = { + fId, + formats[fId].displayName + } + _len_0 = _len_0 + 1 + end + return _accum_0 + end)() + } + self.options = { + { + "output_format", + Option("list", "Output Format", options.output_format, formatOpts) + }, + { + "twopass", + Option("bool", "Two Pass", options.twopass) + }, + { + "apply_current_filters", + Option("bool", "Apply Current Video Filters", options.apply_current_filters) + }, + { + "scale_height", + Option("list", "Scale Height", options.scale_height, scaleHeightOpts) + }, + { + "strict_filesize_constraint", + Option("bool", "Strict Filesize Constraint", options.strict_filesize_constraint) + }, + { + "write_filename_on_metadata", + Option("bool", "Write Filename on Metadata", options.write_filename_on_metadata) + }, + { + "target_filesize", + Option("int", "Target Filesize", options.target_filesize, filesizeOpts) + }, + { + "crf", + Option("int", "CRF", options.crf, crfOpts) + }, + { + "fps", + Option("list", "FPS", options.fps, fpsOpts) + } + } + self.keybinds = { + ["LEFT"] = (function() + local _base_1 = self + local _fn_0 = _base_1.leftKey + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["RIGHT"] = (function() + local _base_1 = self + local _fn_0 = _base_1.rightKey + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["UP"] = (function() + local _base_1 = self + local _fn_0 = _base_1.prevOpt + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["DOWN"] = (function() + local _base_1 = self + local _fn_0 = _base_1.nextOpt + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ENTER"] = (function() + local _base_1 = self + local _fn_0 = _base_1.confirmOpts + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancelOpts + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + end, + __base = _base_0, + __name = "EncodeOptionsPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + EncodeOptionsPage = _class_0 + end + local PreviewPage + do + local _class_0 + local _parent_0 = Page + local _base_0 = { + prepare = function(self) + local vf = mp.get_property_native("vf") + vf[#vf + 1] = { + name = "sub" + } + if self.region:is_valid() then + vf[#vf + 1] = { + name = "crop", + params = { + w = tostring(self.region.w), + h = tostring(self.region.h), + x = tostring(self.region.x), + y = tostring(self.region.y) + } + } + end + mp.set_property_native("vf", vf) + if self.startTime > -1 and self.endTime > -1 then + mp.set_property_native("ab-loop-a", self.startTime) + mp.set_property_native("ab-loop-b", self.endTime) + mp.set_property_native("time-pos", self.startTime) + end + return mp.set_property_native("pause", false) + end, + dispose = function(self) + mp.set_property("ab-loop-a", "no") + mp.set_property("ab-loop-b", "no") + for prop, value in pairs(self.originalProperties) do + mp.set_property_native(prop, value) + end + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append("Press " .. tostring(bold('ESC')) .. " to exit preview.\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + cancel = function(self) + self:hide() + return self.callback() + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback, region, startTime, endTime) + self.callback = callback + self.originalProperties = { + ["vf"] = mp.get_property_native("vf"), + ["time-pos"] = mp.get_property_native("time-pos"), + ["pause"] = mp.get_property_native("pause") + } + self.keybinds = { + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancel + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self.region = region + self.startTime = startTime + self.endTime = endTime + self.isLoop = false + end, + __base = _base_0, + __name = "PreviewPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + PreviewPage = _class_0 + end + local MainPage + do + local _class_0 + local _parent_0 = Page + local _base_0 = { + setStartTime = function(self) + self.startTime = mp.get_property_number("time-pos") + if self.visible then + self:clear() + return self:draw() + end + end, + setEndTime = function(self) + self.endTime = mp.get_property_number("time-pos") + if self.visible then + self:clear() + return self:draw() + end + end, + setupStartAndEndTimes = function(self) + if mp.get_property_native("duration") then + self.startTime = 0 + self.endTime = mp.get_property_native("duration") + else + self.startTime = -1 + self.endTime = -1 + end + if self.visible then + self:clear() + return self:draw() + end + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('WebM maker')) .. "\\N\\N") + ass:append(tostring(bold('c:')) .. " crop\\N") + ass:append(tostring(bold('1:')) .. " set start time (current is " .. tostring(seconds_to_time_string(self.startTime)) .. ")\\N") + ass:append(tostring(bold('2:')) .. " set end time (current is " .. tostring(seconds_to_time_string(self.endTime)) .. ")\\N") + ass:append(tostring(bold('o:')) .. " change encode options\\N") + ass:append(tostring(bold('p:')) .. " preview\\N") + ass:append(tostring(bold('e:')) .. " encode\\N\\N") + ass:append(tostring(bold('ESC:')) .. " close\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + onUpdateCropRegion = function(self, updated, newRegion) + if updated then + self.region = newRegion + end + return self:show() + end, + crop = function(self) + self:hide() + local cropPage = CropPage((function() + local _base_1 = self + local _fn_0 = _base_1.onUpdateCropRegion + return function(...) + return _fn_0(_base_1, ...) + end + end)(), self.region) + return cropPage:show() + end, + onOptionsChanged = function(self, updated) + return self:show() + end, + changeOptions = function(self) + self:hide() + local encodeOptsPage = EncodeOptionsPage((function() + local _base_1 = self + local _fn_0 = _base_1.onOptionsChanged + return function(...) + return _fn_0(_base_1, ...) + end + end)()) + return encodeOptsPage:show() + end, + onPreviewEnded = function(self) + return self:show() + end, + preview = function(self) + self:hide() + local previewPage = PreviewPage((function() + local _base_1 = self + local _fn_0 = _base_1.onPreviewEnded + return function(...) + return _fn_0(_base_1, ...) + end + end)(), self.region, self.startTime, self.endTime) + return previewPage:show() + end, + encode = function(self) + self:hide() + if self.startTime < 0 then + message("No start time, aborting") + return + end + if self.endTime < 0 then + message("No end time, aborting") + return + end + if self.startTime >= self.endTime then + message("Start time is ahead of end time, aborting") + return + end + return encode(self.region, self.startTime, self.endTime) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.keybinds = { + ["c"] = (function() + local _base_1 = self + local _fn_0 = _base_1.crop + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["1"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setStartTime + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["2"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setEndTime + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["o"] = (function() + local _base_1 = self + local _fn_0 = _base_1.changeOptions + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["p"] = (function() + local _base_1 = self + local _fn_0 = _base_1.preview + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["e"] = (function() + local _base_1 = self + local _fn_0 = _base_1.encode + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.hide + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self.startTime = -1 + self.endTime = -1 + self.region = Region() + end, + __base = _base_0, + __name = "MainPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + MainPage = _class_0 + end + monitor_dimensions() + local mainPage = MainPage() + mp.add_key_binding(options.keybind, "display-webm-encoder", (function() + local _base_0 = mainPage + local _fn_0 = _base_0.show + return function(...) + return _fn_0(_base_0, ...) + end + end)(), { + repeatable = false + }) + return mp.register_event("file-loaded", (function() + local _base_0 = mainPage + local _fn_0 = _base_0.setupStartAndEndTimes + return function(...) + return _fn_0(_base_0, ...) + end + end)()) diff --git a/webm.lua b/webm.lua new file mode 100644 index 0000000..3d81140 --- /dev/null +++ b/webm.lua @@ -0,0 +1,2719 @@ +local mp = require("mp") +local assdraw = require("mp.assdraw") +local msg = require("mp.msg") +local utils = require("mp.utils") +local mpopts = require("mp.options") +local options = { + -- Defaults to shift+w + keybind = "W", + -- If empty, saves on the same directory of the playing video. + -- A starting "~" will be replaced by the home dir. + -- This field is delimited by double-square-brackets - [[ and ]] - instead of + -- quotes, because Windows users might run into a issue when using + -- backslashes as a path separator. Examples of valid inputs for this field + -- would be: [[]] (the default, empty value), [[C:\Users\John]] (on Windows), + -- and [[/home/john]] (on Unix-like systems eg. Linux). + output_directory = [[~/Videos/mpv-encodes]], + 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, Wwith 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 = "%T_%S-%E", + -- 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 = 9500, + -- 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/libopus) + -- 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 = 7, + -- 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 = "libopus" + 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 find_path +find_path = function(startTime, endTime) + local path = mp.get_property('path') + if not path then + return nil, nil, nil, nil, nil + end + local is_stream = not file_exists(path) + local is_temporary = false + if is_stream then + if mp.get_property('file-format') == 'hls' then + path = utils.join_path(parse_directory('~'), 'cache_dump.ts') + mp.command_native({ + 'dump_cache', + seconds_to_time_string(startTime, false, true), + seconds_to_time_string(endTime + 5, false, true), + path + }) + endTime = endTime - startTime + startTime = 0 + is_temporary = true + end + end + return path, is_stream, is_temporary, startTime, endTime +end +local encode +encode = function(region, startTime, endTime) + local format = formats[options.output_format] + local originalStartTime = startTime + local originalEndTime = endTime + local path, is_temporary, is_stream + path, is_temporary, is_stream, startTime, endTime = find_path(startTime, endTime) + if not path then + message("No file is being played") + return + end + local command = { + "mpv", + path, + "--start=" .. seconds_to_time_string(startTime, false, true), + "--end=" .. seconds_to_time_string(endTime, false, true), + "--loop-file=no", + "--no-pause" + } + 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(originalStartTime, originalEndTime, format) + local out_path = utils.join_path(dir, formatted_filename) + append(command, { + "--o=" .. tostring(out_path) + }) + if options.twopass and format.supportsTwopass and not is_stream then + local first_pass_cmdline + do + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #command do + local arg = command[_index_0] + _accum_0[_len_0] = arg + _len_0 = _len_0 + 1 + end + first_pass_cmdline = _accum_0 + end + append(first_pass_cmdline, { + "--ovcopts-add=flags=+pass1" + }) + message("Starting first pass...") + msg.verbose("First-pass command line: ", table.concat(first_pass_cmdline, " ")) + local res = run_subprocess({ + args = first_pass_cmdline, + cancellable = false + }) + if not res then + message("First pass failed! Check the logs for details.") + return + end + append(command, { + "--ovcopts-add=flags=+pass2" + }) + if format.videoCodec == "libvpx" then + msg.verbose("Patching libvpx pass log file...") + vp8_patch_logfile(get_pass_logfile_path(out_path), endTime - startTime) + end + end + msg.info("Encoding to", out_path) + msg.verbose("Command line:", table.concat(command, " ")) + if options.run_detached then + message("Started encode, process was detached.") + return utils.subprocess_detached({ + args = command + }) + else + local res = false + if not should_display_progress() then + message("Started encode...") + res = run_subprocess({ + args = command, + cancellable = false + }) + else + local ewp = EncodeWithProgress(startTime, endTime) + res = ewp:startEncode(command) + end + if res then + message("Encoded successfully! Saved to\\N" .. tostring(bold(out_path))) + else + message("Encode failed! Check the logs for details.") + end + os.remove(get_pass_logfile_path(out_path)) + if is_temporary then + return os.remove(path) + end + end +end +local CropPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + reset = function(self) + local dimensions = get_video_dimensions() + local xa, ya + do + local _obj_0 = dimensions.top_left + xa, ya = _obj_0.x, _obj_0.y + end + self.pointA:set_from_screen(xa, ya) + local xb, yb + do + local _obj_0 = dimensions.bottom_right + xb, yb = _obj_0.x, _obj_0.y + end + self.pointB:set_from_screen(xb, yb) + if self.visible then + return self:draw() + end + end, + setPointA = function(self) + local posX, posY = mp.get_mouse_pos() + self.pointA:set_from_screen(posX, posY) + if self.visible then + return self:draw() + end + end, + setPointB = function(self) + local posX, posY = mp.get_mouse_pos() + self.pointB:set_from_screen(posX, posY) + if self.visible then + return self:draw() + end + end, + cancel = function(self) + self:hide() + return self.callback(false, nil) + end, + finish = function(self) + local region = Region() + region:set_from_points(self.pointA, self.pointB) + self:hide() + return self.callback(true, region) + end, + draw_box = function(self, ass) + local region = Region() + region:set_from_points(self.pointA:to_screen(), self.pointB:to_screen()) + local d = get_video_dimensions() + ass:new_event() + ass:append("{\\an7}") + ass:pos(0, 0) + ass:append('{\\bord0}') + ass:append('{\\shad0}') + ass:append('{\\c&H000000&}') + ass:append('{\\alpha&H77}') + ass:draw_start() + ass:rect_cw(d.top_left.x, d.top_left.y, region.x, region.y + region.h) + ass:rect_cw(region.x, d.top_left.y, d.bottom_right.x, region.y) + ass:rect_cw(d.top_left.x, region.y + region.h, region.x + region.w, d.bottom_right.y) + ass:rect_cw(region.x + region.w, region.y, d.bottom_right.x, d.bottom_right.y) + return ass:draw_stop() + end, + draw = function(self) + local window = { } + window.w, window.h = mp.get_osd_size() + local ass = assdraw.ass_new() + self:draw_box(ass) + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('Crop:')) .. "\\N") + ass:append(tostring(bold('1:')) .. " change point A (" .. tostring(self.pointA.x) .. ", " .. tostring(self.pointA.y) .. ")\\N") + ass:append(tostring(bold('2:')) .. " change point B (" .. tostring(self.pointB.x) .. ", " .. tostring(self.pointB.y) .. ")\\N") + ass:append(tostring(bold('r:')) .. " reset to whole screen\\N") + ass:append(tostring(bold('ESC:')) .. " cancel crop\\N") + local width, height = math.abs(self.pointA.x - self.pointB.x), math.abs(self.pointA.y - self.pointB.y) + ass:append(tostring(bold('ENTER:')) .. " confirm crop (" .. tostring(width) .. "x" .. tostring(height) .. ")\\N") + return mp.set_osd_ass(window.w, window.h, ass.text) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback, region) + self.pointA = VideoPoint() + self.pointB = VideoPoint() + self.keybinds = { + ["1"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setPointA + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["2"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setPointB + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["r"] = (function() + local _base_1 = self + local _fn_0 = _base_1.reset + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancel + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ENTER"] = (function() + local _base_1 = self + local _fn_0 = _base_1.finish + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self:reset() + self.callback = callback + if region and region:is_valid() then + self.pointA.x = region.x + self.pointA.y = region.y + self.pointB.x = region.x + region.w + self.pointB.y = region.y + region.h + end + end, + __base = _base_0, + __name = "CropPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + CropPage = _class_0 +end +local Option +do + local _class_0 + local _base_0 = { + hasPrevious = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return true + elseif "int" == _exp_0 then + if self.opts.min then + return self.value > self.opts.min + else + return true + end + elseif "list" == _exp_0 then + return self.value > 1 + end + end, + hasNext = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return true + elseif "int" == _exp_0 then + if self.opts.max then + return self.value < self.opts.max + else + return true + end + elseif "list" == _exp_0 then + return self.value < #self.opts.possibleValues + end + end, + leftKey = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = not self.value + elseif "int" == _exp_0 then + self.value = self.value - self.opts.step + if self.opts.min and self.opts.min > self.value then + self.value = self.opts.min + end + elseif "list" == _exp_0 then + if self.value > 1 then + self.value = self.value - 1 + end + end + end, + rightKey = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = not self.value + elseif "int" == _exp_0 then + self.value = self.value + self.opts.step + if self.opts.max and self.opts.max < self.value then + self.value = self.opts.max + end + elseif "list" == _exp_0 then + if self.value < #self.opts.possibleValues then + self.value = self.value + 1 + end + end + end, + getValue = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return self.value + elseif "int" == _exp_0 then + return self.value + elseif "list" == _exp_0 then + local value, _ + do + local _obj_0 = self.opts.possibleValues[self.value] + value, _ = _obj_0[1], _obj_0[2] + end + return value + end + end, + setValue = function(self, value) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = value + elseif "int" == _exp_0 then + self.value = value + elseif "list" == _exp_0 then + local set = false + for i, possiblePair in ipairs(self.opts.possibleValues) do + local possibleValue, _ + possibleValue, _ = possiblePair[1], possiblePair[2] + if possibleValue == value then + set = true + self.value = i + break + end + end + if not set then + return msg.warn("Tried to set invalid value " .. tostring(value) .. " to " .. tostring(self.displayText) .. " option.") + end + end + end, + getDisplayValue = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return self.value and "yes" or "no" + elseif "int" == _exp_0 then + if self.opts.altDisplayNames and self.opts.altDisplayNames[self.value] then + return self.opts.altDisplayNames[self.value] + else + return tostring(self.value) + end + elseif "list" == _exp_0 then + local value, displayValue + do + local _obj_0 = self.opts.possibleValues[self.value] + value, displayValue = _obj_0[1], _obj_0[2] + end + return displayValue or value + end + end, + draw = function(self, ass, selected) + if selected then + ass:append(tostring(bold(self.displayText)) .. ": ") + else + ass:append(tostring(self.displayText) .. ": ") + end + if self:hasPrevious() then + ass:append("◀ ") + end + ass:append(self:getDisplayValue()) + if self:hasNext() then + ass:append(" ▶") + end + return ass:append("\\N") + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, optType, displayText, value, opts) + self.optType = optType + self.displayText = displayText + self.opts = opts + self.value = 1 + return self:setValue(value) + end, + __base = _base_0, + __name = "Option" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Option = _class_0 +end +local EncodeOptionsPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + getCurrentOption = function(self) + return self.options[self.currentOption][2] + end, + leftKey = function(self) + (self:getCurrentOption()):leftKey() + return self:draw() + end, + rightKey = function(self) + (self:getCurrentOption()):rightKey() + return self:draw() + end, + prevOpt = function(self) + self.currentOption = math.max(1, self.currentOption - 1) + return self:draw() + end, + nextOpt = function(self) + self.currentOption = math.min(#self.options, self.currentOption + 1) + return self:draw() + end, + confirmOpts = function(self) + for _, optPair in ipairs(self.options) do + local optName, opt + optName, opt = optPair[1], optPair[2] + options[optName] = opt:getValue() + end + self:hide() + return self.callback(true) + end, + cancelOpts = function(self) + self:hide() + return self.callback(false) + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('Options:')) .. "\\N\\N") + for i, optPair in ipairs(self.options) do + local opt = optPair[2] + opt:draw(ass, self.currentOption == i) + end + ass:append("\\N▲ / ▼: navigate\\N") + ass:append(tostring(bold('ENTER:')) .. " confirm options\\N") + ass:append(tostring(bold('ESC:')) .. " cancel\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback) + self.callback = callback + self.currentOption = 1 + local scaleHeightOpts = { + possibleValues = { + { + -1, + "no" + }, + { + 240 + }, + { + 360 + }, + { + 480 + }, + { + 720 + }, + { + 1080 + }, + { + 1440 + }, + { + 2160 + } + } + } + local filesizeOpts = { + step = 250, + min = 0, + altDisplayNames = { + [0] = "0 (constant quality)" + } + } + local crfOpts = { + step = 1, + min = -1, + altDisplayNames = { + [-1] = "disabled" + } + } + local fpsOpts = { + possibleValues = { + { + -1, + "source" + }, + { + 15 + }, + { + 24 + }, + { + 30 + }, + { + 48 + }, + { + 50 + }, + { + 60 + }, + { + 120 + }, + { + 240 + } + } + } + local formatIds = { + "webm-vp8", + "webm-vp9", + "mp4", + "mp4-nvenc", + "raw", + "mp3", + "gif" + } + local formatOpts = { + possibleValues = (function() + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #formatIds do + local fId = formatIds[_index_0] + _accum_0[_len_0] = { + fId, + formats[fId].displayName + } + _len_0 = _len_0 + 1 + end + return _accum_0 + end)() + } + self.options = { + { + "output_format", + Option("list", "Output Format", options.output_format, formatOpts) + }, + { + "twopass", + Option("bool", "Two Pass", options.twopass) + }, + { + "apply_current_filters", + Option("bool", "Apply Current Video Filters", options.apply_current_filters) + }, + { + "scale_height", + Option("list", "Scale Height", options.scale_height, scaleHeightOpts) + }, + { + "strict_filesize_constraint", + Option("bool", "Strict Filesize Constraint", options.strict_filesize_constraint) + }, + { + "write_filename_on_metadata", + Option("bool", "Write Filename on Metadata", options.write_filename_on_metadata) + }, + { + "target_filesize", + Option("int", "Target Filesize", options.target_filesize, filesizeOpts) + }, + { + "crf", + Option("int", "CRF", options.crf, crfOpts) + }, + { + "fps", + Option("list", "FPS", options.fps, fpsOpts) + } + } + self.keybinds = { + ["LEFT"] = (function() + local _base_1 = self + local _fn_0 = _base_1.leftKey + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["RIGHT"] = (function() + local _base_1 = self + local _fn_0 = _base_1.rightKey + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["UP"] = (function() + local _base_1 = self + local _fn_0 = _base_1.prevOpt + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["DOWN"] = (function() + local _base_1 = self + local _fn_0 = _base_1.nextOpt + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ENTER"] = (function() + local _base_1 = self + local _fn_0 = _base_1.confirmOpts + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancelOpts + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + end, + __base = _base_0, + __name = "EncodeOptionsPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + EncodeOptionsPage = _class_0 +end +local PreviewPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + prepare = function(self) + local vf = mp.get_property_native("vf") + vf[#vf + 1] = { + name = "sub" + } + if self.region:is_valid() then + vf[#vf + 1] = { + name = "crop", + params = { + w = tostring(self.region.w), + h = tostring(self.region.h), + x = tostring(self.region.x), + y = tostring(self.region.y) + } + } + end + mp.set_property_native("vf", vf) + if self.startTime > -1 and self.endTime > -1 then + mp.set_property_native("ab-loop-a", self.startTime) + mp.set_property_native("ab-loop-b", self.endTime) + mp.set_property_native("time-pos", self.startTime) + end + return mp.set_property_native("pause", false) + end, + dispose = function(self) + mp.set_property("ab-loop-a", "no") + mp.set_property("ab-loop-b", "no") + for prop, value in pairs(self.originalProperties) do + mp.set_property_native(prop, value) + end + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append("Press " .. tostring(bold('ESC')) .. " to exit preview.\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + cancel = function(self) + self:hide() + return self.callback() + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback, region, startTime, endTime) + self.callback = callback + self.originalProperties = { + ["vf"] = mp.get_property_native("vf"), + ["time-pos"] = mp.get_property_native("time-pos"), + ["pause"] = mp.get_property_native("pause") + } + self.keybinds = { + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancel + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self.region = region + self.startTime = startTime + self.endTime = endTime + self.isLoop = false + end, + __base = _base_0, + __name = "PreviewPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + PreviewPage = _class_0 +end +local MainPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + setStartTime = function(self) + self.startTime = mp.get_property_number("time-pos") + if self.visible then + self:clear() + return self:draw() + end + end, + setEndTime = function(self) + self.endTime = mp.get_property_number("time-pos") + if self.visible then + self:clear() + return self:draw() + end + end, + setupStartAndEndTimes = function(self) + if mp.get_property_native("duration") then + self.startTime = 0 + self.endTime = mp.get_property_native("duration") + else + self.startTime = -1 + self.endTime = -1 + end + if self.visible then + self:clear() + return self:draw() + end + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('WebM maker')) .. "\\N\\N") + ass:append(tostring(bold('c:')) .. " crop\\N") + ass:append(tostring(bold('1:')) .. " set start time (current is " .. tostring(seconds_to_time_string(self.startTime)) .. ")\\N") + ass:append(tostring(bold('2:')) .. " set end time (current is " .. tostring(seconds_to_time_string(self.endTime)) .. ")\\N") + ass:append(tostring(bold('o:')) .. " change encode options\\N") + ass:append(tostring(bold('p:')) .. " preview\\N") + ass:append(tostring(bold('e:')) .. " encode\\N\\N") + ass:append(tostring(bold('ESC:')) .. " close\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + onUpdateCropRegion = function(self, updated, newRegion) + if updated then + self.region = newRegion + end + return self:show() + end, + crop = function(self) + self:hide() + local cropPage = CropPage((function() + local _base_1 = self + local _fn_0 = _base_1.onUpdateCropRegion + return function(...) + return _fn_0(_base_1, ...) + end + end)(), self.region) + return cropPage:show() + end, + onOptionsChanged = function(self, updated) + return self:show() + end, + changeOptions = function(self) + self:hide() + local encodeOptsPage = EncodeOptionsPage((function() + local _base_1 = self + local _fn_0 = _base_1.onOptionsChanged + return function(...) + return _fn_0(_base_1, ...) + end + end)()) + return encodeOptsPage:show() + end, + onPreviewEnded = function(self) + return self:show() + end, + preview = function(self) + self:hide() + local previewPage = PreviewPage((function() + local _base_1 = self + local _fn_0 = _base_1.onPreviewEnded + return function(...) + return _fn_0(_base_1, ...) + end + end)(), self.region, self.startTime, self.endTime) + return previewPage:show() + end, + encode = function(self) + self:hide() + if self.startTime < 0 then + message("No start time, aborting") + return + end + if self.endTime < 0 then + message("No end time, aborting") + return + end + if self.startTime >= self.endTime then + message("Start time is ahead of end time, aborting") + return + end + return encode(self.region, self.startTime, self.endTime) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.keybinds = { + ["c"] = (function() + local _base_1 = self + local _fn_0 = _base_1.crop + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["1"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setStartTime + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["2"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setEndTime + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["o"] = (function() + local _base_1 = self + local _fn_0 = _base_1.changeOptions + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["p"] = (function() + local _base_1 = self + local _fn_0 = _base_1.preview + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["e"] = (function() + local _base_1 = self + local _fn_0 = _base_1.encode + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.hide + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self.startTime = -1 + self.endTime = -1 + self.region = Region() + end, + __base = _base_0, + __name = "MainPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + MainPage = _class_0 +end +monitor_dimensions() +local mainPage = MainPage() +mp.add_key_binding(options.keybind, "display-webm-encoder", (function() + local _base_0 = mainPage + local _fn_0 = _base_0.show + return function(...) + return _fn_0(_base_0, ...) + end +end)(), { + repeatable = false +}) +return mp.register_event("file-loaded", (function() + local _base_0 = mainPage + local _fn_0 = _base_0.setupStartAndEndTimes + return function(...) + return _fn_0(_base_0, ...) + end +end)())