From 8a56e69f33aecd81ef957e50c8bdfd57f3bdfc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Av=C3=A9?= Date: Fri, 13 Mar 2026 14:54:48 +0700 Subject: [PATCH] Add frame seeking script for mpv --- home/mpv/files/scripts/frame_seek.lua | 180 ++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 home/mpv/files/scripts/frame_seek.lua diff --git a/home/mpv/files/scripts/frame_seek.lua b/home/mpv/files/scripts/frame_seek.lua new file mode 100644 index 0000000..ab8ca8b --- /dev/null +++ b/home/mpv/files/scripts/frame_seek.lua @@ -0,0 +1,180 @@ +-- frame-seek.lua +-- Allows seeking to a specific frame number or timestamp + +local input = require("mp.input") + +local jump_mode = nil -- "frame" or "time" +local relative = false +local minus = false +local fps = 0 + +function parse_timestamp(input_str) + -- Formats: + -- HH:MM:SS.ms + -- MM:SS.ms + -- SS.ms + -- .ms + + -- More than 60 minutes or seconds can be entered - it will seek any amount accurately + + -- First try to match HH:MM:SS.ms + local hours, minutes, seconds = input_str:match("^(%d+):(%d+):(%d+%.?%d*)$") + if hours and minutes and seconds then + return tonumber(hours) * 3600 + tonumber(minutes) * 60 + tonumber(seconds) + end + + -- Try to match MM:SS.ms + local minutes, seconds = input_str:match("^(%d+):(%d+%.?%d*)$") + if minutes and seconds then + return tonumber(minutes) * 60 + tonumber(seconds) + end + + -- Try to match just seconds (with or without decimal) + local seconds = input_str:match("^(%d+%.?%d*)$") + if seconds then + return tonumber(seconds) + end + + local milliseconds = input_str:match("^%.(%d+)$") + if milliseconds ~= nil then + return tonumber("0." .. milliseconds) + end + + return nil +end + +function seek_to_frame(frame_num) + fps = mp.get_property_number("estimated-vf-fps") + if not fps or fps <= 0 then + mp.osd_message("Error: Cannot determine framerate") + return + end + + local timestamp = frame_num / fps + + seek_to_timestamp(timestamp) +end + +function seek_to_timestamp(timestamp) + if minus then timestamp = -timestamp end + + local cur_time = mp.get_property_number("time-pos") + if not cur_time then return end + + if relative then + if timestamp == 0 then return end + + if math.abs(timestamp) < 10 then + mp.commandv("seek", timestamp, "exact") + else + -- Only show OSD if seek >10s + mp.command("seek " .. timestamp .. " exact") + end + else + -- Handle imprecise float + if math.abs(timestamp - cur_time) < 1e-7 then return end + mp.command("seek " .. timestamp .. " absolute+exact") + end + + mp.observe_property("time-pos", "number", display_osd_message) +end + +function display_osd_message(_, timestamp) + if timestamp == nil then return end + mp.unobserve_property(display_osd_message) + + -- Format the display nicely + local hours = math.floor(timestamp / 3600) + local minutes = math.floor((timestamp % 3600) / 60) + local seconds = math.floor(timestamp % 60) + local milliseconds = math.floor((timestamp % 1) * 1000 + 0.5) + + local display_time = string.format("%02d:%02d", minutes, seconds) + + if hours ~= 0 then + display_time = string.format("%d:", hours) .. display_time + end + + if milliseconds ~= 0 or jump_mode == "frame" then + display_time = display_time .. string.format(".%03d", milliseconds) + end + + if jump_mode == "frame" and fps and fps > 0 then + local frame_num = math.floor(timestamp * fps + 0.5) + mp.osd_message(string.format("Seeking to frame %d (%s)", frame_num, display_time)) + else + mp.osd_message(string.format("Seeking to %s", display_time)) + end +end + +function jump_submit(input) + if not input or input == "" then + reset() + return + end + + -- Handle relative marker + if input:sub(1, 1) == "r" then + relative = true + input = input:sub(2) + end + + -- Handle negative input + minus = false + if input:sub(1, 1) == "-" then + minus = true + input = input:sub(2) + end + + if jump_mode == "frame" then + local frame_num = tonumber(input) + if frame_num then + seek_to_frame(math.floor(frame_num)) + else + mp.osd_message("Invalid frame number") + end + elseif jump_mode == "time" then + local timestamp = parse_timestamp(input) + if timestamp then + seek_to_timestamp(timestamp) + else + mp.osd_message("Invalid timestamp format") + end + end +end + +function reset() + jump_mode = nil + relative = false + minus = false +end + +function run_script(mode, prompt, relative_flag) + if mp.get_property("path") == nil then return end + + reset() + jump_mode = mode + relative = relative_flag + + input.get({ + prompt = prompt, + submit = jump_submit, + }) +end + +-- Register key bindings +mp.add_key_binding("ctrl+t", "seek-timestamp", function() + run_script("time", "Seek to time:", false) +end) + +mp.add_key_binding("ctrl+T", "seek-frame", function() + run_script("frame", "Seek to frame:", false) +end) + +mp.add_key_binding(nil, "seek-timestamp-relative", function() + run_script("time", "Seek forward by time:", true) +end) + +mp.add_key_binding(nil, "seek-frame-relative", function() + run_script("frame", "Seek forward by frame:", true) +end)