537 lines
16 KiB
Lua
537 lines
16 KiB
Lua
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)
|