Files
mpv-scripts/scripts/screenshot-duo.lua
harley 7a6a5fc1f2 Initial import: mpv config + scripts
- mpv.conf: screenshot dir, loop-file
- input.conf: zoom/pan/export/screenshot bindings
- export-loop.lua (e): A-B loop to MP4 with zoom/pan/brightness bake
- export-loop-webp.lua (W): A-B loop to animated WebP + GIF
- screenshot-duo.lua (S): raw+on-screen dual screenshots
- webp-anim-bridge.lua: animated WebP playback via ImageMagick
- transcribe-subtitles.lua (T): faster-whisper SRT export
2026-07-07 23:53:06 -04:00

177 lines
6.7 KiB
Lua

-- screenshot-duo.lua
-- Press S to capture two screenshots at the current frame:
-- _normal.png — raw frame via ffmpeg (no adjustments)
-- _modified.png — what you see on screen (gamma/brightness/etc baked in)
--
-- Suggested input.conf binding:
-- S script-binding screenshot_duo/capture
--
-- Saves to ~/Videos/mpv-loops/.
local mp = require("mp")
local utils = require("mp.utils")
local OUTPUT_DIR = os.getenv("HOME") .. "/Videos/mpv-loops"
local ENABLE_OSD_MSG = true
local function osd_msg(text, duration)
if ENABLE_OSD_MSG then
mp.osd_message(text, duration or 3)
end
end
local function format_label(seconds)
local h = math.floor(seconds / 3600)
local m = math.floor((seconds % 3600) / 60)
local s = math.floor(seconds % 60)
return string.format("%02d%02d%02d", h, m, s)
end
local function dedup_path(base_path, suffix, ext)
local candidate = string.format("%s_%s.%s", base_path, suffix, ext)
local counter = 1
while true do
local f = io.open(candidate, "r")
if not f then return candidate end
f:close()
candidate = string.format("%s_%s_%d.%s", base_path, suffix, counter, ext)
counter = counter + 1
end
end
local function format_size(path)
local info = utils.file_info(path)
if not info or not info.size then return "" end
local s = info.size
if s > 1024 * 1024 then
return string.format(" (%.1f MB)", s / 1024 / 1024)
elseif s > 1024 then
return string.format(" (%.0f KB)", s / 1024)
else
return string.format(" (%d B)", s)
end
end
-- Build crop filter string for current zoom/pan.
-- Returns nil if no zoom/pan is active (no filter needed).
local function zoom_pan_crop()
local zoom = mp.get_property_number("video-zoom", -999)
local pan_x = mp.get_property_number("video-pan-x", -999)
local pan_y = mp.get_property_number("video-pan-y", -999)
mp.msg.warn(string.format("[zp] raw: zoom=%+5.2f pan_x=%+5.2f pan_y=%+5.2f", zoom, pan_x, pan_y))
if zoom <= 0 then
mp.msg.warn("[zp] zoom <= 0, skipping crop")
return nil
end
if pan_x == -999 then pan_x = 0 end
if pan_y == -999 then pan_y = 0 end
local s = 2 ^ zoom
local filter = string.format("crop=iw/%g:ih/%g:iw*(0.5-0.5/%g-%g):ih*(0.5-0.5/%g-%g)",
s, s, s, pan_x, s, pan_y)
mp.msg.warn(string.format("[zp] filter=%s", filter))
mp.osd_message(string.format("ZOOM=%.2f PAN_X=%.2f PAN_Y=%.2f", zoom, pan_x, pan_y), 3)
return filter
end
local function capture_pair()
-- 1. Get current position and file path
local time_pos = mp.get_property_number("time-pos")
if not time_pos then
osd_msg("No video position.", 2)
return
end
local file_path = mp.get_property("path")
if not file_path then
osd_msg("No file loaded.", 2)
return
end
-- 2. Build filenames
local filename_stripped = mp.get_property("filename/no-ext") or "video"
local basename = filename_stripped:gsub("[^%w._-]", "_")
local label = format_label(time_pos)
local stem = string.format("%s_%s", basename, label)
local base_path = utils.join_path(OUTPUT_DIR, stem)
local ext = "png"
local normal_path = dedup_path(base_path, "normal", ext)
local modified_path = dedup_path(base_path, "modified", ext)
-- 3. Ensure output directory exists
mp.command_native({"run", "mkdir", "-p", OUTPUT_DIR})
-- 4. Build ffmpeg args for normal screenshot
local crop_filter = zoom_pan_crop()
local ffmpeg_args = {"ffmpeg", "-y"}
local src = mp.get_property("stream-path") or mp.get_property("path", "")
local mf_prefix = "mf:///tmp/mpv-webp-anim/"
if src:sub(1, #mf_prefix) == mf_prefix or src:find("/tmp/mpv-webp-anim/") then
-- mf:// from webp-anim-bridge: use specific frame
local pattern = src
if pattern:sub(1, 5) == "mf://" then pattern = pattern:sub(6) end
local mf_fps = mp.get_property_number("mf-fps", 10)
local frame_num = math.floor(time_pos * mf_fps)
local frame_file = pattern:gsub("%%04d", string.format("%04d", frame_num))
if frame_file == pattern then
local dir = pattern:match("^(.*/)") or "."
frame_file = dir .. string.format("frame_%04d.png", frame_num)
end
table.insert(ffmpeg_args, "-i"); table.insert(ffmpeg_args, frame_file)
table.insert(ffmpeg_args, "-vframes"); table.insert(ffmpeg_args, "1")
table.insert(ffmpeg_args, "-q:v"); table.insert(ffmpeg_args, "3")
else
table.insert(ffmpeg_args, "-ss"); table.insert(ffmpeg_args, string.format("%06.3f", time_pos))
table.insert(ffmpeg_args, "-i"); table.insert(ffmpeg_args, src)
table.insert(ffmpeg_args, "-vframes"); table.insert(ffmpeg_args, "1")
table.insert(ffmpeg_args, "-q:v"); table.insert(ffmpeg_args, "3")
end
if crop_filter then
table.insert(ffmpeg_args, "-vf")
table.insert(ffmpeg_args, crop_filter)
end
table.insert(ffmpeg_args, normal_path)
-- 5. Modified screenshot via mpv (captures current on-screen image with adjustments)
mp.commandv("screenshot-to-file", modified_path)
-- 6. Launch ffmpeg async (background, no flashing)
osd_msg("Capturing screenshots ...", 2)
print(string.format("[screenshot-duo] Normal: %s", normal_path))
print(string.format("[screenshot-duo] Modified: %s", modified_path))
mp.command_native({
name = "subprocess",
args = ffmpeg_args,
playback_only = false,
capture_stdout = false,
capture_stderr = false,
}, function(success, result, err)
if success then
local mpv_info = utils.file_info(modified_path)
local mpv_size = ""
if mpv_info and mpv_info.size then
if mpv_info.size > 1024 * 1024 then
mpv_size = string.format(" (%.1f MB)", mpv_info.size / 1024 / 1024)
elseif mpv_info.size > 1024 then
mpv_size = string.format(" (%.0f KB)", mpv_info.size / 1024)
else
mpv_size = string.format(" (%d B)", mpv_info.size)
end
end
osd_msg(string.format("Screenshots: %s%s, %s%s",
normal_path, format_size(normal_path),
modified_path, mpv_size), 5)
print(string.format("[screenshot-duo] Both saved: %s, %s", normal_path, modified_path))
else
-- mpv screenshot succeeded but ffmpeg failed
osd_msg("Normal screenshot failed. Modified saved.", 4)
print("[screenshot-duo] ffmpeg ERROR: " .. (err or "unknown"))
end
end)
end
mp.add_key_binding(nil, "capture", capture_pair)