local opt = require 'mp.options' local msg = require 'mp.msg' cfg = { lookahead = 5, --if the next subtitle appears after this threshold then speedup speedup = 2, --the value that 'speed' is set to during speedup leadin = 1, --seconds to stop short of the next subtitle sub_timeout = 5, --if a subtitle is visible for longer than this value, speedup begins; set to 0 to disable skipmode = false, --instead of speeding up playback seek to the next known subtitle maxSkip = 2.5, --max skip distance (seconds) when skipmode is enabled minSkip = 1, --this is also configurable but setting it too low can actually make your watch time longer skipdelay = 0.8, --in skip mode, this setting delays each skip by x seconds (must be >=0) directskip = false, --seek to next known subtitle (must be in cache) no matter how far away exact_skip = true, --use accurate but slow skips --Because mpv syncs subtitles to audio it is possible that if audio processing lags behind-- --video processing then normal playback may not resume in sync with the video. If 'avsync' > leadin-- --then this disables the audio so that we can ensure normal playback resumes on time. dropOnAVdesync = true, ignorePattern = false, --if true, subtitles are matched against 'subPattern'. A successful match will be treated as if there was no subtitle subPattern = '^[#♯♩♪♬♫🎵🎶%[%(]+.*[#♯♩♪♬♫🎵🎶%]%)]+$' } opt.read_options(cfg) readahead_secs = mp.get_property_native('demuxer-readahead-secs') normalspeed = mp.get_property_native('speed') enable = false state = 0 firstskip = true --make the first skip in skip mode not have to wait for skipdelay aid = nil function shouldIgnore(subtext) if cfg.ignorePattern and subtext and subtext ~= '' then local st = subtext:match('^%s*(.-)%s*$') -- trim whitespace if st:find(cfg.subPattern) then return true end else return false end end function clamp(v, l, u) if l and v < l then v = l elseif u and v > u then v = u end return v end function formatTime(s) if not s then return nil end s = math.abs(s) local _s = s % 60 s = s / 60 local m = math.floor(s % 60) s = s / 60 local h = math.floor(s) return string.format('%02d:%02d:%02f', h, m, _s) end function sleep(s) local ntime = os.clock() + s repeat until os.clock() > ntime end function reset_state() nextsub, shouldspeedup, speedup_zone_begin, speedup_zone_end = nil, false, nil, nil last_speedup_zone_begin = nil last_skip_position = nil last_nextsub_check = nil firstskip = true state = 0 end function restore_normalspeed() if not cfg.skipmode then mp.set_property('speed', normalspeed) if video_sync then mp.set_property('video-sync', video_sync) end end if aid and aid ~= mp.get_property('aid') then mp.set_property('aid', aid) end end function speed_up() normalspeed = mp.get_property('speed') video_sync = mp.get_property('video-sync') mp.set_property('video-sync', 'desync') mp.set_property('speed', cfg.speedup) if cfg.dropOnAVdesync then aid = mp.get_property('aid') mp.observe_property('avsync', 'native', check_audio) end end function skip(skipval) if skipval < cfg.minSkip then msg.warn('skip(): tskip < minSkip; abort!') return end if cfg.exact_skip then mp.commandv('seek', skipval, 'relative', 'exact') else mp.commandv('seek', skipval, 'relative') end end function delayskip(position, skipdelay) if not (firstskip or skipdelay == 0) then sleep(skipdelay) local tposition = mp.get_property_number('time-pos') if not tposition then position = position + skipdelay else position = tposition end end firstskip = false return position end function skipval(nextsub) local demuxer_cache_duration = mp.get_property_number('demuxer-cache-duration', 0) msg.trace('skipval()') msg.trace(' demuxer_cache_duration:', demuxer_cache_duration) msg.trace(' nextsub:', nextsub) local skipval = demuxer_cache_duration * 0.8 if skipval == 0 or nextsub == 0 then skipval = cfg.maxSkip end if nextsub > 0 then if cfg.directskip then skipval = clamp(nextsub - cfg.leadin, 0, nil) elseif nextsub - cfg.leadin <= skipval then skipval = clamp(nextsub - cfg.leadin, 0, skipval) else skipval = clamp(skipval, 0, clamp(skipval, 0, clamp(nextsub - cfg.leadin, 0, cfg.maxSkip))) end end if skipval < cfg.minSkip then skipval = 0 elseif skipval > cfg.maxSkip and not cfg.directskip then skipval = cfg.maxSkip end msg.trace(' skipval:', skipval) return skipval end function wait_finish_seeking() repeat local seeking = mp.get_property_bool('seeking') until not seeking end function check_audio(_, ds) if not ds or cfg.skipmode or state == 0 or cfg.leadin == 0 then return elseif (state == 1 or state == 3) and tonumber(ds) > cfg.leadin and mp.get_property('aid') ~= 'no' then aid = mp.get_property('aid') mp.set_property('aid', 'no') msg.warn('avsync greater than leadin, dropping audio') end end function check_should_speedup(subend) local subspeed = mp.get_property_number('sub-speed', 1) local subdelay = mp.get_property_number('sub-delay') local substart = mp.get_property_number('sub-start') subend = subend * subspeed + subdelay if substart then substart = substart * subspeed + subdelay end if cfg.sub_timeout > 0 and substart and substart < subend then if subend - substart >= cfg.sub_timeout then subend = substart + cfg.sub_timeout end end local sub_visibility = mp.get_property_bool('sub-visibility') if sub_visibility then mp.set_property_bool('sub-visibility', false) end mp.commandv('sub-step', 1) local nextsubstart = mp.get_property_number('sub-start') if nextsubstart then nextsubstart = nextsubstart * subspeed + subdelay end if cfg.ignorePattern and nextsubstart and subend < nextsubstart then repeat local ignore = shouldIgnore(mp.get_property('sub-text')) if ignore then local t_nextsubstart = mp.get_property_number('sub-end') if t_nextsubstart then t_nextsubstart = t_nextsubstart * subspeed + subdelay end if t_nextsubstart and t_nextsubstart > nextsubstart then nextsubstart = t_nextsubstart mp.commandv('sub-step', 1) else break end end until not ignore end mp.set_property_number('sub-delay', subdelay) if sub_visibility then mp.set_property_bool('sub-visibility', true) end msg.trace('s-start,s-end,ns-start:', formatTime(substart), formatTime(subend), formatTime(nextsubstart)) local nextsub if nextsubstart then if subend < nextsubstart then nextsub = nextsubstart - subend end end if cfg.leadin > cfg.lookahead then cfg.leadin = 0 end local shouldspeedup = nextsub and nextsub >= cfg.lookahead - cfg.leadin local speedup_begin = subend if shouldspeedup then msg.debug('check_should_speedup()') msg.debug(' shouldspeedup:', tostring(shouldspeedup)) msg.debug(' speedup_begin:', formatTime(speedup_begin) or '') msg.debug(' nextsub:', nextsub or '') end return nextsub, shouldspeedup, speedup_begin end function check_position(_, position) if position then if state == 0 and speedup_zone_begin and position >= speedup_zone_begin and shouldspeedup then if cfg.skipmode then msg.debug('check_position[0] -> [2]') msg.debug(' position:', formatTime(position)) firstskip = true state = 2 else msg.debug('check_position[0] -> [1]') msg.debug(' position:', formatTime(position)) speed_up() state = 1 end msg.debug(' speedup_zone_begin:', formatTime(speedup_zone_begin)) msg.debug(' speedup_zone_end:', formatTime(speedup_zone_end)) elseif state == 0 and not nextsub and last_speedup_zone_begin and position - last_speedup_zone_begin > 2 then msg.debug('check_position[0] -> [3]') msg.debug(' position:', formatTime(position)) if not cfg.skipmode then speed_up() end last_speedup_zone_begin = nil last_nextsub_check = position speedup_zone_begin = position speedup_zone_end = nil firstskip = true state = 3 elseif state == 1 and speedup_zone_end and position >= speedup_zone_end then restore_normalspeed() reset_state() msg.debug('check_position[1] -> [0]') msg.debug(' position:', formatTime(position)) elseif state == 2 then -- msg.debug('check_position[2]') -- msg.debug(' position:', formatTime(position)) if speedup_zone_end and position >= speedup_zone_end then msg.debug('check_position[2] -> [0] pos >= end') msg.debug(' position:', formatTime(position)) msg.debug(' speedup_zone_end:', formatTime(speedup_zone_end)) if not cfg.exact_skip and last_skip_position and position > speedup_zone_end then msg.debug(' ->seek back to:', formatTime(last_skip_position)) wait_finish_seeking() mp.set_property_number('time-pos', last_skip_position) end reset_state() elseif speedup_zone_begin <= position and position < speedup_zone_end then if mp.get_property('pause') == 'no' then local position_after_skipdelay = position wait_finish_seeking() if position + cfg.skipdelay < speedup_zone_end then position_after_skipdelay = delayskip(position, cfg.skipdelay) end local nextsub = speedup_zone_end - position_after_skipdelay local tSkip = 0 if nextsub > 0 then tSkip = skipval(nextsub) if position_after_skipdelay + tSkip >= speedup_zone_end then if speedup_zone_end - position_after_skipdelay >= cfg.minSkip then wait_finish_seeking() mp.set_property_number('time-pos', speedup_zone_end) msg.debug('check_position[2]') msg.debug(' position:', formatTime(position_after_skipdelay)) msg.debug(' nextsub:', nextsub) msg.debug(' direct skip to:', formatTime(speedup_zone_end)) reset_state() end elseif tSkip >= cfg.minSkip then local seeking = mp.get_property_bool('seeking') if not seeking then last_skip_position = position_after_skipdelay skip(tSkip) msg.debug('check_position[2]') msg.debug(' position:', formatTime(position_after_skipdelay)) msg.debug(' nextsub:', nextsub) msg.debug(' skipval:', tSkip) end end elseif nextsub < 0 and not cfg.exact_skip then local cursubend = mp.get_property_number('sub-end') local margin = 0.5 if cursubend and cursubend > speedup_zone_end + cfg.leadin then margin = clamp((cursubend - (speedup_zone_end + cfg.leadin)) * 0.35, 0, 1) end if position_after_skipdelay > speedup_zone_end + cfg.leadin + margin then wait_finish_seeking() mp.set_property_number('time-pos', speedup_zone_end) msg.debug('check_position[2]') msg.debug(' position:', formatTime(position_after_skipdelay)) msg.debug(' nextsub:', nextsub) msg.debug(' skipval:', tSkip) msg.debug(' margin:', margin) msg.debug(' ->seek back to: ' .. formatTime(speedup_zone_end)) end reset_state() else reset_state() end end end elseif state == 3 then if position - last_nextsub_check > 0.5 then local t_nextsub, t_shouldspeedup, t_speedup_zone_begin = check_should_speedup(position) if t_nextsub then msg.debug('check_position[3]') msg.debug(' position:', formatTime(position)) msg.debug(' ->found next sub') if not t_shouldspeedup then msg.debug(' ->stop speedup') msg.debug(' [3] -> [0]') restore_normalspeed() reset_state() return else nextsub, shouldspeedup = t_nextsub, t_shouldspeedup speedup_zone_end = t_speedup_zone_begin + nextsub - cfg.leadin if cfg.skipmode then msg.debug('check_position[3] -> [2]') state = 2 return else msg.debug('check_position[3] -> [1]') state = 1 last_nextsub_check = position return end end end last_nextsub_check = position end if cfg.skipmode then local seeking = mp.get_property_bool('seeking') if mp.get_property('pause') == 'no' and not seeking then local tlast_skip_position = position position = delayskip(position, cfg.skipdelay) local tSkip = skipval(0) if tSkip >= cfg.minSkip then last_skip_position = tlast_skip_position skip(tSkip) msg.debug('check_position[3]') msg.debug(' position:', formatTime(position)) msg.debug(' nextsub: ---') msg.debug(' skipval:', tSkip) end end end else end end end function speed_transition(_, subend) if not subend then return end msg.debug('speed_transition()') if state == 3 or (state == 2 and not cfg.exact_skip) then msg.debug(' state >= 2: check seek back / reset') local position = mp.get_property_number('time-pos') if cfg.skipmode and last_skip_position then msg.debug(' position:', formatTime(position)) msg.debug(' ->seek back to:', formatTime(last_skip_position)) wait_finish_seeking() mp.set_property_number('time-pos', last_skip_position) reset_state() return end restore_normalspeed() reset_state() end local t_nextsub, t_shouldspeedup, t_speedup_zone_begin = check_should_speedup(subend) if t_shouldspeedup then if state ~= 0 then msg.debug(' ->reset: state > 0') restore_normalspeed() reset_state() end nextsub, shouldspeedup, speedup_zone_begin = t_nextsub, t_shouldspeedup, t_speedup_zone_begin speedup_zone_end = speedup_zone_begin + nextsub - cfg.leadin msg.debug(' speedup_zone_end:', formatTime(speedup_zone_end) or '') else if state ~= 0 then msg.debug(' ->reset: state > 0') restore_normalspeed() end reset_state() end last_speedup_zone_begin = t_speedup_zone_begin end function toggle() if not enable then normalspeed = mp.get_property('speed') local calculated_readaheadsecs = math.max(5, readahead_secs, cfg.maxSkip + cfg.leadin, cfg.lookahead + cfg.leadin) if readahead_secs < calculated_readaheadsecs then mp.set_property('demuxer-readahead-secs', calculated_readaheadsecs) end last_speedup_zone_begin = mp.get_property_number('time-pos') mp.observe_property('sub-end', 'number', speed_transition) mp.observe_property('time-pos', 'number', check_position) mp.osd_message('speed-transition enabled') msg.info('enabled') else restore_normalspeed() reset_state() mp.set_property('demuxer-readahead-secs', readahead_secs) mp.unobserve_property(speed_transition) mp.unobserve_property(check_position) mp.unobserve_property(check_audio) mp.osd_message('speed-transition disabled') msg.info('disabled') end state = 0 enable = not enable end function switch_mode() cfg.skipmode = not cfg.skipmode if not enable then toggle() end if cfg.skipmode then if state == 1 or state == 3 then if state == 1 then state = 2 end mp.set_property('speed', normalspeed) end mp.osd_message('skip mode') msg.info('skip mode') else if state == 2 or state == 3 then if state == 2 then state = 1 end speed_up() end mp.osd_message('speed mode') msg.info('speed mode') end end function reset_on_file_load() restore_normalspeed() reset_state() end function change_speedup(v) cfg.speedup = cfg.speedup + v mp.osd_message('speedup: ' .. cfg.speedup) msg.info('speedup:', cfg.speedup) end function change_leadin(v) cfg.leadin = clamp(cfg.leadin + v, 0, 2) mp.osd_message('leadin: ' .. cfg.leadin) msg.info('leadin:', cfg.leadin) end function change_lookAhead(v) cfg.lookahead = clamp(cfg.lookahead + v, 0, nil) mp.osd_message('lookahead: ' .. cfg.lookahead) msg.info('lookahead:', cfg.lookahead) end mp.add_key_binding('ctrl+j', 'toggle_speedtrans', toggle) mp.add_key_binding('alt+j', 'switch_mode', switch_mode) mp.add_key_binding('alt++', 'increase_speedup', function() change_speedup(0.1) end, { repeatable = true }) mp.add_key_binding('alt+-', 'decrease_speedup', function() change_speedup(-0.1) end, { repeatable = true }) mp.add_key_binding('alt+0', 'increase_leadin', function() change_leadin(0.25) end) mp.add_key_binding('alt+9', 'decrease_leadin', function() change_leadin(-0.25) end) mp.add_key_binding('alt+8', 'increase_lookahead', function() change_lookAhead(0.25) end) mp.add_key_binding('alt+7', 'decrease_lookahead', function() change_lookAhead(-0.25) end) mp.register_event('file-loaded', reset_on_file_load)