-
Notifications
You must be signed in to change notification settings - Fork 29
/
speed-transition.lua
536 lines (484 loc) · 16.1 KB
/
speed-transition.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
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)