summaryrefslogtreecommitdiff
path: root/.config/mpv/scripts/encode.lua
diff options
context:
space:
mode:
authoryuzu-eva <stevenhu@web.de>2023-04-01 19:12:44 +0200
committeryuzu-eva <stevenhu@web.de>2023-04-01 19:12:44 +0200
commitccba79d7f97140a325af7b98b8ce2917e7cf4fc8 (patch)
tree2b3ff294f2ab915a08916125a9e4fd2c3fa126ac /.config/mpv/scripts/encode.lua
parent39e9d4e4f740647e49cf7f2f090bc0ddb27a7d76 (diff)
parentbe35ccddeb63db26949183ade5a801593918a597 (diff)
merge desktop into master
Diffstat (limited to '.config/mpv/scripts/encode.lua')
-rw-r--r--.config/mpv/scripts/encode.lua315
1 files changed, 315 insertions, 0 deletions
diff --git a/.config/mpv/scripts/encode.lua b/.config/mpv/scripts/encode.lua
new file mode 100644
index 0000000..df518d8
--- /dev/null
+++ b/.config/mpv/scripts/encode.lua
@@ -0,0 +1,315 @@
+local utils = require "mp.utils"
+local msg = require "mp.msg"
+local options = require "mp.options"
+
+local ON_WINDOWS = (package.config:sub(1,1) ~= "/")
+
+local start_timestamp = nil
+local profile_start = ""
+
+-- implementation detail of the osd message
+local timer = nil
+local timer_duration = 2
+
+function append_table(lhs, rhs)
+ for i = 1,#rhs do
+ lhs[#lhs+1] = rhs[i]
+ end
+ return lhs
+end
+
+function file_exists(name)
+ local f = io.open(name, "r")
+ if f ~= nil then
+ io.close(f)
+ return true
+ else
+ return false
+ end
+end
+
+function get_extension(path)
+ local candidate = string.match(path, "%.([^.]+)$")
+ if candidate then
+ for _, ext in ipairs({ "mkv", "webm", "mp4", "avi" }) do
+ if candidate == ext then
+ return candidate
+ end
+ end
+ end
+ return "mkv"
+end
+
+function get_output_string(dir, format, input, extension, title, from, to, profile)
+ local res = utils.readdir(dir)
+ if not res then
+ return nil
+ end
+ local files = {}
+ for _, f in ipairs(res) do
+ files[f] = true
+ end
+ local output = format
+ output = string.gsub(output, "$f", function() return input end)
+ output = string.gsub(output, "$t", function() return title end)
+ output = string.gsub(output, "$s", function() return seconds_to_time_string(from, true) end)
+ output = string.gsub(output, "$e", function() return seconds_to_time_string(to, true) end)
+ output = string.gsub(output, "$d", function() return seconds_to_time_string(to-from, true) end)
+ output = string.gsub(output, "$x", function() return extension end)
+ output = string.gsub(output, "$p", function() return profile end)
+ if ON_WINDOWS then
+ output = string.gsub(output, "[/\\|<>?:\"*]", "_")
+ end
+ if not string.find(output, "$n") then
+ return files[output] and nil or output
+ end
+ local i = 1
+ while true do
+ local potential_name = string.gsub(output, "$n", tostring(i))
+ if not files[potential_name] then
+ return potential_name
+ end
+ i = i + 1
+ end
+end
+
+function get_video_filters()
+ local filters = {}
+ for _, vf in ipairs(mp.get_property_native("vf")) do
+ local name = vf["name"]
+ name = string.gsub(name, '^lavfi%-', '')
+ local filter
+ if name == "crop" then
+ local p = vf["params"]
+ filter = string.format("crop=%d:%d:%d:%d", p.w, p.h, p.x, p.y)
+ elseif name == "mirror" then
+ filter = "hflip"
+ elseif name == "flip" then
+ filter = "vflip"
+ elseif name == "rotate" then
+ local rotation = vf["params"]["angle"]
+ -- rotate is NOT the filter we want here
+ if rotation == "90" then
+ filter = "transpose=clock"
+ elseif rotation == "180" then
+ filter = "transpose=clock,transpose=clock"
+ elseif rotation == "270" then
+ filter = "transpose=cclock"
+ end
+ end
+ filters[#filters + 1] = filter
+ end
+ return filters
+end
+
+function get_input_info(default_path, only_active)
+ local accepted = {
+ video = true,
+ audio = not mp.get_property_bool("mute"),
+ sub = mp.get_property_bool("sub-visibility")
+ }
+ local ret = {}
+ for _, track in ipairs(mp.get_property_native("track-list")) do
+ local track_path = track["external-filename"] or default_path
+ if not only_active or (track["selected"] and accepted[track["type"]]) then
+ local tracks = ret[track_path]
+ if not tracks then
+ ret[track_path] = { track["ff-index"] }
+ else
+ tracks[#tracks + 1] = track["ff-index"]
+ end
+ end
+ end
+ return ret
+end
+
+function seconds_to_time_string(seconds, full)
+ local ret = string.format("%02d:%02d.%03d"
+ , math.floor(seconds / 60) % 60
+ , math.floor(seconds) % 60
+ , seconds * 1000 % 1000
+ )
+ if full or seconds > 3600 then
+ ret = string.format("%d:%s", math.floor(seconds / 3600), ret)
+ end
+ return ret
+end
+
+function start_encoding(from, to, settings)
+ local args = {
+ settings.ffmpeg_command,
+ "-loglevel", "panic", "-hide_banner",
+ }
+ local append_args = function(table) args = append_table(args, table) end
+
+ local path = mp.get_property("path")
+ local is_stream = not file_exists(path)
+ if is_stream then
+ path = mp.get_property("stream-path")
+ end
+
+ local track_args = {}
+ local start = seconds_to_time_string(from, false)
+ local input_index = 0
+ for input_path, tracks in pairs(get_input_info(path, settings.only_active_tracks)) do
+ append_args({
+ "-ss", start,
+ "-i", input_path,
+ })
+ if settings.only_active_tracks then
+ for _, track_index in ipairs(tracks) do
+ track_args = append_table(track_args, { "-map", string.format("%d:%d", input_index, track_index)})
+ end
+ else
+ track_args = append_table(track_args, { "-map", tostring(input_index)})
+ end
+ input_index = input_index + 1
+ end
+
+ append_args({"-to", tostring(to-from)})
+ append_args(track_args)
+
+ -- apply some of the video filters currently in the chain
+ local filters = {}
+ if settings.preserve_filters then
+ filters = get_video_filters()
+ end
+ if settings.append_filter ~= "" then
+ filters[#filters + 1] = settings.append_filter
+ end
+ if #filters > 0 then
+ append_args({ "-filter:v", table.concat(filters, ",") })
+ end
+
+ -- split the user-passed settings on whitespace
+ for token in string.gmatch(settings.codec, "[^%s]+") do
+ args[#args + 1] = token
+ end
+
+ -- path of the output
+ local output_directory = settings.output_directory
+ if output_directory == "" then
+ if is_stream then
+ output_directory = "."
+ else
+ output_directory, _ = utils.split_path(path)
+ end
+ else
+ output_directory = string.gsub(output_directory, "^~", os.getenv("HOME") or "~")
+ end
+ local input_name = mp.get_property("filename/no-ext") or "encode"
+ local title = mp.get_property("media-title")
+ local extension = get_extension(path)
+ local output_name = get_output_string(output_directory, settings.output_format, input_name, extension, title, from, to, settings.profile)
+ if not output_name then
+ mp.osd_message("Invalid path " .. output_directory)
+ return
+ end
+ args[#args + 1] = utils.join_path(output_directory, output_name)
+
+ if settings.print then
+ local o = ""
+ -- fuck this is ugly
+ for i = 1, #args do
+ local fmt = ""
+ if i == 1 then
+ fmt = "%s%s"
+ elseif i >= 2 and i <= 4 then
+ fmt = "%s"
+ elseif args[i-1] == "-i" or i == #args or args[i-1] == "-filter:v" then
+ fmt = "%s '%s'"
+ else
+ fmt = "%s %s"
+ end
+ o = string.format(fmt, o, args[i])
+ end
+ print(o)
+ end
+ if settings.detached then
+ utils.subprocess_detached({ args = args })
+ else
+ local res = utils.subprocess({ args = args, max_size = 0, cancellable = false })
+ if res.status == 0 then
+ mp.osd_message("Finished encoding succesfully")
+ else
+ mp.osd_message("Failed to encode, check the log")
+ end
+ end
+end
+
+function clear_timestamp()
+ timer:kill()
+ start_timestamp = nil
+ profile_start = ""
+ mp.remove_key_binding("encode-ESC")
+ mp.remove_key_binding("encode-ENTER")
+ mp.osd_message("", 0)
+end
+
+function set_timestamp(profile)
+ if not mp.get_property("path") then
+ mp.osd_message("No file currently playing")
+ return
+ end
+ if not mp.get_property_bool("seekable") then
+ mp.osd_message("Cannot encode non-seekable media")
+ return
+ end
+
+ if not start_timestamp or profile ~= profile_start then
+ profile_start = profile
+ start_timestamp = mp.get_property_number("time-pos")
+ local msg = function()
+ mp.osd_message(
+ string.format("encode [%s]: waiting for end timestamp", profile or "default"),
+ timer_duration
+ )
+ end
+ msg()
+ timer = mp.add_periodic_timer(timer_duration, msg)
+ mp.add_forced_key_binding("ESC", "encode-ESC", clear_timestamp)
+ mp.add_forced_key_binding("ENTER", "encode-ENTER", function() set_timestamp(profile) end)
+ else
+ local from = start_timestamp
+ local to = mp.get_property_number("time-pos")
+ if to <= from then
+ mp.osd_message("Second timestamp cannot be before the first", timer_duration)
+ timer:kill()
+ timer:resume()
+ return
+ end
+ clear_timestamp()
+ mp.osd_message(string.format("Encoding from %s to %s"
+ , seconds_to_time_string(from, false)
+ , seconds_to_time_string(to, false)
+ ), timer_duration)
+ -- include the current frame into the extract
+ local fps = mp.get_property_number("container-fps") or 30
+ to = to + 1 / fps / 2
+ local settings = {
+ detached = true,
+ container = "",
+ only_active_tracks = false,
+ preserve_filters = true,
+ append_filter = "",
+ codec = "-an -sn -c:v libvpx -crf 10 -b:v 1000k",
+ output_format = "$f_$n.webm",
+ output_directory = "",
+ ffmpeg_command = "ffmpeg",
+ print = true,
+ }
+ if profile then
+ options.read_options(settings, profile)
+ if settings.container ~= "" then
+ msg.warn("The 'container' setting is deprecated, use 'output_format' now")
+ settings.output_format = settings.output_format .. "." .. settings.container
+ end
+ settings.profile = profile
+ else
+ settings.profile = "default"
+ end
+ start_encoding(from, to, settings)
+ end
+end
+
+mp.add_key_binding(nil, "set-timestamp", set_timestamp)