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