Compare commits

...

No commits in common. "0cd6d549dc0fcb1e9294db0ec3d0297069f9e59e" and "9d0c7dcd969bd2d4fc37c027aeb8f261c78f4ce2" have entirely different histories.

6 changed files with 482 additions and 5 deletions

View File

@ -10,3 +10,6 @@ https://music.youtube.com/playlist?list=OLAK5uy_nscRtcc2KZcydiVY7q0feukzm177h8yy
mpv 'https://music.youtube.com/playlist?list=OLAK5uy_nscRtcc2KZcydiVY7q0feukzm177h8yyw'
```
You can even skip songs forwards and backwards like you can with local files using the < and > keys.
## crop.lua
Found on https://github.com/occivink/mpv-scripts#croplua. Useful for the rare occasion that I want mpv to show only part of the screen.

View File

@ -193,7 +193,7 @@ Alt+- add video-zoom -0.01
# crop.lua
# ==========
# start cropping
#c script-message-to crop start-crop hard
c script-message-to crop start-crop hard
#alt+c script-message-to crop start-crop soft
# delogo mode can be used like so
#l script-message-to crop start-crop delogo

View File

@ -11,7 +11,8 @@ keep-open=yes
volume=60
#reset-on-next-file=volume
#osd-level=0
ytdl-format="bestvideo[ext=webm][height<=1080]+bestaudio[ext=webm]/best[ext=webm]/best"
#ytdl-format="bestvideo[ext=webm][height<=1080]+bestaudio[ext=webm]/best[ext=webm]/best"
ytdl-format="bestvideo[height<=1080]+bestaudio/best"
#script-opts=ytdl_hook-ytdl_path=/usr/bin/yt-dlp
volume-max=170
#msg-level=all=no

434
scripts/crop.lua Normal file
View File

@ -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)

View File

@ -130,13 +130,15 @@ local function copyffplay()
-- default crops setting to start with (puts the crop at about the center of the screen).
-- Subtracting 119 from the input height is just enough to crop the ticker off the
-- bottom of the weathernews stream
local c = "crop=900:ih-119:400:0"
local c = "crop=900:ih-119:500:0"
-- constrained quality https://trac.ffmpeg.org/wiki/Encode/VP9#constrainedq
local cq = " -crf 32 -b:v 2200k "
-- I usually run mpv from Dired in emacs, using the "openwith" package, which calls mpv
-- with the full path to the video, so the copied ffplay command can find the file from
-- any working-directory. Running mpv from the shell sets the mpv's "path" property to
-- whatever path you used on the command line.
local f = mp.get_property("path")
local cmd = "ffplay " .. ist(b) .. " -i " .. "'file:" .. f .. "'" .. isss(a) .. " -vf " .. c
local cmd = "ffplay " .. ist(b) .. " -i " .. "'file:" .. f .. "'" .. isss(a) .. cq .. " -vf " .. c
if set_clipboard(cmd) then
mp.osd_message(string.format("Copied to Clipboard: %s", cmd))
else

View File

@ -3,13 +3,50 @@
# bash, in case I want to source them from another shell
#
# I'll eventually integrate my entire shell config into my Nix home-manager config
# aqstream tv channel aliases
alias tbs='mpv https://tbs3.mov3.co/hls/tbs.m3u8' \
ntv='mpv https://ntv4.mov3.co/hls/ntv.m3u8' \
fujitv='mpv https://fujitv4.mov3.co/hls/fujitv.m3u8'
# curl wni json schedule, cache it in /tmp, and print
alias wnischedule='curl -w "\n" "https://smtgvs.weathernews.jp/a/solive_timetable/timetable.json" \
>/tmp/wnischedule; cat /tmp/wnischedule'
alias ffmpeg="ffmpeg -benchmark "
# How I take screenshots. Change the output directory suit your system.
# Documented under "INPUT DEVICES" > "x11grab" in ffmpeg-all man page
alias sc='ffmpeg -y -f x11grab -video_size 1920x1080 -select_region 1 -i $DISPLAY \
-vframes 1 ~/写真/スクショ/$(date +"%g%m%d_%R:%S").jpg'
-vframes 1 ~/写真/スクショ/$(date +"%g%m%d_%R:%S").jpg'
# Makes an audio-less webm of a saved file and puts it in /tmp for uploading to /jp/
rmaudio() {
ffmpeg -an -i $1 -c copy /tmp/$( basename $1 )
}
# Converts youtube stream url into the chat-only url
ytchat() {
local URL
if [ $# -gt 0 ]; then
URL=$1
else
URL="$(xclip -se c -o)"
fi
URL="https://www.youtube.com/live_chat?is_popout=1&v="${URL#*=}
xclip -se c <<<"$URL"
echo $URL
}
# concats 2 or more webms specified in /tmp/a
## learned from this
# ffmpeg concat videos, second answer
# https://stackoverflow.com/questions/64866231/remove-a-section-from-the-middle-of-a-video-without-concat
# ffmpeg -i input.mp4 -t "$start_cut_section" -c copy part1.mp4&
# ffmpeg -i input.mp4 -ss "$end_cut_section" -c copy part2.mp4&
# echo "file 'part1.mp4'" > filelist;
# echo "file 'part2.mp4'" >> filelist;
# wait;
# ffmpeg -f concat -i filelist -c copy output.mp4;
# rm filelist;
alias concatvid="cd /tmp/; ffmpeg -y -f concat -i a -c copy /tmp/concat.webm; cd -"