dotfiles

personal dotfiles
git clone anongit@rnpnr.xyz:dotfiles.git
Log | Files | Refs | Feed | Submodules

Commit: 08efd193a2dbaa93505d02ea834527ae3cf18c68
Parent: 83b1f5dc597587f3b3b9916634ee16bd0f5d0198
Author: 0x766F6964
Date:   Sat, 14 Nov 2020 19:06:12 -0700

mpv: update scripts and move some to submodules

Diffstat:
M.config/mpv/scripts/autoload.lua | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
D.config/mpv/scripts/detect-image.lua | 89-------------------------------------------------------------------------------
M.config/mpv/scripts/gallery-thumbgen.lua | 16++++++++--------
D.config/mpv/scripts/image-positioning.lua | 341-------------------------------------------------------------------------------
A.config/mpv/scripts/mpv-image-viewer | 1+
A.config/mpv/scripts/mpv-reload | 1+
D.config/mpv/scripts/reload.lua | 395-------------------------------------------------------------------------------
M.config/mpv/scripts/webm.lua | 892++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
M.gitmodules | 6++++++
9 files changed, 876 insertions(+), 947 deletions(-)

diff --git a/.config/mpv/scripts/autoload.lua b/.config/mpv/scripts/autoload.lua @@ -2,15 +2,34 @@ -- the currently played file. It does so by scanning the directory a file is -- located in when starting playback. It sorts the directory entries -- alphabetically, and adds entries before and after the current file to --- the internal playlist. (It stops if the it would add an already existing +-- the internal playlist. (It stops if it would add an already existing -- playlist entry at the same position - this makes it "stable".) -- Add at most 5000 * 2 files when starting a file (before + after). + +--[[ +To configure this script use file autoload.conf in directory script-opts (the "script-opts" +directory must be in the mpv configuration directory, typically ~/.config/mpv/). + +Example configuration would be: + +disabled=no +images=no +videos=yes +audio=yes + +--]] + MAXENTRIES = 5000 +local msg = require 'mp.msg' local options = require 'mp.options' +local utils = require 'mp.utils' o = { - disabled = false + disabled = false, + images = true, + videos = true, + audio = true } options.read_options(o) @@ -20,12 +39,29 @@ function Set (t) return set end -EXTENSIONS = Set { - 'mkv', 'avi', 'mp4', 'ogv', 'webm', 'rmvb', 'flv', 'wmv', 'mpeg', 'mpg', 'm4v', '3gp', - 'mp3', 'wav', 'ogm', 'flac', 'm4a', 'wma', 'ogg', 'opus', +function SetUnion (a,b) + local res = {} + for k in pairs(a) do res[k] = true end + for k in pairs(b) do res[k] = true end + return res +end + +EXTENSIONS_VIDEO = Set { + 'mkv', 'avi', 'mp4', 'ogv', 'webm', 'rmvb', 'flv', 'wmv', 'mpeg', 'mpg', 'm4v', '3gp' } -mputils = require 'mp.utils' +EXTENSIONS_AUDIO = Set { + 'mp3', 'wav', 'ogm', 'flac', 'm4a', 'wma', 'ogg', 'opus' +} + +EXTENSIONS_IMAGES = Set { + 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'gif', 'webp', 'svg', 'bmp' +} + +EXTENSIONS = Set {} +if o.videos then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_VIDEO) end +if o.audio then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_AUDIO) end +if o.images then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_IMAGES) end function add_files_at(index, files) index = index - 1 @@ -84,22 +120,38 @@ function alnumcomp(x, y) return #xt < #yt end +local autoloaded = nil + function find_and_add_entries() local path = mp.get_property("path", "") - local dir, filename = mputils.split_path(path) - if o.disabled or #dir == 0 then + local dir, filename = utils.split_path(path) + msg.trace(("dir: %s, filename: %s"):format(dir, filename)) + if o.disabled then + msg.verbose("stopping: autoload disabled") + return + elseif #dir == 0 then + msg.verbose("stopping: not a local path") return end + local pl_count = mp.get_property_number("playlist-count", 1) - if (pl_count > 1 and autoload == nil) or + -- check if this is a manually made playlist + if (pl_count > 1 and autoloaded == nil) or (pl_count == 1 and EXTENSIONS[string.lower(get_extension(filename))] == nil) then + msg.verbose("stopping: manually made playlist") return else - autoload = true + autoloaded = true end - local files = mputils.readdir(dir, "files") + local pl = mp.get_property_native("playlist", {}) + local pl_current = mp.get_property_number("playlist-pos-1", 1) + msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current, + utils.to_string(pl))) + + local files = utils.readdir(dir, "files") if files == nil then + msg.verbose("no other files in directory") return end table.filter(files, function (v, k) @@ -118,8 +170,6 @@ function find_and_add_entries() dir = "" end - local pl = mp.get_property_native("playlist", {}) - local pl_current = mp.get_property_number("playlist-pos", 0) + 1 -- Find the current pl entry (dir+"/"+filename) in the sorted dir list local current for i = 1, #files do @@ -131,6 +181,7 @@ function find_and_add_entries() if current == nil then return end + msg.trace("current file position in files: "..current) local append = {[-1] = {}, [1] = {}} for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1 @@ -144,6 +195,7 @@ function find_and_add_entries() local filepath = dir .. file if pl_e then -- If there's a playlist entry, and it's the same file, stop. + msg.trace(pl_e.filename.." == "..filepath.." ?") if pl_e.filename == filepath then break end @@ -151,11 +203,11 @@ function find_and_add_entries() if direction == -1 then if pl_current == 1 then -- never add additional entries in the middle - mp.msg.info("Prepending " .. file) + msg.info("Prepending " .. file) table.insert(append[-1], 1, filepath) end else - mp.msg.info("Adding " .. file) + msg.info("Adding " .. file) table.insert(append[1], filepath) end end diff --git a/.config/mpv/scripts/detect-image.lua b/.config/mpv/scripts/detect-image.lua @@ -1,89 +0,0 @@ -local opts = { - command_on_first_image_loaded="", - command_on_image_loaded="", - command_on_non_image_loaded="", -} -(require 'mp.options').read_options(opts) - -if opts.command_on_first_image_loaded == "" - and opts.command_on_image_loaded == "" - and opts.command_on_non_image_loaded == "" -then - return -end - -local msg = require 'mp.msg' - -local was_image = false -local frame_count = nil -local audio_tracks = nil -local out_params_ready = nil -local path = nil - -function run_maybe(str) - if str ~= "" then - mp.command(str) - end -end - -function set_image(is_image) - if is_image and not was_image then - msg.verbose("First image detected") - run_maybe(opts.command_on_first_image_loaded) - end - if is_image then - msg.verbose("Image detected") - run_maybe(opts.command_on_image_loaded) - end - if not is_image and was_image then - msg.verbose("Non-image detected") - run_maybe(opts.command_on_non_image_loaded) - end - was_image = is_image -end - -function state_changed() - -- only do things when state is consistent - if path ~= nil and audio_tracks ~= nil then - if frame_count == nil and audio_tracks > 0 then - set_image(false) - elseif out_params_ready and frame_count ~= nil then - -- png have 0 frames, jpg 1 ¯\_(ツ)_/¯ - set_image((frame_count == 0 or frame_count == 1) and audio_tracks == 0) - end - end -end - -mp.observe_property("dwidth", "number", function(_, val) - out_params_ready = (val ~= nil and val > 0) - state_changed() -end) - -mp.observe_property("estimated-frame-count", "number", function(_, val) - frame_count = val - state_changed() -end) - -mp.observe_property("path", "string", function(_, val) - if not val or val == "" then - path = nil - else - path = val - end - state_changed() -end) - -mp.register_event("tracks-changed", function() - audio_tracks = 0 - local tracks = 0 - for _, track in ipairs(mp.get_property_native("track-list")) do - tracks = tracks + 1 - if track.type == "audio" then - audio_tracks = audio_tracks + 1 - end - end - if tracks == 0 then - audio_tracks = nil - end - state_changed() -end) diff --git a/.config/mpv/scripts/gallery-thumbgen.lua b/.config/mpv/scripts/gallery-thumbgen.lua @@ -180,17 +180,17 @@ function thumbnail_command(input_path, width, height, take_thumbnail_at, output_ if not accurate then add({ "--hr-seek=no"}) end - add({ "--start", take_thumbnail_at }) + add({ "--start="..take_thumbnail_at }) end add({ "--no-config", "--msg-level=all=no", - "--vf", "lavfi=[" .. vf .. ",format=bgra]", - "--audio", "no", - "--sub", "no", - "--frames", "1", - "--image-display-duration", "0", - "--of", "rawvideo", "--ovc", "rawvideo", - "--o", output_path + "--vf=lavfi=[" .. vf .. ",format=bgra]", + "--audio=no", + "--sub=no", + "--frames=1", + "--image-display-duration=0", + "--of=rawvideo", "--ovc=rawvideo", + "--o="..output_path }) end return out diff --git a/.config/mpv/scripts/image-positioning.lua b/.config/mpv/scripts/image-positioning.lua @@ -1,341 +0,0 @@ -local opts = { - pan_follows_cursor_margin = 50, - pan_follows_cursor_move_if_full_view = false, - - drag_to_pan_margin = 50, - drag_to_pan_move_if_full_view = false, -} -(require 'mp.options').read_options(opts) - -function clamp(value, low, high) - if value <= low then - return low - elseif value >= high then - return high - else - return value - end -end - -local msg = require 'mp.msg' -local assdraw = require 'mp.assdraw' - -function get_video_dimensions() - -- this function is very much ripped from video/out/aspect.c in mpv's source - local video_params = mp.get_property_native("video-out-params") - if not video_params then - _video_dimensions = nil - return nil - end - _video_dimensions = { - top_left = { 0, 0 }, - bottom_right = { 0, 0 }, - size = { 0, 0 }, - ratios = { 0, 0 }, -- by how much the original video got scaled - } - 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 - 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 = 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 - --account for C int cast truncating as opposed to flooring - 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[1], _video_dimensions.bottom_right[1] = 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[2], _video_dimensions.bottom_right[2] = split_scaling(window_h, scaled_height, zoom, align_y, pan_y) - else - _video_dimensions.top_left[1] = 0 - _video_dimensions.bottom_right[1] = window_w - _video_dimensions.top_left[2] = 0 - _video_dimensions.bottom_right[2] = window_h - end - _video_dimensions.size[1] = _video_dimensions.bottom_right[1] - _video_dimensions.top_left[1] - _video_dimensions.size[2] = _video_dimensions.bottom_right[2] - _video_dimensions.top_left[2] - _video_dimensions.ratios[1] = _video_dimensions.size[1] / w - _video_dimensions.ratios[2] = _video_dimensions.size[2] / h - return _video_dimensions -end - -local cleanup = nil -- function set up by drag-to-pan/pan-follows cursor and must be called to clean lingering state - -function drag_to_pan_handler(table) - if cleanup then - cleanup() - cleanup = nil - end - if table["event"] == "down" then - local video_dimensions = get_video_dimensions() - if not video_dimensions then return end - local window_w, window_h = mp.get_osd_size() - local mouse_pos_origin, video_pan_origin = {}, {} - local moved = false - mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos() - video_pan_origin[1] = mp.get_property_number("video-pan-x") - video_pan_origin[2] = mp.get_property_number("video-pan-y") - local margin = opts.drag_to_pan_margin - local move_up = true - local move_lateral = true - if not opts.drag_to_pan_move_if_full_view then - if video_dimensions.size[1] <= window_w then - move_lateral = false - end - if video_dimensions.size[2] <= window_h then - move_up = false - end - end - if not move_up and not move_lateral then return end - local idle = function() - if moved then - local mX, mY = mp.get_mouse_pos() - local pX = video_pan_origin[1] - local pY = video_pan_origin[2] - if move_lateral then - pX = video_pan_origin[1] + (mX - mouse_pos_origin[1]) / video_dimensions.size[1] - if video_dimensions.size[1] + 2 * margin > window_w then - pX = clamp(pX, - (-margin + window_w / 2) / video_dimensions.size[1] - 0.5, - (margin - window_w / 2) / video_dimensions.size[1] + 0.5) - else - pX = clamp(pX, - (margin - window_w / 2) / video_dimensions.size[1] + 0.5, - (-margin + window_w / 2) / video_dimensions.size[1] - 0.5) - end - end - if move_up then - pY = video_pan_origin[2] + (mY - mouse_pos_origin[2]) / video_dimensions.size[2] - if video_dimensions.size[2] + 2 * margin > window_h then - pY = clamp(pY, - (-margin + window_h / 2) / video_dimensions.size[2] - 0.5, - (margin - window_h / 2) / video_dimensions.size[2] + 0.5) - else - pY = clamp(pY, - (margin - window_h / 2) / video_dimensions.size[2] + 0.5, - (-margin + window_h / 2) / video_dimensions.size[2] - 0.5) - end - end - mp.command("no-osd set video-pan-x " .. clamp(pX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(pY, -3, 3)) - moved = false - end - end - mp.register_idle(idle) - mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end) - cleanup = function() - mp.remove_key_binding("image-viewer-mouse-move") - mp.unregister_idle(idle) - end - end -end - -function pan_follows_cursor_handler(table) - if cleanup then - cleanup() - cleanup = nil - end - if table["event"] == "down" then - local video_dimensions = get_video_dimensions() - if not video_dimensions then return end - local window_w, window_h = mp.get_osd_size() - local moved = true - local idle = function() - if moved then - local mX, mY = mp.get_mouse_pos() - local x = math.min(1, math.max(- 2 * mX / window_w + 1, -1)) - local y = math.min(1, math.max(- 2 * mY / window_h + 1, -1)) - local command = "" - local margin, move_full = opts.pan_follows_cursor_margin, opts.pan_follows_cursor_move_if_full_view - if (not move_full and window_w < video_dimensions.size[1]) then - command = command .. "no-osd set video-pan-x " .. clamp(x * (video_dimensions.size[1] - window_w + 2 * margin) / (2 * video_dimensions.size[1]), -3, 3) .. ";" - elseif mp.get_property_number("video-pan-x") ~= 0 then - command = command .. "no-osd set video-pan-x " .. "0;" - end - if (not move_full and window_h < video_dimensions.size[2]) then - command = command .. "no-osd set video-pan-y " .. clamp(y * (video_dimensions.size[2] - window_h + 2 * margin) / (2 * video_dimensions.size[2]), -3, 3) .. ";" - elseif mp.get_property_number("video-pan-y") ~= 0 then - command = command .. "no-osd set video-pan-y " .. "0;" - end - if command ~= "" then - mp.command(command) - end - moved = false - end - end - mp.register_idle(idle) - mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end) - cleanup = function() - mp.remove_key_binding("image-viewer-mouse-move") - mp.unregister_idle(idle) - end - end -end - -function cursor_centric_zoom_handler(amt) - local zoom_inc = tonumber(amt) - if not zoom_inc or zoom_inc == 0 then return end - local video_dimensions = get_video_dimensions() - if not video_dimensions then return end - local mouse_pos_origin, video_pan_origin = {}, {} - mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos() - video_pan_origin[1] = mp.get_property("video-pan-x") - video_pan_origin[2] = mp.get_property("video-pan-y") - local zoom_origin = mp.get_property("video-zoom") - -- how far the cursor is form the middle of the video (in percentage) - local rx = (video_dimensions.top_left[1] + video_dimensions.size[1] / 2 - mouse_pos_origin[1]) / (video_dimensions.size[1] / 2) - local ry = (video_dimensions.top_left[2] + video_dimensions.size[2] / 2 - mouse_pos_origin[2]) / (video_dimensions.size[2] / 2) - - -- the size in pixels of the (in|de)crement - local diffHeight = (2 ^ zoom_inc - 1) * video_dimensions.size[2] - local diffWidth = (2 ^ zoom_inc - 1) * video_dimensions.size[1] - local newPanX = (video_pan_origin[1] * video_dimensions.size[1] + rx * diffWidth / 2) / (video_dimensions.size[1] + diffWidth) - local newPanY = (video_pan_origin[2] * video_dimensions.size[2] + ry * diffHeight / 2) / (video_dimensions.size[2] + diffHeight) - mp.command("no-osd set video-zoom " .. zoom_origin + zoom_inc .. "; no-osd set video-pan-x " .. clamp(newPanX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(newPanY, -3, 3)) -end - -function align_border(x, y) - local video_dimensions = get_video_dimensions() - if not video_dimensions then return end - local window_w, window_h = mp.get_osd_size() - local x, y = tonumber(x), tonumber(y) - local command = "" - if x then - command = command .. "no-osd set video-pan-x " .. clamp(x * (video_dimensions.size[1] - window_w) / (2 * video_dimensions.size[1]), -3, 3) .. ";" - end - if y then - command = command .. "no-osd set video-pan-y " .. clamp(y * (video_dimensions.size[2] - window_h) / (2 * video_dimensions.size[2]), -3, 3) .. ";" - end - if command ~= "" then - mp.command(command) - end -end - -function pan_image(axis, amount, zoom_invariant, image_constrained) - amount = tonumber(amount) - if not amount or amount == 0 or axis ~= "x" and axis ~= "y" then return end - if zoom_invariant == "yes" then - amount = amount / 2 ^ mp.get_property_number("video-zoom") - end - local prop = "video-pan-" .. axis - local old_pan = mp.get_property_number(prop) - axis = (axis == "x") and 1 or 2 - if image_constrained == "yes" then - local video_dimensions = get_video_dimensions() - if not video_dimensions then return end - local window = {} - window[1], window[2] = mp.get_osd_size() - local pixels_moved = amount * video_dimensions.size[axis] - -- should somehow refactor this - if pixels_moved > 0 then - if window[axis] > video_dimensions.size[axis] then - if video_dimensions.bottom_right[axis] >= window[axis] then return end - if video_dimensions.bottom_right[axis] + pixels_moved > window[axis] then - amount = (window[axis] - video_dimensions.bottom_right[axis]) / video_dimensions.size[axis] - end - else - if video_dimensions.top_left[axis] >= 0 then return end - if video_dimensions.top_left[axis] + pixels_moved > 0 then - amount = (0 - video_dimensions.top_left[axis]) / video_dimensions.size[axis] - end - end - else - if window[axis] > video_dimensions.size[axis] then - if video_dimensions.top_left[axis] <= 0 then return end - if video_dimensions.top_left[axis] + pixels_moved < 0 then - amount = (0 - video_dimensions.top_left[axis]) / video_dimensions.size[axis] - end - else - if video_dimensions.bottom_right[axis] <= window[axis] then return end - if video_dimensions.bottom_right[axis] + pixels_moved < window[axis] then - amount = (window[axis] - video_dimensions.bottom_right[axis]) / video_dimensions.size[axis] - end - end - end - end - mp.set_property_number(prop, old_pan + amount) -end - -function rotate_video(amt) - local rot = mp.get_property_number("video-rotate") - rot = (rot + amt) % 360 - mp.set_property_number("video-rotate", rot) -end - -function reset_pan_if_visible() - local video_dimensions = get_video_dimensions() - if not video_dimensions then return end - local window_w, window_h = mp.get_osd_size() - local command = "" - if (window_w >= video_dimensions.size[1]) then - command = command .. "no-osd set video-pan-x 0" .. ";" - end - if (window_h >= video_dimensions.size[2]) then - command = command .. "no-osd set video-pan-y 0" .. ";" - end - if command ~= "" then - mp.command(command) - end -end - -mp.add_key_binding(nil, "drag-to-pan", drag_to_pan_handler, {complex = true}) -mp.add_key_binding(nil, "pan-follows-cursor", pan_follows_cursor_handler, {complex = true}) -mp.add_key_binding(nil, "cursor-centric-zoom", cursor_centric_zoom_handler) -mp.add_key_binding(nil, "align-border", align_border) -mp.add_key_binding(nil, "pan-image", pan_image) -mp.add_key_binding(nil, "rotate-video", rotate_video) -mp.add_key_binding(nil, "reset-pan-if-visible", reset_pan_if_visible) -mp.add_key_binding(nil, "force-print-filename", force_print_filename) diff --git a/.config/mpv/scripts/mpv-image-viewer b/.config/mpv/scripts/mpv-image-viewer @@ -0,0 +1 @@ +Subproject commit 3ad8a0513355025cd8af9a9621292be59c367113 diff --git a/.config/mpv/scripts/mpv-reload b/.config/mpv/scripts/mpv-reload @@ -0,0 +1 @@ +Subproject commit 2b8a719fe166d6d42b5f1dd64761f97997b54a86 diff --git a/.config/mpv/scripts/reload.lua b/.config/mpv/scripts/reload.lua @@ -1,395 +0,0 @@ --- reload.lua --- --- When an online video is stuck buffering or got very slow CDN --- source, restarting often helps. This script provides automatic --- reloading of videos that doesn't have buffering progress for some --- time while keeping the current time position. It also adds `Ctrl+r` --- keybinding to reload video manually. --- --- SETTINGS --- --- To override default setting put `lua-settings/reload.conf` file in --- mpv user folder, on linux it is `~/.config/mpv`. NOTE: config file --- name should match the name of the script. --- --- Default `reload.conf` settings: --- --- ``` --- # enable automatic reload on timeout --- # when paused-for-cache event fired, we will wait --- # paused_for_cache_timer_timeout sedonds and then reload the video --- paused_for_cache_timer_enabled=yes --- --- # checking paused_for_cache property interval in seconds, --- # can not be less than 0.05 (50 ms) --- paused_for_cache_timer_interval=1 --- --- # time in seconds to wait until reload --- paused_for_cache_timer_timeout=10 --- --- # enable automatic reload based on demuxer cache --- # if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout --- # time interval, the video will be reloaded as soon as demuxer cache depleated --- demuxer_cache_timer_enabled=yes --- --- # checking demuxer-cache-time property interval in seconds, --- # can not be less than 0.05 (50 ms) --- demuxer_cache_timer_interval=2 --- --- # if demuxer cache didn't receive any data during demuxer_cache_timer_timeout --- # we decide that it has no progress and will reload the stream when --- # paused_for_cache event happens --- demuxer_cache_timer_timeout=20 --- --- # when the end-of-file is reached, reload the stream to check --- # if there is more content available. --- reload_eof_enabled=no --- --- # keybinding to reload stream from current time position --- # you can disable keybinding by setting it to empty value --- # reload_key_binding= --- reload_key_binding=Ctrl+r --- ``` --- --- DEBUGGING --- --- Debug messages will be printed to stdout with mpv command line option --- `--msg-level='reload=debug'` - -local msg = require 'mp.msg' -local options = require 'mp.options' -local utils = require 'mp.utils' - - -local settings = { - paused_for_cache_timer_enabled = true, - paused_for_cache_timer_interval = 1, - paused_for_cache_timer_timeout = 10, - demuxer_cache_timer_enabled = true, - demuxer_cache_timer_interval = 2, - demuxer_cache_timer_timeout = 20, - reload_eof_enabled = false, - reload_key_binding = "Ctrl+r", -} - --- global state stores properties between reloads -local property_path = nil -local property_time_pos = 0 -local property_keep_open = nil - --- FSM managing the demuxer cache. --- --- States: --- --- * fetch - fetching new data --- * stale - unable to fetch new data for time < 'demuxer_cache_timer_timeout' --- * stuck - unable to fetch new data for time >= 'demuxer_cache_timer_timeout' --- --- State transitions: --- --- +---------------------------+ --- v | --- +-------+ +-------+ +-------+ --- + fetch +<--->+ stale +---->+ stuck | --- +-------+ +-------+ +-------+ --- | ^ | ^ | ^ --- +---+ +---+ +---+ -local demuxer_cache = { - timer = nil, - - state = { - name = 'uninitialized', - demuxer_cache_time = 0, - in_state_time = 0, - }, - - events = { - continue_fetch = { name = 'continue_fetch', from = 'fetch', to = 'fetch' }, - continue_stale = { name = 'continue_stale', from = 'stale', to = 'stale' }, - continue_stuck = { name = 'continue_stuck', from = 'stuck', to = 'stuck' }, - fetch_to_stale = { name = 'fetch_to_stale', from = 'fetch', to = 'stale' }, - stale_to_fetch = { name = 'stale_to_fetch', from = 'stale', to = 'fetch' }, - stale_to_stuck = { name = 'stale_to_stuck', from = 'stale', to = 'stuck' }, - stuck_to_fetch = { name = 'stuck_to_fetch', from = 'stuck', to = 'fetch' }, - }, - -} - --- Always start with 'fetch' state -function demuxer_cache.reset_state() - demuxer_cache.state = { - name = demuxer_cache.events.continue_fetch.to, - demuxer_cache_time = 0, - in_state_time = 0, - } -end - --- Has 'demuxer_cache_time' changed -function demuxer_cache.has_progress_since(t) - return demuxer_cache.state.demuxer_cache_time ~= t -end - -function demuxer_cache.is_state_fetch() - return demuxer_cache.state.name == demuxer_cache.events.continue_fetch.to -end - -function demuxer_cache.is_state_stale() - return demuxer_cache.state.name == demuxer_cache.events.continue_stale.to -end - -function demuxer_cache.is_state_stuck() - return demuxer_cache.state.name == demuxer_cache.events.continue_stuck.to -end - -function demuxer_cache.transition(event) - if demuxer_cache.state.name == event.from then - - -- state setup - demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time - - if event.name == 'continue_fetch' then - demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval - elseif event.name == 'continue_stale' then - demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval - elseif event.name == 'continue_stuck' then - demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval - elseif event.name == 'fetch_to_stale' then - demuxer_cache.state.in_state_time = 0 - elseif event.name == 'stale_to_fetch' then - demuxer_cache.state.in_state_time = 0 - elseif event.name == 'stale_to_stuck' then - demuxer_cache.state.in_state_time = 0 - elseif event.name == 'stuck_to_fetch' then - demuxer_cache.state.in_state_time = 0 - end - - -- state transition - demuxer_cache.state.name = event.to - - msg.debug('demuxer_cache.transition', event.name, utils.to_string(demuxer_cache.state)) - else - msg.error( - 'demuxer_cache.transition', - 'illegal transition', event.name, - 'from state', demuxer_cache.state.name) - end -end - -function demuxer_cache.initialize(demuxer_cache_timer_interval) - demuxer_cache.reset_state() - demuxer_cache.timer = mp.add_periodic_timer( - demuxer_cache_timer_interval, - function() - demuxer_cache.demuxer_cache_timer_tick( - mp.get_property_native('demuxer-cache-time'), - demuxer_cache_timer_interval) - end - ) -end - --- If there is no progress of demuxer_cache_time in --- settings.demuxer_cache_timer_timeout time interval switch state to --- 'stuck' and switch back to 'fetch' as soon as any progress is made -function demuxer_cache.demuxer_cache_timer_tick(demuxer_cache_time, demuxer_cache_timer_interval) - local event = nil - local cache_has_progress = demuxer_cache.has_progress_since(demuxer_cache_time) - - -- I miss pattern matching so much - if demuxer_cache.is_state_fetch() then - if cache_has_progress then - event = demuxer_cache.events.continue_fetch - else - event = demuxer_cache.events.fetch_to_stale - end - elseif demuxer_cache.is_state_stale() then - if cache_has_progress then - event = demuxer_cache.events.stale_to_fetch - elseif demuxer_cache.state.in_state_time < settings.demuxer_cache_timer_timeout then - event = demuxer_cache.events.continue_stale - else - event = demuxer_cache.events.stale_to_stuck - end - elseif demuxer_cache.is_state_stuck() then - if cache_has_progress then - event = demuxer_cache.events.stuck_to_fetch - else - event = demuxer_cache.events.continue_stuck - end - end - - event.demuxer_cache_time = demuxer_cache_time - event.interval = demuxer_cache_timer_interval - demuxer_cache.transition(event) -end - - -local paused_for_cache = { - timer = nil, - time = 0, -} - -function paused_for_cache.reset_timer() - msg.debug('paused_for_cache.reset_timer', paused_for_cache.time) - if paused_for_cache.timer then - paused_for_cache.timer:kill() - paused_for_cache.timer = nil - paused_for_cache.time = 0 - end -end - -function paused_for_cache.start_timer(interval_seconds, timeout_seconds) - msg.debug('paused_for_cache.start_timer', paused_for_cache.time) - if not paused_for_cache.timer then - paused_for_cache.timer = mp.add_periodic_timer( - interval_seconds, - function() - paused_for_cache.time = paused_for_cache.time + interval_seconds - if paused_for_cache.time >= timeout_seconds then - paused_for_cache.reset_timer() - reload_resume() - end - msg.debug('paused_for_cache', 'tick', paused_for_cache.time) - end - ) - end -end - -function paused_for_cache.handler(property, is_paused) - if is_paused then - - if demuxer_cache.is_state_stuck() then - msg.info("demuxer cache has no progress") - -- reset demuxer state to avoid immediate reload if - -- paused_for_cache event triggered right after reload - demuxer_cache.reset_state() - reload_resume() - end - - paused_for_cache.start_timer( - settings.paused_for_cache_timer_interval, - settings.paused_for_cache_timer_timeout) - else - paused_for_cache.reset_timer() - end -end - -function read_settings() - options.read_options(settings, mp.get_script_name()) - msg.debug(utils.to_string(settings)) -end - -function debug_info(event) - msg.debug("event =", utils.to_string(event)) - msg.debug("path =", mp.get_property("path")) - msg.debug("time-pos =", mp.get_property("time-pos")) - msg.debug("paused-for-cache =", mp.get_property("paused-for-cache")) - msg.debug("stream-path =", mp.get_property("stream-path")) - msg.debug("stream-pos =", mp.get_property("stream-pos")) - msg.debug("stream-end =", mp.get_property("stream-end")) - msg.debug("duration =", mp.get_property("duration")) - msg.debug("seekable =", mp.get_property("seekable")) -end - -function reload(path, time_pos) - msg.debug("reload", path, time_pos) - if time_pos == nil then - mp.commandv("loadfile", path, "replace") - else - mp.commandv("loadfile", path, "replace", "start=+" .. time_pos) - end -end - -function reload_resume() - local path = mp.get_property("path", property_path) - local time_pos = mp.get_property("time-pos") - local reload_duration = mp.get_property_native("duration") - - local playlist_count = mp.get_property_number("playlist/count") - local playlist_pos = mp.get_property_number("playlist-pos") - local playlist = {} - for i = 0, playlist_count-1 do - playlist[i] = mp.get_property("playlist/" .. i .. "/filename") - end - -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero - -- duration property. When reloading VOD, to keep the current time position - -- we should provide offset from the start. Stream doesn't have fixed start. - -- Decent choice would be to reload stream from it's current 'live' positon. - -- That's the reason we don't pass the offset when reloading streams. - if reload_duration and reload_duration > 0 then - msg.info("reloading video from", time_pos, "second") - reload(path, time_pos) - else - msg.info("reloading stream") - reload(path, nil) - end - msg.info("file ", playlist_pos+1, " of ", playlist_count, "in playlist") - for i = 0, playlist_pos-1 do - mp.commandv("loadfile", playlist[i], "append") - end - mp.commandv("playlist-move", 0, playlist_pos+1) - for i = playlist_pos+1, playlist_count-1 do - mp.commandv("loadfile", playlist[i], "append") - end -end - -function reload_eof(property, eof_reached) - msg.debug("reload_eof", property, eof_reached) - local time_pos = mp.get_property_number("time-pos") - local duration = mp.get_property_number("duration") - - if eof_reached and math.floor(time_pos) == math.floor(duration) then - msg.debug("property_time_pos", property_time_pos, "time_pos", time_pos) - - -- Check that playback time_pos made progress after the last reload. When - -- eof is reached we try to reload video, in case there is more content - -- available. If time_pos stayed the same after reload, it means that vidkk - -- to avoid infinite reload loop when playback ended - -- math.floor function rounds time_pos to a second, to avoid inane reloads - if math.floor(property_time_pos) == math.floor(time_pos) then - msg.info("eof reached, playback ended") - mp.set_property("keep-open", property_keep_open) - else - msg.info("eof reached, checking if more content available") - reload_resume() - mp.set_property_bool("pause", false) - property_time_pos = time_pos - end - end -end - --- main - -read_settings() - -if settings.reload_key_binding ~= "" then - mp.add_key_binding(settings.reload_key_binding, "reload_resume", reload_resume) -end - -if settings.paused_for_cache_timer_enabled then - mp.observe_property("paused-for-cache", "bool", paused_for_cache.handler) -end - -if settings.demuxer_cache_timer_enabled then - demuxer_cache.initialize(settings.demuxer_cache_timer_interval) -end - -if settings.reload_eof_enabled then - -- vo-configured == video output created && its configuration went ok - mp.observe_property( - "vo-configured", - "bool", - function(name, vo_configured) - msg.debug(name, vo_configured) - if vo_configured then - property_path = mp.get_property("path") - property_keep_open = mp.get_property("keep-open") - mp.set_property("keep-open", "yes") - mp.set_property("keep-open-pause", "no") - end - end - ) - - mp.observe_property("eof-reached", "bool", reload_eof) -end - ---mp.register_event("file-loaded", debug_info) diff --git a/.config/mpv/scripts/webm.lua b/.config/mpv/scripts/webm.lua @@ -5,7 +5,7 @@ local utils = require("mp.utils") local mpopts = require("mp.options") local options = { -- Defaults to shift+w - keybind = "E", + 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 @@ -20,11 +20,17 @@ local options = { -- %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 time, without milliseconds + -- %S, %E - Start and end time, without milliseconds -- %M - "-audio", if audio is enabled, empty otherwise - output_template = "%F-[%S-%E]%M", + -- %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 @@ -50,9 +56,12 @@ local options = { -- Set the number of encoding threads, for codecs libvpx and libvpx-vp9 libvpx_threads = 4, additional_flags = "", - -- Useful for flags that may impact output filesize, such as crf, qmin, qmax etc + -- 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 = "--ovcopts-add=crf=10", + 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", @@ -63,6 +72,37 @@ local options = { } 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}" @@ -110,9 +150,84 @@ file_exists = function(name) 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), @@ -120,13 +235,34 @@ format_filename = function(startTime, endTime, videoFormat) ["%%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 '-audio' or '' + ["%%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) @@ -166,10 +302,10 @@ 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") - msg.verbose("Command stdout: ") - msg.verbose(res.stdout) return false end return true @@ -214,6 +350,22 @@ should_display_progress = function() 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 @@ -438,6 +590,189 @@ make_fullscreen_region = function() 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 @@ -451,6 +786,16 @@ do 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 @@ -703,6 +1048,138 @@ do 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 @@ -765,6 +1242,9 @@ do return nil end, show = function(self) + if self.visible then + return + end self.visible = true self:observe_properties() self:add_keybinds() @@ -773,6 +1253,9 @@ do return self:draw() end, hide = function(self) + if not self.visible then + return + end self.visible = false self:unobserve_properties() self:remove_keybinds() @@ -782,6 +1265,7 @@ do 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 @@ -844,7 +1328,7 @@ do copy_command_line = _accum_0 end append(copy_command_line, { - '--term-status-msg=Encode time-pos: ${=time-pos}' + '--term-status-msg=Encode time-pos: ${=time-pos}\\n' }) self:show() local processFd = run_subprocess_popen(copy_command_line) @@ -904,14 +1388,80 @@ get_active_tracks = function() audio = not mp.get_property_bool("mute"), sub = mp.get_property_bool("sub-visibility") } - local active = { } + local active = { + video = { }, + audio = { }, + sub = { } + } for _, track in ipairs(mp.get_property_native("track-list")) do if track["selected"] and accepted[track["type"]] then - active[#active + 1] = track + 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 @@ -921,6 +1471,15 @@ get_scale_filters = function() 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 @@ -949,16 +1508,11 @@ 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") - for _, track in ipairs(mp.get_property_native("track-list")) do - if track["type"] == "sub" and track["external"] then - append(ret, { - "--sub-files-append=" .. tostring(track['external-filename']) - }) - end - end + append_property(ret, "ytdl-format") return ret end local get_speed_flags @@ -1009,43 +1563,8 @@ apply_current_filters = function(filters) end end end -local encode -encode = function(region, startTime, endTime) - local format = formats[options.output_format] - local path = mp.get_property("path") - if not path then - message("No file is being played") - return - end - local is_stream = not file_exists(path) - local command = { - "mpv", - path, - "--start=" .. seconds_to_time_string(startTime, false, true), - "--end=" .. seconds_to_time_string(endTime, false, true), - "--ovc=" .. tostring(format.videoCodec), - "--oac=" .. tostring(format.audioCodec), - "--loop-file=no" - } - local vid = -1 - local aid = -1 - local sid = -1 - for _, track in ipairs(get_active_tracks()) do - local _exp_0 = track["type"] - if "video" == _exp_0 then - vid = track['id'] - elseif "audio" == _exp_0 then - aid = track['id'] - elseif "sub" == _exp_0 then - sid = track['id'] - end - end - append(command, { - "--vid=" .. (vid >= 0 and tostring(vid) or "no"), - "--aid=" .. (aid >= 0 and tostring(aid) or "no"), - "--sid=" .. (sid >= 0 and tostring(sid) or "no") - }) - append(command, get_playback_options()) +local get_video_filters +get_video_filters = function(format, region) local filters = { } append(filters, format:getPreFilters()) if options.apply_current_filters then @@ -1057,45 +1576,157 @@ encode = function(region, startTime, endTime) }) 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(command, { + append(flags, { "--vf-add=" .. tostring(f) }) end - append(command, get_speed_flags()) + 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 options.target_filesize > 0 and format.acceptsBitrate then - local dT = endTime - startTime - if options.strict_filesize_constraint then - local video_kilobits = options.target_filesize * 8 - if aid >= 0 then - video_kilobits = video_kilobits - dT * options.strict_audio_bitrate + 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, { - "--oacopts-add=b=" .. tostring(options.strict_audio_bitrate) .. "k" + "--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 - video_kilobits = video_kilobits * options.strict_bitrate_multiplier - local bitrate = math.floor(video_kilobits / dT) - append(command, { - "--ovcopts-add=b=" .. tostring(bitrate) .. "k", - "--ovcopts-add=minrate=" .. tostring(bitrate) .. "k", - "--ovcopts-add=maxrate=" .. tostring(bitrate) .. "k" - }) else - local bitrate = math.floor(options.target_filesize * 8 / dT) + local type = format.videoCodec ~= "" and "ovc" or "oac" append(command, { - "--ovcopts-add=b=" .. tostring(bitrate) .. "k" + "--" .. tostring(type) .. "opts-add=b=0" }) end - elseif options.target_filesize <= 0 and format.acceptsBitrate then - append(command, { - "--ovcopts-add=b=0" - }) end for token in string.gmatch(options.additional_flags, "[^%s]+") do command[#command + 1] = token @@ -1104,7 +1735,27 @@ encode = function(region, startTime, endTime) 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 @@ -1118,9 +1769,7 @@ encode = function(region, startTime, endTime) first_pass_cmdline = _accum_0 end append(first_pass_cmdline, { - "--ovcopts-add=flags=+pass1", - "-of=" .. tostring(format.outputExtension), - "-o=" .. tostring(get_null_path()) + "--ovcopts-add=flags=+pass1" }) message("Starting first pass...") msg.verbose("First-pass command line: ", table.concat(first_pass_cmdline, " ")) @@ -1135,22 +1784,11 @@ encode = function(region, startTime, endTime) 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 - local dir = "" - if is_stream then - dir = parse_directory("~") - else - local _ - dir, _ = utils.split_path(path) - end - if options.output_directory ~= "" then - dir = parse_directory(options.output_directory) - end - local formatted_filename = format_filename(startTime, endTime, format) - local out_path = utils.join_path(dir, formatted_filename) - append(command, { - "-o=" .. tostring(out_path) - }) msg.info("Encoding to", out_path) msg.verbose("Command line:", table.concat(command, " ")) if options.run_detached then @@ -1171,9 +1809,13 @@ encode = function(region, startTime, endTime) res = ewp:startEncode(command) end if res then - return message("Encoded successfully! Saved to\\N" .. tostring(bold(out_path))) + message("Encoded successfully! Saved to\\N" .. tostring(bold(out_path))) else - return message("Encode failed! Check the logs for details.") + 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 @@ -1229,6 +1871,7 @@ do 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}') @@ -1253,7 +1896,8 @@ do 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") - ass:append(tostring(bold('ENTER:')) .. " confirm 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 } @@ -1587,11 +2231,53 @@ do [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", - "raw" + "mp4-nvenc", + "raw", + "mp3", + "gif" } local formatOpts = { possibleValues = (function() @@ -1636,6 +2322,14 @@ do { "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 = { diff --git a/.gitmodules b/.gitmodules @@ -10,3 +10,9 @@ [submodule "src/c/doas"] path = src/c/doas url = git@github.com:0x766F6964/doas.git +[submodule ".config/mpv/scripts/mpv-reload"] + path = .config/mpv/scripts/mpv-reload + url = https://github.com/4e6/mpv-reload.git +[submodule ".config/mpv/scripts/mpv-image-viewer"] + path = .config/mpv/scripts/mpv-image-viewer + url = https://github.com/occivink/mpv-image-viewer.git