summaryrefslogtreecommitdiff
path: root/.config/mpv/scripts
diff options
context:
space:
mode:
authoryuzu-eva <stevenhu@web.de>2022-12-23 23:48:27 +0100
committeryuzu-eva <stevenhu@web.de>2022-12-23 23:48:27 +0100
commit275b74ab4f23e2ddf40c4d4bb411a00d116aee8c (patch)
tree8f0d1a5acbeec67fe08b0a4b59e016657ee91a44 /.config/mpv/scripts
parentc9502c9c4b61c4cab7655c484ef7e577308343cf (diff)
added mpv scripts
Diffstat (limited to '.config/mpv/scripts')
-rw-r--r--.config/mpv/scripts/crop.lua434
-rw-r--r--.config/mpv/scripts/encode.lua315
2 files changed, 749 insertions, 0 deletions
diff --git a/.config/mpv/scripts/crop.lua b/.config/mpv/scripts/crop.lua
new file mode 100644
index 0000000..adbfaa3
--- /dev/null
+++ b/.config/mpv/scripts/crop.lua
@@ -0,0 +1,434 @@
+local opts = {
+ mode = "hard", -- can be "hard" or "soft". If hard, apply a crop filter, if soft zoom + pan. Or a bonus "delogo" mode
+ draw_shade = true,
+ shade_opacity = "77",
+ draw_frame = false,
+ frame_border_width = 2,
+ frame_border_color = "EEEEEE",
+ draw_crosshair = true,
+ draw_text = true,
+ mouse_support = true,
+ coarse_movement = 30,
+ left_coarse = "LEFT",
+ right_coarse = "RIGHT",
+ up_coarse = "UP",
+ down_coarse = "DOWN",
+ fine_movement = 1,
+ left_fine = "ALT+LEFT",
+ right_fine = "ALT+RIGHT",
+ up_fine = "ALT+UP",
+ down_fine = "ALT+DOWN",
+ accept = "ENTER,MOUSE_BTN0",
+ cancel = "ESC",
+}
+(require 'mp.options').read_options(opts)
+
+function split(input)
+ local ret = {}
+ for str in string.gmatch(input, "([^,]+)") do
+ ret[#ret + 1] = str
+ end
+ return ret
+end
+local msg = require 'mp.msg'
+
+opts.accept = split(opts.accept)
+opts.cancel = split(opts.cancel)
+function mode_ok(mode)
+ return mode == "soft" or mode == "hard" or mode == "delogo"
+end
+if not mode_ok(opts.mode) then
+ msg.error("Invalid mode value: " .. opts.mode)
+ return
+end
+
+local assdraw = require 'mp.assdraw'
+local active = false
+local active_mode = "" -- same possible values as opts.mode
+local rect_centered = false
+local rect_keepaspect = false
+local needs_drawing = false
+local crop_first_corner = nil -- in normalized video space
+local crop_cursor = {
+ x = 0,
+ y = 0
+}
+
+function redraw()
+ needs_drawing = true
+end
+
+function rect_from_two_points(p1, p2, centered, ratio)
+ local c1 = {p1.x, p1.y}
+ local c2 = {p2.x, p2.y}
+ if ratio then
+ -- adjust position of p2, such
+ if math.abs(c2[1] - c1[1]) < ratio * math.abs(c2[2] - c1[2]) then
+ local is_left = c2[1] < c1[1] and -1 or 1
+ c2[1] = c1[1] + is_left * math.abs(c2[2] - c1[2]) * ratio
+ else
+ local is_up = c2[2] < c1[2] and -1 or 1
+ c2[2] = c1[2] + is_up * math.abs(c2[1] - c1[1]) / ratio
+ end
+ end
+ if centered then
+ -- p1 is center => convert it into corner
+ c1[1] = c1[1] - (c2[1] - c1[1])
+ c1[2] = c1[2] - (c2[2] - c1[2])
+ end
+ -- sort corners
+ if c1[1] > c2[1] then c1[1], c2[1] = c2[1], c1[1] end
+ if c1[2] > c2[2] then c1[2], c2[2] = c2[2], c1[2] end
+ return { x = c1[1], y = c1[2] }, { x = c2[1], y = c2[2] }
+end
+
+function clamp(low, value, high)
+ if value <= low then
+ return low
+ elseif value >= high then
+ return high
+ else
+ return value
+ end
+end
+
+function clamp_point(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
+
+function screen_to_video_norm(point, dim)
+ return {
+ x = (point.x - dim.ml) / (dim.w - dim.ml - dim.mr),
+ y = (point.y - dim.mt) / (dim.h - dim.mt - dim.mb)
+ }
+end
+
+function video_norm_to_screen(point, dim)
+ return {
+ x = math.floor(point.x * (dim.w - dim.ml - dim.mr) + dim.ml + 0.5),
+ y = math.floor(point.y * (dim.h - dim.mt - dim.mb) + dim.mt + 0.5)
+ }
+end
+
+function draw_shade(ass, unshaded, window)
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append("{\\an7}")
+ ass:append("{\\bord0}")
+ ass:append("{\\shad0}")
+ ass:append("{\\c&H000000&}")
+ ass:append("{\\1a&H" .. opts.shade_opacity .. "}")
+ ass:append("{\\2a&HFF}")
+ ass:append("{\\3a&HFF}")
+ ass:append("{\\4a&HFF}")
+ local c1, c2 = unshaded.top_left, unshaded.bottom_right
+ local v = window
+ -- c1.x c2.x
+ -- +-----+------------+
+ -- | | ur |
+ -- c1.y| ul +-------+----+
+ -- | | | |
+ -- c2.y+-----+-------+ lr |
+ -- | ll | |
+ -- +-------------+----+
+ ass:draw_start()
+ ass:rect_cw(v.top_left.x, v.top_left.y, c1.x, c2.y) -- ul
+ ass:rect_cw(c1.x, v.top_left.y, v.bottom_right.x, c1.y) -- ur
+ ass:rect_cw(v.top_left.x, c2.y, c2.x, v.bottom_right.y) -- ll
+ ass:rect_cw(c2.x, c1.y, v.bottom_right.x, v.bottom_right.y) -- lr
+ ass:draw_stop()
+ -- also possible to draw a rect over the whole video
+ -- and \iclip it in the middle, but seemingy slower
+end
+
+function draw_frame(ass, frame)
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append("{\\an7}")
+ ass:append("{\\bord0}")
+ ass:append("{\\shad0}")
+ ass:append("{\\c&H" .. opts.frame_border_color .. "&}")
+ ass:append("{\\1a&H00&}")
+ ass:append("{\\2a&HFF&}")
+ ass:append("{\\3a&HFF&}")
+ ass:append("{\\4a&HFF&}")
+ local c1, c2 = frame.top_left, frame.bottom_right
+ local b = opts.frame_border_width
+ ass:draw_start()
+ ass:rect_cw(c1.x, c1.y - b, c2.x + b, c1.y)
+ ass:rect_cw(c2.x, c1.y, c2.x + b, c2.y + b)
+ ass:rect_cw(c1.x - b, c2.y, c2.x, c2.y + b)
+ ass:rect_cw(c1.x - b, c1.y - b, c1.x, c2.y)
+ ass:draw_stop()
+end
+
+function draw_crosshair(ass, center, window_size)
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append("{\\an7}")
+ ass:append("{\\bord0}")
+ ass:append("{\\shad0}")
+ ass:append("{\\c&HBBBBBB&}")
+ ass:append("{\\1a&H00&}")
+ ass:append("{\\2a&HFF&}")
+ ass:append("{\\3a&HFF&}")
+ ass:append("{\\4a&HFF&}")
+ ass:draw_start()
+ ass:rect_cw(center.x - 0.5, 0, center.x + 0.5, window_size.h)
+ ass:rect_cw(0, center.y - 0.5, window_size.w, center.y + 0.5)
+ ass:draw_stop()
+end
+
+function draw_position_text(ass, text, position, window_size, offset)
+ ass:new_event()
+ local align = 1
+ local ofx = 1
+ local ofy = -1
+ if position.x > window_size.w / 2 then
+ align = align + 2
+ ofx = -1
+ end
+ if position.y < window_size.h / 2 then
+ align = align + 6
+ ofy = 1
+ end
+ ass:append("{\\an"..align.."}")
+ ass:append("{\\fs26}")
+ ass:append("{\\bord1.5}")
+ ass:pos(ofx*offset + position.x, ofy*offset + position.y)
+ ass:append(text)
+end
+
+function draw_crop_zone()
+ if needs_drawing then
+ local dim = mp.get_property_native("osd-dimensions")
+ if not dim then
+ cancel_crop()
+ return
+ end
+
+ local cursor = {
+ x = crop_cursor.x,
+ y = crop_cursor.y,
+ }
+ local ass = assdraw.ass_new()
+
+ if crop_first_corner and (opts.draw_shade or opts.draw_frame) then
+ local frame = {}
+ frame.top_left, frame.bottom_right = rect_from_two_points(
+ video_norm_to_screen(crop_first_corner, dim),
+ cursor,
+ rect_centered,
+ rect_keepaspect and dim.w/dim.h)
+ -- don't draw shade over non-visible video parts
+ if opts.draw_shade then
+ local window = {
+ top_left = { x = 0, y = 0 },
+ bottom_right = { x = dim.w, y = dim.h },
+ }
+ draw_shade(ass, frame, window)
+ end
+ if opts.draw_frame then
+ draw_frame(ass, frame)
+ end
+ end
+
+
+ if opts.draw_crosshair then
+ draw_crosshair(ass, cursor, { w = dim.w, h = dim.h })
+ end
+
+ if opts.draw_text then
+ local vop = mp.get_property_native("video-out-params")
+ if vop then
+ local cursor_norm = screen_to_video_norm(cursor, dim)
+ local text = string.format("%d, %d", cursor_norm.x * vop.w, cursor_norm.y * vop.h)
+ if crop_first_corner then
+ text = string.format("%s (%dx%d)", text,
+ math.abs((cursor_norm.x - crop_first_corner.x) * vop.w ),
+ math.abs((cursor_norm.y - crop_first_corner.y) * vop.h )
+ )
+ end
+ draw_position_text(ass, text, cursor, { w = dim.w, h = dim.h }, 6)
+ end
+ end
+
+ mp.set_osd_ass(dim.w, dim.h, ass.text)
+ needs_drawing = false
+ end
+end
+
+function crop_video(x1, y1, x2, y2)
+ if active_mode == "soft" then
+ local w = x2 - x1
+ local h = y2 - y1
+ local dim = mp.get_property_native("osd-dimensions")
+ if not dim then return end
+
+ local zoom = mp.get_property_number("video-zoom")
+ local newZoom1 = math.log(dim.h * (2 ^ zoom) / (dim.h - dim.mt - dim.mb) / h) / math.log(2)
+ local newZoom2 = math.log(dim.w * (2 ^ zoom) / (dim.w - dim.ml - dim.mr) / w) / math.log(2)
+ mp.set_property("video-zoom", math.min(newZoom1, newZoom2))
+ mp.set_property("video-pan-x", 0.5 - (x1 + w / 2))
+ mp.set_property("video-pan-y", 0.5 - (y1 + h / 2))
+ elseif active_mode == "hard" or active_mode == "delogo" then
+ x1 = clamp(0, x1, 1)
+ y1 = clamp(0, y1, 1)
+ x2 = clamp(0, x2, 1)
+ y2 = clamp(0, y2, 1)
+ local vop = mp.get_property_native("video-out-params")
+ local vf_table = mp.get_property_native("vf")
+ local x = math.floor(x1 * vop.w + 0.5)
+ local y = math.floor(y1 * vop.h + 0.5)
+ local w = math.floor((x2 - x1) * vop.w + 0.5)
+ local h = math.floor((y2 - y1) * vop.h + 0.5)
+ if active_mode == "delogo" then
+ -- delogo is a little special and needs some padding to function
+ w = math.min(vop.w - 1, w)
+ h = math.min(vop.h - 1, h)
+ x = math.max(1, x)
+ y = math.max(1, y)
+ if x + w == vop.w then w = w - 1 end
+ if y + h == vop.h then h = h - 1 end
+ end
+ vf_table[#vf_table + 1] = {
+ name=(active_mode == "hard") and "crop" or "delogo",
+ params= { x = tostring(x), y = tostring(y), w = tostring(w), h = tostring(h) }
+ }
+ mp.set_property_native("vf", vf_table)
+ end
+end
+
+function update_crop_zone_state()
+ local dim = mp.get_property_native("osd-dimensions")
+ if not dim then
+ cancel_crop()
+ return
+ end
+ local corner = crop_cursor
+ if crop_first_corner == nil then
+ crop_first_corner = screen_to_video_norm(crop_cursor, dim)
+ redraw()
+ else
+ local c1, c2 = rect_from_two_points(
+ video_norm_to_screen(crop_first_corner, dim),
+ crop_cursor,
+ rect_centered,
+ rect_keepaspect and dim.w/dim.h)
+ local c1norm = screen_to_video_norm(c1, dim)
+ local c2norm = screen_to_video_norm(c2, dim)
+ crop_video(c1norm.x, c1norm.y, c2norm.x, c2norm.y)
+ cancel_crop()
+ end
+end
+
+local bindings = {}
+local bindings_repeat = {}
+
+function cancel_crop()
+ crop_first_corner = nil
+ for key, _ in pairs(bindings) do
+ mp.remove_key_binding("crop-"..key)
+ end
+ for key, _ in pairs(bindings_repeat) do
+ mp.remove_key_binding("crop-"..key)
+ end
+ mp.unobserve_property(redraw)
+ mp.unregister_idle(draw_crop_zone)
+ mp.set_osd_ass(1280, 720, '')
+ active = false
+end
+
+function start_crop(mode)
+ if active then return end
+ if not mp.get_property_native("osd-dimensions") then return end
+ if mode and not mode_ok(mode) then
+ msg.error("Invalid mode value: " .. mode)
+ return
+ end
+ local mode_maybe = mode or opts.mode
+ if mode_maybe ~= 'soft' then
+ local hwdec = mp.get_property("hwdec-current")
+ if hwdec and hwdec ~= "no" and not string.find(hwdec, "-copy$") then
+ msg.error("Cannot crop with hardware decoding active (see manual)")
+ return
+ end
+ end
+ active = true
+ active_mode = mode_maybe
+
+ if opts.mouse_support then
+ crop_cursor.x, crop_cursor.y = mp.get_mouse_pos()
+ end
+ redraw()
+ for key, func in pairs(bindings) do
+ mp.add_forced_key_binding(key, "crop-"..key, func)
+ end
+ for key, func in pairs(bindings_repeat) do
+ mp.add_forced_key_binding(key, "crop-"..key, func, { repeatable = true })
+ end
+ mp.register_idle(draw_crop_zone)
+ mp.observe_property("osd-dimensions", nil, redraw)
+end
+
+function toggle_crop(mode)
+ if mode and not mode_ok(mode) then
+ msg.error("Invalid mode value: " .. mode)
+ end
+ local toggle_mode = mode or opts.mode
+ if toggle_mode == "soft" then return end -- can't toggle soft mode
+
+ local remove_filter = function()
+ local to_remove = (toggle_mode == "hard") and "crop" or "delogo"
+ local vf_table = mp.get_property_native("vf")
+ if #vf_table > 0 then
+ for i = #vf_table, 1, -1 do
+ if vf_table[i].name == to_remove then
+ for j = i, #vf_table-1 do
+ vf_table[j] = vf_table[j+1]
+ end
+ vf_table[#vf_table] = nil
+ mp.set_property_native("vf", vf_table)
+ return true
+ end
+ end
+ end
+ return false
+ end
+ if not remove_filter() then
+ start_crop(mode)
+ end
+end
+
+-- bindings
+if opts.mouse_support then
+ bindings["MOUSE_MOVE"] = function() crop_cursor.x, crop_cursor.y = mp.get_mouse_pos(); redraw() end
+end
+for _, key in ipairs(opts.accept) do
+ bindings[key] = update_crop_zone_state
+end
+for _, key in ipairs(opts.cancel) do
+ bindings[key] = cancel_crop
+end
+function movement_func(move_x, move_y)
+ return function()
+ crop_cursor.x = crop_cursor.x + move_x
+ crop_cursor.y = crop_cursor.y + move_y
+ redraw()
+ end
+end
+bindings_repeat[opts.left_coarse] = movement_func(-opts.coarse_movement, 0)
+bindings_repeat[opts.right_coarse] = movement_func(opts.coarse_movement, 0)
+bindings_repeat[opts.up_coarse] = movement_func(0, -opts.coarse_movement)
+bindings_repeat[opts.down_coarse] = movement_func(0, opts.coarse_movement)
+bindings_repeat[opts.left_fine] = movement_func(-opts.fine_movement, 0)
+bindings_repeat[opts.right_fine] = movement_func(opts.fine_movement, 0)
+bindings_repeat[opts.up_fine] = movement_func(0, -opts.fine_movement)
+bindings_repeat[opts.down_fine] = movement_func(0, opts.fine_movement)
+
+
+mp.add_key_binding(nil, "start-crop", start_crop)
+mp.add_key_binding(nil, "toggle-crop", toggle_crop)
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)