-- evafast.lua -- -- Much speed. -- -- Jumps forwards when right arrow is tapped, speeds up when it's held. -- Inspired by bilibili.com's player. Allows you to have both seeking and fast-forwarding on the same key. -- Also supports toggling fastforward mode with a keypress. -- Adjust --input-ar-delay to define when to start fastforwarding. -- Define --hr-seek if you want accurate seeking. -- If you just want a nicer fastforward.lua without hybrid key behavior, set seek_distance to 0. -- Consider setting --sub-filter-regex="\`\s*\'" (on Linux) to ignore empty lines. local options = { -- How far to jump on press, set to 0 to disable seeking and force fastforward seek_distance = 5, -- Playback speed modifier, applied once every speed_interval until cap is reached speed_increase = 0.1, speed_decrease = 0.1, -- At what interval to apply speed modifiers speed_interval = 0.05, -- Playback speed cap speed_cap = 2, -- Playback speed cap when subtitles are displayed, ignored when equal to speed_cap subs_speed_cap = 1.6, -- Multiply current speed by modifier before adjustment (exponential speedup) -- Use much lower values than default e.g. speed_increase=0.05, speed_decrease=0.025 multiply_modifier = false, -- Show current speed on the osd (or flash speed if using uosc) show_speed = true, -- Show current speed on the osd when toggled (or flash speed if using uosc) show_speed_toggled = true, -- Show current speed on the osd when speeding up towards a target time (or flash speed if using uosc) show_speed_target = false, -- Show seek actions on the osd (or flash timeline if using uosc) show_seek = true, -- Look ahead for smoother transition when subs_speed_cap is set subs_lookahead = true, -- Symbols prepended to the osd message osd_symbol = "{\\fnmpv-osd-symbols} {\\r}", osd_rewind = "{\\fnmpv-osd-symbols} {\\r}" } mp.options = require "mp.options" mp.options.read_options(options, "evafast", function() end) local uosc_available = false local has_subtitle = true local speedup_target = nil local toggled_display = true local toggled = false local toggled_rewind = false local speedup = false local original_speed = 1 local next_sub_at = -1 local rewinding = false local forced_slowdown = false local file_duration = 0 local last_key_state = "up" local was_rewinding = false local ass_start = mp.get_property_osd("osd-ass-cc/0") local ass_stop = mp.get_property_osd("osd-ass-cc/1") local function speed_transition(current_speed, target_speed) local speed_correction = current_speed >= target_speed and -options.speed_decrease or options.speed_increase local time_for_correction = 0 local adjusted_speed = current_speed while adjusted_speed ~= target_speed do time_for_correction = time_for_correction + options.speed_interval * adjusted_speed if options.multiply_modifier then adjusted_speed = adjusted_speed + adjusted_speed * speed_correction else adjusted_speed = adjusted_speed + speed_correction end if (current_speed < target_speed and adjusted_speed > target_speed) or (current_speed > target_speed and adjusted_speed < target_speed) then adjusted_speed = target_speed end end return time_for_correction end local function next_sub(current_time) local sub_delay = mp.get_property_native("sub-delay", 0) local sub_visible = mp.get_property_bool("sub-visibility") if sub_visible then mp.set_property_bool("sub-visibility", false) end mp.command("no-osd sub-step 1") local sub_next_delay = mp.get_property_native("sub-delay", 0) mp.set_property("sub-delay", sub_delay) if sub_visible then mp.set_property_bool("sub-visibility", sub_visible) end if sub_delay - sub_next_delay == 0 then return -2 end local sub_next = current_time + sub_delay - sub_next_delay normalized = math.floor(sub_next * 1000 + 0.5) / 1000 return normalized end local function flash_state(current_speed, display, forced) local uosc_show = uosc_available and (display == nil or display == "uosc") local osd_show = not uosc_available and (display == nil or display == "osd") local show_special = (not speedup_target and options.show_speed_toggled) or (speedup_target and options.show_speed_target) local show_toggled = show_special and (toggled or not speedup) local show_regular = not toggled and toggled_display and options.show_speed if current_speed and (show_regular or show_toggled or forced) then if uosc_show then mp.command("script-binding uosc/flash-speed") elseif osd_show then if current_speed == true then current_speed = mp.get_property_number("speed", 1) end mp.osd_message(ass_start .. (was_rewinding and options.osd_rewind or options.osd_symbol) .. ass_stop .. string.format("x%.1f", current_speed)) end elseif not current_speed and options.show_seek then if uosc_show then mp.command("script-binding uosc/flash-timeline") elseif osd_show then mp.osd_message(ass_start .. (was_rewinding and options.osd_rewind or options.osd_symbol)) end end end local function ensure_timer(reset) if not reset and speed_timer:is_enabled() then return end speed_timer.timeout = 0 speed_timer:resume() speed_timer.timeout = options.speed_interval end local function evafast_speedup(toggle) if not toggled and not speedup_target and not speed_timer:is_enabled() then original_speed = mp.get_property_number("speed", 1) end speedup = true if toggle then toggled = true end ensure_timer() end local function evafast_slowdown(display) forced_slowdown = false if not display then toggled_display = false end toggled = false speedup = false ensure_timer() end local function evafast_toggle() if toggled_rewind then mp.set_property("play-dir", "+") end toggled_rewind = false if speedup then evafast_slowdown() else evafast_speedup(true) end end local function evafast_toggle_rewind() rewinding = not speedup mp.set_property("play-dir", rewinding and "-" or "+") evafast_toggle() toggled_rewind = rewinding end local function adjust_speed() local current_time = mp.get_property_number("time-pos", 0) local current_speed = mp.get_property_number("speed", 1) local target_speed = original_speed if speedup then target_speed = options.speed_cap if has_subtitle and target_speed ~= options.subs_speed_cap then local sub_displayed = mp.get_property("sub-start") ~= nil if sub_displayed then target_speed = options.subs_speed_cap elseif options.subs_lookahead then if next_sub_at < current_time and next_sub_at ~= -2 then next_sub_at = next_sub(current_time) end if target_speed ~= options.subs_speed_cap and next_sub_at > current_time then local time_for_correction = speed_transition(options.speed_cap, options.subs_speed_cap) if current_time + time_for_correction >= next_sub_at then target_speed = options.subs_speed_cap end end end end if speedup_target ~= nil then local effective_speedup_target = speedup_target >= 0 and speedup_target or (file_duration + speedup_target) if current_time >= effective_speedup_target then evafast_slowdown() else local time_for_correction = speed_transition(current_speed, original_speed) if current_time + time_for_correction > effective_speedup_target or forced_slowdown then forced_slowdown = true speedup = false target_speed = original_speed end end end end if math.floor(target_speed * 1000 + 0.5) == math.floor(current_speed * 1000 + 0.5) then if forced_slowdown or (not toggled and (not speedup or options.subs_speed_cap == options.speed_cap or (not has_subtitle and not speedup_target))) then speed_timer:kill() toggled_display = true if speedup_target ~= nil then evafast_slowdown() end speedup_target = nil end return end local new_speed = current_speed local speed_correction = 0 if options.multiply_modifier then speed_correction = current_speed * options.speed_increase else speed_correction = options.speed_increase end if current_speed > target_speed then new_speed = math.max(current_speed - speed_correction, target_speed) else new_speed = math.min(current_speed + speed_correction, target_speed) end mp.set_property("speed", new_speed) flash_state(new_speed) end speed_timer = mp.add_periodic_timer(100, adjust_speed) speed_timer:kill() local function evafast(keypress, rewind) was_rewinding = false if rewinding and not toggled_rewind and (not rewind or (keypress["event"] == "up" and last_key_state ~= "down")) then rewinding = false was_rewinding = true mp.set_property("play-dir", "+") end if rewind then was_rewinding = true end if keypress["event"] == "down" then if not speed_timer:is_enabled() then if not toggled and not speedup_target then original_speed = mp.get_property_number("speed", 1) end flash_state(nil, "osd") flash_state(1, "uosc", true) end toggled_display = true speed_timer:stop() if options.seek_distance == 0 then keypress["event"] = "repeat" end end if keypress["event"] == "press" or keypress["event"] == "up" and last_key_state ~= "repeat" then if not toggled and not speedup_target then speed_timer:kill() mp.set_property("speed", original_speed) end flash_state() ensure_timer() if rewind then if not toggled_rewind then rewinding = false mp.set_property("play-dir", "+") -- unnecessary in some cases end mp.commandv("seek", -options.seek_distance) else mp.commandv("seek", options.seek_distance) end elseif keypress["event"] == "repeat" and last_key_state ~= "repeat" then speedup = true ensure_timer() if rewind then mp.set_property("play-dir", "-") rewinding = true end elseif keypress["event"] == "up" and not toggled and not speedup_target then evafast_slowdown(true) ensure_timer(true) end last_key_state = keypress["event"] end local function evafast_rewind(keypress) evafast(keypress, true) end mp.observe_property("duration", "native", function(prop, val) file_duration = val or 0 end) mp.observe_property("sid", "native", function(prop, val) has_subtitle = (val or 0) ~= 0 next_sub_at = -1 end) mp.observe_property("sub-start", "native", function(prop, val) next_sub_at = -1 end) mp.register_event("file-loaded", function() next_sub_at = -1 end) mp.register_event("seek", function() next_sub_at = -1 end) mp.register_script_message("uosc-version", function(version) uosc_available = true end) mp.register_script_message("speedup-target", function(time) local current_time = mp.get_property_number("time-pos", 0) sign = string.sub(time, 1, 1) time = tonumber(time) or 0 if sign == "+" then time = current_time + time end if current_time >= time and time >= 0 then speedup_target = nil evafast_slowdown() return end speedup_target = time evafast_speedup() end) mp.register_script_message("get-version", function(script) mp.commandv("script-message-to", script, "evafast-version", "2.0") end) mp.add_key_binding("RIGHT", "evafast", evafast, {repeatable = true, complex = true}) mp.add_key_binding(nil, "evafast-rewind", evafast_rewind, {repeatable = true, complex = true}) mp.add_key_binding(nil, "flash-speed", function() flash_state(true, nil, true) end) mp.add_key_binding(nil, "speedup", evafast_speedup) mp.add_key_binding(nil, "slowdown", evafast_slowdown) mp.add_key_binding(nil, "toggle", evafast_toggle) mp.add_key_binding(nil, "toggle-rewind", evafast_toggle_rewind) mp.commandv("script-message-to", "uosc", "get-version", mp.get_script_name())