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 = "X", -- 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). -- The [[]] delimiter is not needed when using from a configuration file -- in the script-opts folder. output_directory = [[]], run_detached = false, -- Template string for the output file -- %f - Filename, with extension -- %F - Filename, without extension -- %T - Media title, if it exists, or filename, with extension (useful for some streams, such as YouTube). -- %s, %e - Start and end time, with milliseconds -- %S, %E - Start and end time, without milliseconds -- %M - "-audio", if audio is enabled, empty otherwise -- %R - "-(height)p", where height is the video's height, or scale_height, if it's enabled. -- More specifiers are supported, see https://mpv.io/manual/master/#options-screenshot-template -- Property expansion is supported (with %{} at top level, ${} when nested), see https://mpv.io/manual/master/#property-expansion output_template = "%F-[%s-%e]%M", -- Scale video to a certain height, keeping the aspect ratio. -1 disables it. scale_height = -1, -- Change the FPS of the output video, dropping or duplicating frames as needed. -- -1 means the FPS will be unchanged from the source. fps = -1, -- Target filesize, in kB. This will be used to calculate the bitrate -- used on the encode. If this is set to <= 0, the video bitrate will be set -- to 0, which might enable constant quality modes, depending on the -- video codec that's used (VP8 and VP9, for example). target_filesize = 2500, -- If true, will use stricter flags to ensure the resulting file doesn't -- overshoot the target filesize. Not recommended, as constrained quality -- mode should work well, unless you're really having trouble hitting -- the target size. strict_filesize_constraint = false, strict_bitrate_multiplier = 0.95, -- In kilobits. strict_audio_bitrate = 64, -- Sets the output format, from a few predefined ones. -- Currently we have: -- webm-vp8 (libvpx/libvorbis) -- webm-vp9 (libvpx-vp9/libopus) -- mp4 (h264/AAC) -- mp4-nvenc (h264-NVENC/AAC) -- raw (rawvideo/pcm_s16le). -- mp3 (libmp3lame) -- and gif output_format = "webm-vp8", twopass = true, -- If set, applies the video filters currently used on the playback to the encode. apply_current_filters = true, -- If set, writes the video's filename to the "Title" field on the metadata. write_filename_on_metadata = false, -- Set the number of encoding threads, for codecs libvpx and libvpx-vp9 libvpx_threads = 4, additional_flags = "", -- Constant Rate Factor (CRF). The value meaning and limits may change, -- from codec to codec. Set to -1 to disable. crf = 10, -- Useful for flags that may impact output filesize, such as qmin, qmax etc -- Won't be applied when strict_filesize_constraint is on. non_strict_additional_flags = "", -- Display the encode progress, in %. Requires run_detached to be disabled. -- On Windows, it shows a cmd popup. "auto" will display progress on non-Windows platforms. display_progress = "auto", -- The font size used in the menu. Isn't used for the notifications (started encode, finished encode etc) font_size = 28, margin = 10, message_duration = 5, -- gif dither mode, 0-5 for bayer w/ bayer_scale 0-5, 6 for paletteuse default (sierra2_4a) gif_dither = 2, -- Force square pixels on output video -- Some players like recent Firefox versions display videos with non-square pixels with wrong aspect ratio force_square_pixels = false, } 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 emit_event emit_event = function(event_name, ...) return mp.commandv("script-message", "webm-" .. tostring(event_name), ...) end local test_set_options test_set_options = function(new_options_json) local new_options = utils.parse_json(new_options_json) for k, v in pairs(new_options) do options[k] = v end end mp.register_script_message("mpv-webm-set-options", test_set_options) 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, postCommandModifier = function(self, command, region, startTime, endTime) return command 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), "--ovcopts-add=auto-alt-ref=1", "--ovcopts-add=lag-in-frames=25", "--ovcopts-add=quality=good", "--ovcopts-add=cpu-used=0" } 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 = false 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 = { postCommandModifier = function(self, command, region, startTime, endTime) local new_command = { } local start_ts = seconds_to_time_string(startTime, false, true) local end_ts = seconds_to_time_string(endTime, false, true) start_ts = start_ts:gsub(":", "\\\\:") end_ts = end_ts:gsub(":", "\\\\:") local cfilter = "[vid1]trim=start=" .. tostring(start_ts) .. ":end=" .. tostring(end_ts) .. "[vidtmp];" if mp.get_property("deinterlace") == "yes" then cfilter = cfilter .. "[vidtmp]yadif=mode=1[vidtmp];" end for _, v in ipairs(command) do local _continue_0 = false repeat if v:match("^%-%-vf%-add=lavfi%-scale") or v:match("^%-%-vf%-add=lavfi%-crop") or v:match("^%-%-vf%-add=fps") or v:match("^%-%-vf%-add=lavfi%-eq") then local n = v:gsub("^%-%-vf%-add=", ""):gsub("^lavfi%-", "") cfilter = cfilter .. "[vidtmp]" .. tostring(n) .. "[vidtmp];" else if v:match("^%-%-video%-rotate=90") then cfilter = cfilter .. "[vidtmp]transpose=1[vidtmp];" else if v:match("^%-%-video%-rotate=270") then cfilter = cfilter .. "[vidtmp]transpose=2[vidtmp];" else if v:match("^%-%-video%-rotate=180") then cfilter = cfilter .. "[vidtmp]transpose=1[vidtmp];[vidtmp]transpose=1[vidtmp];" else if v:match("^%-%-deinterlace=") then _continue_0 = true break else append(new_command, { v }) _continue_0 = true break end end end end end _continue_0 = true until true if not _continue_0 then break end end cfilter = cfilter .. "[vidtmp]split[topal][vidf];" cfilter = cfilter .. "[topal]palettegen[pal];" cfilter = cfilter .. "[vidf]fifo[vidf];" if options.gif_dither == 6 then cfilter = cfilter .. "[vidf][pal]paletteuse[vo]" else cfilter = cfilter .. "[vidf][pal]paletteuse=dither=bayer:bayer_scale=" .. tostring(options.gif_dither) .. ":diff_mode=rectangle[vo]" end append(new_command, { "--lavfi-complex=" .. tostring(cfilter) }) return new_command end } _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() local filters = { } if options.force_square_pixels then append(filters, { "lavfi-scale=iw*sar:ih" }) end if options.scale_height > 0 then append(filters, { "lavfi-scale=-2:" .. tostring(options.scale_height) }) end return filters end local get_fps_filters get_fps_filters = function() if options.fps > 0 then return { "fps=" .. tostring(options.fps) } end return { } end local get_contrast_brightness_and_saturation_filters get_contrast_brightness_and_saturation_filters = function() local mpv_brightness = mp.get_property("brightness") local mpv_contrast = mp.get_property("contrast") local mpv_saturation = mp.get_property("saturation") if mpv_brightness == 0 and mpv_contrast == 0 and mpv_saturation == 0 then return { } end local eq_saturation = (mpv_saturation + 100) / 100.0 local eq_contrast = (mpv_contrast + 100) / 100.0 local eq_brightness = (mpv_brightness / 50.0 + eq_contrast - 1) / 2.0 return { "lavfi-eq=contrast=" .. tostring(eq_contrast) .. ":saturation=" .. tostring(eq_saturation) .. ":brightness=" .. tostring(eq_brightness) } 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-pos") append_property(ret, "sub-delay") append_property(ret, "video-rotate") append_property(ret, "ytdl-format") append_property(ret, "deinterlace") 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, get_contrast_brightness_and_saturation_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_stream, is_temporary path, is_stream, is_temporary, 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) }) emit_event("encode-started") 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.") emit_event("encode-finished", "fail") 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 command = format:postCommandModifier(command, region, startTime, endTime) 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))) emit_event("encode-finished", "success") else message("Encode failed! Check the logs for details.") emit_event("encode-finished", "fail") 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, optVisible = function(self) if self.visibleCheckFn == nil then return true else return self.visibleCheckFn() end end } _base_0.__index = _base_0 _class_0 = setmetatable({ __init = function(self, optType, displayText, value, opts, visibleCheckFn) self.optType = optType self.displayText = displayText self.opts = opts self.value = 1 self.visibleCheckFn = visibleCheckFn 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) for i = self.currentOption - 1, 1, -1 do if self.options[i][2]:optVisible() then self.currentOption = i break end end return self:draw() end, nextOpt = function(self) for i = self.currentOption + 1, #self.options do if self.options[i][2]:optVisible() then self.currentOption = i break end end 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] if opt:optVisible() then opt:draw(ass, self.currentOption == i) end 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" }, { 144 }, { 240 }, { 360 }, { 480 }, { 540 }, { 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)() } local gifDitherOpts = { possibleValues = { { 0, "bayer_scale 0" }, { 1, "bayer_scale 1" }, { 2, "bayer_scale 2" }, { 3, "bayer_scale 3" }, { 4, "bayer_scale 4" }, { 5, "bayer_scale 5" }, { 6, "sierra2_4a" } } } 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) }, { "gif_dither", Option("list", "GIF Dither Type", options.gif_dither, gifDitherOpts, function() return self.options[1][2]:getValue() == "gif" end) }, { "force_square_pixels", Option("bool", "Force Square Pixels", options.force_square_pixels) } } 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, show = function(self) _class_0.__parent.show(self) return emit_event("show-main-page") 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 }) 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)()) msg.verbose("Loaded mpv-webm script!") return emit_event("script-loaded")