Skip to content

Instantly share code, notes, and snippets.

@dardo82
Last active April 24, 2022 22:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dardo82/bf19d4d39a7f0882b9a32eae9e0199db to your computer and use it in GitHub Desktop.
Save dardo82/bf19d4d39a7f0882b9a32eae9e0199db to your computer and use it in GitHub Desktop.
MPV configuration
[default]
pause
keep-open
fullscreen
geometry=0:0
hwdec=vaapi
framedrop=vo
alang=it,en
ao=coreaudio
af=scaletempo
osd-level=3
osd-fractions
slang=it
sub-scale=1
ytdl-format=best
ytdl-raw-options=write-auto-sub=,sub-lang="it,en"
hr-seek=yes
input-ipc-server=/tmp/mpvsocket
local opts = {
draw_shade = true,
shade_opacity = "77",
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
opts.accept = split(opts.accept)
opts.cancel = split(opts.cancel)
local assdraw = require 'mp.assdraw'
local msg = require 'mp.msg'
local needs_drawing = false
local dimensions_changed = false
local crop_first_corner = nil -- in video space
local crop_cursor = {
x = -1,
y = -1
}
function get_video_dimensions()
if not dimensions_changed then return _video_dimensions end
-- 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 return nil end
dimensions_changed = false
local keep_aspect = mp.get_property_bool("keepaspect")
local w = video_params["w"]
local h = video_params["h"]
local dw = video_params["dw"]
local dh = video_params["dh"]
if mp.get_property_number("video-rotate") % 180 == 90 then
w, h = h,w
dw, dh = dh, dw
end
_video_dimensions = {
top_left = {},
bottom_right = {},
ratios = {},
}
if keep_aspect then
local unscaled = mp.get_property_native("video-unscaled")
local panscan = mp.get_property_number("panscan")
local window_w, window_h = mp.get_osd_size()
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.x, _video_dimensions.bottom_right.x = split_scaling(window_w, scaled_width, zoom, align_x, pan_x)
local align_y = mp.get_property_number("video-align-y")
local pan_y = mp.get_property_number("video-pan-y")
_video_dimensions.top_left.y, _video_dimensions.bottom_right.y = split_scaling(window_h, scaled_height, zoom, align_y, pan_y)
else
_video_dimensions.top_left.x = 0
_video_dimensions.bottom_right.x = window_w
_video_dimensions.top_left.y = 0
_video_dimensions.bottom_right.y = window_h
end
_video_dimensions.ratios.w = w / (_video_dimensions.bottom_right.x - _video_dimensions.top_left.x)
_video_dimensions.ratios.h = h / (_video_dimensions.bottom_right.y - _video_dimensions.top_left.y)
return _video_dimensions
end
function sort_corners(c1, c2)
local r1, r2 = {}, {}
if c1.x < c2.x then r1.x, r2.x = c1.x, c2.x else r1.x, r2.x = c2.x, c1.x end
if c1.y < c2.y then r1.y, r2.y = c1.y, c2.y else r1.y, r2.y = c2.y, c1.y end
return r1, r2
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(point, video_dim)
return {
x = math.floor(video_dim.ratios.w * (point.x - video_dim.top_left.x) + 0.5),
y = math.floor(video_dim.ratios.h * (point.y - video_dim.top_left.y) + 0.5)
}
end
function video_to_screen(point, video_dim)
return {
x = math.floor(point.x / video_dim.ratios.w + video_dim.top_left.x + 0.5),
y = math.floor(point.y / video_dim.ratios.h + video_dim.top_left.y + 0.5)
}
end
function draw_shade(ass, unshaded, video)
ass:new_event()
ass:pos(0, 0)
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 = video
-- 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_crosshair(ass, center, window_size)
ass:new_event()
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:pos(0, 0)
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 video_dim = get_video_dimensions()
if not video_dim then
cancel_crop()
return
end
local window_size = {}
window_size.w, window_size.h = mp.get_osd_size()
crop_cursor = clamp_point(video_dim.top_left, crop_cursor, video_dim.bottom_right)
local ass = assdraw.ass_new()
if opts.draw_shade and crop_first_corner then
local first_corner = video_to_screen(crop_first_corner, video_dim)
local unshaded = {}
unshaded.top_left, unshaded.bottom_right = sort_corners(first_corner, crop_cursor)
-- don't draw shade over non-visible video parts
local window = {
top_left = { x = 0, y = 0 },
bottom_right = { x = window_size.w, y = window_size.h },
}
local video_visible = {
top_left = clamp_point(window.top_left, video_dim.top_left, window.bottom_right),
bottom_right = clamp_point(window.top_left, video_dim.bottom_right, window.bottom_right),
}
draw_shade(ass, unshaded, video_visible)
end
if opts.draw_crosshair then
draw_crosshair(ass, crop_cursor, window_size)
end
if opts.draw_text then
cursor_video = screen_to_video(crop_cursor, video_dim)
local text = string.format("%d, %d", cursor_video.x, cursor_video.y)
if crop_first_corner then
text = string.format("%s (%dx%d)", text,
math.abs(cursor_video.x - crop_first_corner.x),
math.abs(cursor_video.y - crop_first_corner.y)
)
end
draw_position_text(ass, text, crop_cursor, window_size, 6)
end
mp.set_osd_ass(window_size.w, window_size.h, ass.text)
needs_drawing = false
end
end
function crop_video(x, y, w, h)
local vf_table = mp.get_property_native("vf")
vf_table[#vf_table + 1] = {
name="crop",
params= {
x = tostring(x),
y = tostring(y),
w = tostring(w),
h = tostring(h)
}
}
mp.set_property_native("vf", vf_table)
end
function update_crop_zone_state()
local dim = get_video_dimensions()
if not dim then
cancel_crop()
return
end
crop_cursor = clamp_point(dim.top_left, crop_cursor, dim.bottom_right)
corner_video = screen_to_video(crop_cursor, dim)
if crop_first_corner == nil then
crop_first_corner = corner_video
needs_drawing = true
else
local c1, c2 = sort_corners(crop_first_corner, corner_video)
crop_video(c1.x, c1.y, c2.x - c1.x, c2.y - c1.y)
cancel_crop()
end
end
function reset_crop()
dimensions_changed = true
needs_drawing = true
end
local bindings = {}
local bindings_repeat = {}
function cancel_crop()
needs_drawing = false
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(reset_crop)
mp.unregister_idle(draw_crop_zone)
mp.set_osd_ass(1280, 720, '')
end
-- bindings
if opts.mouse_support then
bindings["MOUSE_MOVE"] = function() crop_cursor.x, crop_cursor.y = mp.get_mouse_pos(); needs_drawing = true 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
needs_drawing = true
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)
local properties = {
"keepaspect",
"video-out-params",
"video-unscaled",
"panscan",
"video-zoom",
"video-align-x",
"video-pan-x",
"video-align-y",
"video-pan-y",
"osd-width",
"osd-height",
}
function start_crop()
if not mp.get_property("video-out-params", nil) then return end
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
crop_cursor.x, crop_cursor.y = mp.get_mouse_pos()
needs_drawing = true
dimensions_changed = true
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)
for _, p in ipairs(properties) do
mp.observe_property(p, "native", reset_crop)
end
end
function toggle_crop()
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 == "crop" 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
end
end
end
start_crop()
end
mp.add_key_binding(nil, "start-crop", start_crop)
mp.add_key_binding(nil, "toggle-crop", toggle_crop)
# see encode_webm.conf for a detailed explanations of all the options
only_active_tracks=yes
preserve_filters=yes
append_filter=
codec=-scodec mov_text
output_format=$f_$n.$x
output_directory=
detached=yes
ffmpeg_command=ffmpeg
print=yes
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 is_stream = not file_exists(path)
local format = mp.get_property("file-format")
local candidate = string.match(path, "%.([^.]+)$")
if candidate then
for _, ext in ipairs({ "mkv", "webm", "mp4", "avi" }) do
if candidate == ext then
return candidate
else
if is_stream then
if format == "mov,mp4,m4a,3gp,3g2,mj2" then
return "mp4"
end
end
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", input)
output = string.gsub(output, "$t", title)
output = string.gsub(output, "$s", seconds_to_time_string(from, true))
output = string.gsub(output, "$e", seconds_to_time_string(to, true))
output = string.gsub(output, "$d", seconds_to_time_string(to-from, true))
output = string.gsub(output, "$x", extension)
output = string.gsub(output, "$p", profile)
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"]
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)
# MPV input devices configuration
LEFT frame-back-step # -1
RIGHT frame-step # 1
UP frame-back-step # -3
DOWN frame-step # 3
PGUP frame-back-step # -9
PGDWN frame-step # 9
HOME frame-back-step # -27
END frame-step # 27
Ctrl+LEFT seek -1
Ctrl+RIGHT seek 1
Ctrl+UP seek -3
Ctrl+DOWN seek 3
Ctrl+PGUP seek -9
Ctrl+PGDWN seek 9
Ctrl+HOME seek -27
Ctrl+END seek 27
Alt+LEFT seek -60
Alt+RIGHT seek 60
Alt+UP seek -180
Alt+DOWN seek 180
Alt+PGUP seek -540
Alt+PGDWN seek 540
Alt+HOME seek -1620
Alt+END seek 1620
Meta+LEFT seek -3600
Meta+RIGHT seek 3600
Meta+UP seek -10800
Meta+DOWN seek 10800
Meta+PGUP seek -32400
Meta+PGDWN seek 32400
Meta+HOME seek -97200
Meta+END seek 97200
Shift+LEFT add chapter -1
Shift+RIGHT add chapter 1
Shift+UP add chapter -3
Shift+DOWN add chapter 3
Shift+PGUP add chapter -9
Shift+PGDWN add chapter 9
Shift+HOME add chapter -27
Shift+END add chapter 27
< seek 0 absolute-percent
> seek 100 absolute-percent
BS playlist-prev
ENTER playlist-next
v add volume -10
V add volume 10
s multiply speed 0.5
S multiply speed 2.0
c script-message-to crop toggle-crop
e script-message-to encode set-timestamp encode
t script-message-to seek_to toggle-seeker
@dardo82
Copy link
Author

dardo82 commented Feb 3, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment