├── README.md ├── evafast.conf └── evafast.lua /README.md: -------------------------------------------------------------------------------- 1 | # evafast 2 | Fast-forwarding and seeking on a single key, with quality of life features like a slight slowdown when subtitles are shown. 3 | 4 | ## Installation 5 | Place evafast.lua in your mpv `scripts` folder. 6 | 7 | ## Usage 8 | Tap right arrow key to seek like default mpv, hold it down to fast-forward. 9 | Playback speed will smoothly ramp up and wind back down. 10 | 11 | Provides the `evafast/toggle` script-binding for speeding up independently of the hybrid key. -------------------------------------------------------------------------------- /evafast.conf: -------------------------------------------------------------------------------- 1 | # How far to jump on press 2 | seek_distance=5 3 | 4 | # Playback speed modifier, applied once every speed_interval until cap is reached 5 | speed_increase=0.1 6 | speed_decrease=0.1 7 | 8 | # At what interval to apply speed modifiers 9 | speed_interval=0.05 10 | 11 | # Playback speed cap 12 | speed_cap=2 13 | 14 | # Playback speed cap when subtitles are displayed, 'no' for same as speed_cap 15 | subs_speed_cap=1.6 16 | 17 | # Multiply current speed by modifier before adjustment (exponential speedup) 18 | # Use much lower values than default e.g. speed_increase=0.05, speed_decrease=0.025 19 | multiply_modifier=no 20 | 21 | # Show current speed on the osd (or flash speed if using uosc) 22 | show_speed=yes 23 | 24 | # Show current speed on the osd when toggled (or flash speed if using uosc) 25 | show_speed_toggled=yes 26 | 27 | # Show seek actions on the osd (or flash timeline if using uosc) 28 | show_seek=yes 29 | 30 | # Look ahead for smoother transition when subs_speed_cap is set 31 | lookahead=no -------------------------------------------------------------------------------- /evafast.lua: -------------------------------------------------------------------------------- 1 | -- evafast.lua 2 | -- 3 | -- Much speed. 4 | -- 5 | -- Jumps forwards when right arrow is pressed, speeds up when it's held. 6 | -- Inspired by bilibili.com's player. Allows you to have both seeking and fast-forwarding on the same key. 7 | -- Also supports toggling fastforward mode with a keypress. 8 | -- Adjust --input-ar-delay to define when to start fastforwarding. 9 | -- Define --hr-seek if you want accurate seeking. 10 | -- If you just want a nicer fastforward.lua without hybrid key behavior, set seek_distance to 0. 11 | 12 | local options = { 13 | -- How far to jump on press, set to 0 to disable seeking and force fastforward 14 | seek_distance = 5, 15 | 16 | -- Playback speed modifier, applied once every speed_interval until cap is reached 17 | speed_increase = 0.1, 18 | speed_decrease = 0.1, 19 | 20 | -- At what interval to apply speed modifiers 21 | speed_interval = 0.05, 22 | 23 | -- Playback speed cap 24 | speed_cap = 2, 25 | 26 | -- Playback speed cap when subtitles are displayed, 'no' for same as speed_cap 27 | subs_speed_cap = 1.6, 28 | 29 | -- Multiply current speed by modifier before adjustment (exponential speedup) 30 | -- Use much lower values than default e.g. speed_increase=0.05, speed_decrease=0.025 31 | multiply_modifier = false, 32 | 33 | -- Show current speed on the osd (or flash speed if using uosc) 34 | show_speed = true, 35 | 36 | -- Show current speed on the osd when toggled (or flash speed if using uosc) 37 | show_speed_toggled = true, 38 | 39 | -- Show current speed on the osd when speeding up towards a target time (or flash speed if using uosc) 40 | show_speed_target = false, 41 | 42 | -- Show seek actions on the osd (or flash timeline if using uosc) 43 | show_seek = true, 44 | 45 | -- Look ahead for smoother transition when subs_speed_cap is set 46 | lookahead = false 47 | } 48 | 49 | mp.options = require "mp.options" 50 | mp.options.read_options(options, "evafast") 51 | 52 | local uosc_available = false 53 | local repeated = false 54 | local speed_timer = nil 55 | local speedup = true 56 | local no_speedup = false 57 | local jumps_reset_speed = true 58 | local toggle_display = false 59 | local toggle_state = false 60 | local freeze = false 61 | 62 | local forced_speed_cap = nil 63 | local use_forced_speed_cap = false 64 | 65 | local speedup_target = nil 66 | 67 | local function speed_transition(test_speed, target) 68 | local time_for_correction = 0 69 | if not freeze then 70 | while test_speed ~= target do 71 | time_for_correction = time_for_correction + options.speed_interval 72 | if test_speed <= target then 73 | if options.multiply_modifier then 74 | test_speed = math.min(test_speed + (test_speed * options.speed_increase), target) 75 | else 76 | test_speed = math.min(test_speed + options.speed_increase, target) 77 | end 78 | else 79 | if options.multiply_modifier then 80 | test_speed = math.max(test_speed - (test_speed * options.speed_decrease), 1) 81 | else 82 | test_speed = math.max(test_speed - options.speed_decrease, 1) 83 | end 84 | end 85 | if test_speed == 1 then break end 86 | end 87 | end 88 | return time_for_correction 89 | end 90 | 91 | local function adjust_speed() 92 | local no_sub_speed = not options.subs_speed_cap or mp.get_property("sub-start") == nil 93 | local effective_speed_cap = no_sub_speed and options.speed_cap or options.subs_speed_cap 94 | local speed = mp.get_property_number("speed", 1) 95 | local old_speed = speed 96 | 97 | if options.lookahead and options.subs_speed_cap and no_sub_speed and not use_forced_speed_cap then 98 | local sub_delay = mp.get_property_native("sub-delay") 99 | local sub_visible = mp.get_property_bool("sub-visibility") 100 | if sub_visible then 101 | mp.set_property_bool("sub-visibility", false) 102 | end 103 | mp.command("no-osd sub-step 1") 104 | local sub_next_delay = mp.get_property_native("sub-delay") 105 | local sub_next = sub_delay - sub_next_delay 106 | mp.set_property("sub-delay", sub_delay) 107 | if sub_visible then 108 | mp.set_property_bool("sub-visibility", sub_visible) 109 | end 110 | -- calculate how long it takes to get from current speed to target speed, and use that as threshold for sub_next 111 | local time_for_correction = speed_transition(speed, options.subs_speed_cap) 112 | if sub_next ~= 0 and sub_next <= (time_for_correction * speed) then 113 | effective_speed_cap = options.subs_speed_cap 114 | use_forced_speed_cap = true 115 | forced_speed_cap = effective_speed_cap 116 | end 117 | end 118 | 119 | if speedup_target ~= nil then 120 | local current_time = mp.get_property_number("time-pos", 0) 121 | if current_time >= speedup_target then 122 | jumps_reset_speed = true 123 | no_speedup = true 124 | repeated = false 125 | freeze = false 126 | else 127 | local time_for_correction = speed_transition(speed, math.max(math.min(options.speed_cap, options.subs_speed_cap and options.subs_speed_cap or options.speed_cap), 1.1)) -- not effective_speed_cap because it may lead to huge fluctuations in transition speed 128 | if (time_for_correction * speed + current_time) > speedup_target then 129 | effective_speed_cap = 1.1 -- >1 so we don't get stuck trying to catch the target 130 | use_forced_speed_cap = true 131 | forced_speed_cap = effective_speed_cap 132 | else 133 | forced_speed_cap = nil 134 | use_forced_speed_cap = false 135 | end 136 | end 137 | end 138 | 139 | if not freeze then 140 | if forced_speed_cap ~= nil then 141 | if speed ~= forced_speed_cap or mp.get_property_bool("pause") then 142 | use_forced_speed_cap = true 143 | end 144 | effective_speed_cap = forced_speed_cap 145 | end 146 | if speedup and not no_speedup and speed <= effective_speed_cap then 147 | if options.multiply_modifier then 148 | speed = math.min(speed + (speed * options.speed_increase), effective_speed_cap) 149 | else 150 | speed = math.min(speed + options.speed_increase, effective_speed_cap) 151 | end 152 | else 153 | if options.multiply_modifier then 154 | speed = math.max(speed - (speed * options.speed_decrease), 1) 155 | else 156 | speed = math.max(speed - options.speed_decrease, 1) 157 | end 158 | end 159 | if forced_speed_cap ~= nil and not use_forced_speed_cap then 160 | forced_speed_cap = nil 161 | end 162 | if speed == options.subs_speed_cap then 163 | if use_forced_speed_cap then 164 | use_forced_speed_cap = false 165 | end 166 | end 167 | end 168 | 169 | if speed ~= old_speed then 170 | mp.set_property("speed", speed) 171 | if (options.show_speed and not toggle_display) or (options.show_speed_toggled and toggle_display and speedup_target == nil) or (options.show_speed_target and speedup_target ~= nil) then 172 | if uosc_available then 173 | mp.command("script-binding uosc/flash-speed") 174 | else 175 | mp.osd_message(("▶▶ x%.1f"):format(speed)) 176 | end 177 | end 178 | end 179 | 180 | if speed == 1 and effective_speed_cap ~= 1 then 181 | if speed_timer ~= nil and not toggle_state then 182 | speed_timer:kill() 183 | speed_timer = nil 184 | end 185 | repeated = false 186 | jumps_reset_speed = true 187 | toggle_display = false 188 | toggle_state = false 189 | speedup_target = nil 190 | elseif speed_timer == nil then 191 | speed_timer = mp.add_periodic_timer(options.speed_interval, adjust_speed) 192 | end 193 | end 194 | 195 | local function evafast(keypress) 196 | if jumps_reset_speed and not toggle_state and (keypress["event"] == "up" or keypress["event"] == "press") then 197 | speedup = false 198 | speedup_target = nil 199 | end 200 | 201 | if options.seek_distance == 0 then 202 | if keypress["event"] == "up" or keypress["event"] == "press" then 203 | speedup = false 204 | no_speedup = true 205 | repeated = false 206 | speedup_target = nil 207 | end 208 | if keypress["event"] == "down" then 209 | keypress["event"] = "repeat" 210 | end 211 | end 212 | 213 | if keypress["event"] == "up" or keypress["event"] == "press" then 214 | toggle_display = toggle_state 215 | if toggle_state and jumps_reset_speed then 216 | speedup = false 217 | speedup_target = nil 218 | end 219 | if speed_timer ~= nil and not toggle_state and mp.get_property_number("speed") == 1 and ((not options.subs_speed_cap or mp.get_property("sub-start") == nil) and options.speed_cap or options.subs_speed_cap) ~= 1 then 220 | speed_timer:kill() 221 | speed_timer = nil 222 | jumps_reset_speed = true 223 | toggle_display = false 224 | toggle_state = false 225 | speedup_target = nil 226 | end 227 | freeze = false 228 | end 229 | 230 | if keypress["event"] == "down" then 231 | repeated = false 232 | speedup = true 233 | freeze = true 234 | toggle_display = false 235 | if options.show_seek and not repeated and not uosc_available then 236 | mp.osd_message("▶▶") 237 | end 238 | elseif (keypress["event"] == "up" and (not repeated or speedup_target)) or keypress["event"] == "press" then 239 | if options.seek_distance ~= 0 then 240 | mp.commandv("seek", options.seek_distance) 241 | if options.show_seek and uosc_available then 242 | mp.command("script-binding uosc/flash-timeline") 243 | end 244 | end 245 | repeated = false 246 | if jumps_reset_speed and not toggle_state then 247 | no_speedup = true 248 | end 249 | elseif keypress["event"] == "repeat" then 250 | freeze = false 251 | speedup = true 252 | no_speedup = false 253 | if not repeated then 254 | adjust_speed() 255 | end 256 | repeated = true 257 | end 258 | end 259 | 260 | local function evafast_speedup() 261 | no_speedup = false 262 | speedup = true 263 | jumps_reset_speed = false 264 | toggle_display = true 265 | toggle_state = true 266 | evafast({event = "repeat"}) 267 | end 268 | 269 | local function evafast_slowdown() 270 | jumps_reset_speed = true 271 | no_speedup = true 272 | repeated = false 273 | freeze = false 274 | speedup_target = nil 275 | end 276 | 277 | local function evafast_toggle() 278 | if (repeated or not jumps_reset_speed) and speedup then 279 | evafast_slowdown() 280 | else 281 | evafast_speedup() 282 | end 283 | end 284 | 285 | mp.register_script_message("uosc-version", function(version) 286 | uosc_available = true 287 | end) 288 | 289 | mp.register_script_message("speedup-target", function(time) 290 | time = tonumber(time) or 0 291 | if mp.get_property_number("time-pos", 0) >= time then 292 | if speedup_target ~= nil then 293 | use_forced_speed_cap = false 294 | forced_speed_cap = nil 295 | speedup_target = nil 296 | evafast_slowdown() 297 | end 298 | return 299 | end 300 | speedup_target = time 301 | evafast_speedup() 302 | end) 303 | 304 | mp.register_script_message("get-version", function(script) 305 | mp.commandv("script-message-to", script, "evafast-version", "1.0") 306 | end) 307 | 308 | mp.add_key_binding("RIGHT", "evafast", evafast, {repeatable = true, complex = true}) 309 | mp.add_key_binding(nil, "speedup", evafast_speedup) 310 | mp.add_key_binding(nil, "slowdown", evafast_slowdown) 311 | mp.add_key_binding(nil, "toggle", evafast_toggle) 312 | 313 | mp.commandv("script-message-to", "uosc", "get-version", mp.get_script_name()) 314 | --------------------------------------------------------------------------------