initial commit
This commit is contained in:
1
scripts/modern.lua
Symbolic link
1
scripts/modern.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../mpv-osc-modern/modern.lua
|
||||
1
scripts/mpv-youtube-queue.lua
Symbolic link
1
scripts/mpv-youtube-queue.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../mpv-youtube-queue/mpv-youtube-queue.lua
|
||||
1025
scripts/pause-indicator.lua
Normal file
1025
scripts/pause-indicator.lua
Normal file
File diff suppressed because it is too large
Load Diff
418
scripts/reload.lua
Normal file
418
scripts/reload.lua
Normal file
@@ -0,0 +1,418 @@
|
||||
-- reload.lua
|
||||
--
|
||||
-- When an online video is stuck buffering or got very slow CDN
|
||||
-- source, restarting often helps. This script provides automatic
|
||||
-- reloading of videos that doesn't have buffering progress for some
|
||||
-- time while keeping the current time position. It also adds `Ctrl+r`
|
||||
-- keybinding to reload video manually.
|
||||
--
|
||||
-- SETTINGS
|
||||
--
|
||||
-- To override default setting put the `lua-settings/reload.conf` file in
|
||||
-- mpv user folder, on linux it is `~/.config/mpv`. NOTE: config file
|
||||
-- name should match the name of the script.
|
||||
--
|
||||
-- Default `reload.conf` settings:
|
||||
--
|
||||
-- ```
|
||||
-- # enable automatic reload on timeout
|
||||
-- # when paused-for-cache event fired, we will wait
|
||||
-- # paused_for_cache_timer_timeout sedonds and then reload the video
|
||||
-- paused_for_cache_timer_enabled=yes
|
||||
--
|
||||
-- # checking paused_for_cache property interval in seconds,
|
||||
-- # can not be less than 0.05 (50 ms)
|
||||
-- paused_for_cache_timer_interval=1
|
||||
--
|
||||
-- # time in seconds to wait until reload
|
||||
-- paused_for_cache_timer_timeout=10
|
||||
--
|
||||
-- # enable automatic reload based on demuxer cache
|
||||
-- # if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout
|
||||
-- # time interval, the video will be reloaded as soon as demuxer cache depleated
|
||||
-- demuxer_cache_timer_enabled=yes
|
||||
--
|
||||
-- # checking demuxer-cache-time property interval in seconds,
|
||||
-- # can not be less than 0.05 (50 ms)
|
||||
-- demuxer_cache_timer_interval=2
|
||||
--
|
||||
-- # if demuxer cache didn't receive any data during demuxer_cache_timer_timeout
|
||||
-- # we decide that it has no progress and will reload the stream when
|
||||
-- # paused_for_cache event happens
|
||||
-- demuxer_cache_timer_timeout=20
|
||||
--
|
||||
-- # when the end-of-file is reached, reload the stream to check
|
||||
-- # if there is more content available.
|
||||
-- reload_eof_enabled=no
|
||||
--
|
||||
-- # keybinding to reload stream from current time position
|
||||
-- # you can disable keybinding by setting it to empty value
|
||||
-- # reload_key_binding=
|
||||
-- reload_key_binding=Ctrl+r
|
||||
-- ```
|
||||
--
|
||||
-- DEBUGGING
|
||||
--
|
||||
-- Debug messages will be printed to stdout with mpv command line option
|
||||
-- `--msg-level='reload=debug'`. You may also need to add the `--no-msg-color`
|
||||
-- option to make the debug logs visible if you are using a dark colorscheme
|
||||
-- in terminal.
|
||||
|
||||
local msg = require 'mp.msg'
|
||||
local options = require 'mp.options'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
|
||||
local settings = {
|
||||
paused_for_cache_timer_enabled = true,
|
||||
paused_for_cache_timer_interval = 1,
|
||||
paused_for_cache_timer_timeout = 10,
|
||||
demuxer_cache_timer_enabled = true,
|
||||
demuxer_cache_timer_interval = 2,
|
||||
demuxer_cache_timer_timeout = 20,
|
||||
reload_eof_enabled = false,
|
||||
reload_key_binding = "Ctrl+r",
|
||||
}
|
||||
|
||||
-- global state stores properties between reloads
|
||||
local property_path = nil
|
||||
local property_time_pos = 0
|
||||
local property_keep_open = nil
|
||||
|
||||
-- FSM managing the demuxer cache.
|
||||
--
|
||||
-- States:
|
||||
--
|
||||
-- * fetch - fetching new data
|
||||
-- * stale - unable to fetch new data for time < 'demuxer_cache_timer_timeout'
|
||||
-- * stuck - unable to fetch new data for time >= 'demuxer_cache_timer_timeout'
|
||||
--
|
||||
-- State transitions:
|
||||
--
|
||||
-- +---------------------------+
|
||||
-- v |
|
||||
-- +-------+ +-------+ +-------+
|
||||
-- + fetch +<--->+ stale +---->+ stuck |
|
||||
-- +-------+ +-------+ +-------+
|
||||
-- | ^ | ^ | ^
|
||||
-- +---+ +---+ +---+
|
||||
local demuxer_cache = {
|
||||
timer = nil,
|
||||
|
||||
state = {
|
||||
name = 'uninitialized',
|
||||
demuxer_cache_time = 0,
|
||||
in_state_time = 0,
|
||||
},
|
||||
|
||||
events = {
|
||||
continue_fetch = { name = 'continue_fetch', from = 'fetch', to = 'fetch' },
|
||||
continue_stale = { name = 'continue_stale', from = 'stale', to = 'stale' },
|
||||
continue_stuck = { name = 'continue_stuck', from = 'stuck', to = 'stuck' },
|
||||
fetch_to_stale = { name = 'fetch_to_stale', from = 'fetch', to = 'stale' },
|
||||
stale_to_fetch = { name = 'stale_to_fetch', from = 'stale', to = 'fetch' },
|
||||
stale_to_stuck = { name = 'stale_to_stuck', from = 'stale', to = 'stuck' },
|
||||
stuck_to_fetch = { name = 'stuck_to_fetch', from = 'stuck', to = 'fetch' },
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
-- Always start with 'fetch' state
|
||||
function demuxer_cache.reset_state()
|
||||
demuxer_cache.state = {
|
||||
name = demuxer_cache.events.continue_fetch.to,
|
||||
demuxer_cache_time = 0,
|
||||
in_state_time = 0,
|
||||
}
|
||||
end
|
||||
|
||||
-- Has 'demuxer_cache_time' changed
|
||||
function demuxer_cache.has_progress_since(t)
|
||||
return demuxer_cache.state.demuxer_cache_time ~= t
|
||||
end
|
||||
|
||||
function demuxer_cache.is_state_fetch()
|
||||
return demuxer_cache.state.name == demuxer_cache.events.continue_fetch.to
|
||||
end
|
||||
|
||||
function demuxer_cache.is_state_stale()
|
||||
return demuxer_cache.state.name == demuxer_cache.events.continue_stale.to
|
||||
end
|
||||
|
||||
function demuxer_cache.is_state_stuck()
|
||||
return demuxer_cache.state.name == demuxer_cache.events.continue_stuck.to
|
||||
end
|
||||
|
||||
function demuxer_cache.transition(event)
|
||||
if demuxer_cache.state.name == event.from then
|
||||
|
||||
-- state setup
|
||||
demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time
|
||||
|
||||
if event.name == 'continue_fetch' then
|
||||
demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
|
||||
elseif event.name == 'continue_stale' then
|
||||
demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
|
||||
elseif event.name == 'continue_stuck' then
|
||||
demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
|
||||
elseif event.name == 'fetch_to_stale' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
elseif event.name == 'stale_to_fetch' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
elseif event.name == 'stale_to_stuck' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
elseif event.name == 'stuck_to_fetch' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
end
|
||||
|
||||
-- state transition
|
||||
demuxer_cache.state.name = event.to
|
||||
|
||||
msg.debug('demuxer_cache.transition', event.name, utils.to_string(demuxer_cache.state))
|
||||
else
|
||||
msg.error(
|
||||
'demuxer_cache.transition',
|
||||
'illegal transition', event.name,
|
||||
'from state', demuxer_cache.state.name)
|
||||
end
|
||||
end
|
||||
|
||||
function demuxer_cache.initialize(demuxer_cache_timer_interval)
|
||||
demuxer_cache.reset_state()
|
||||
demuxer_cache.timer = mp.add_periodic_timer(
|
||||
demuxer_cache_timer_interval,
|
||||
function()
|
||||
demuxer_cache.demuxer_cache_timer_tick(
|
||||
mp.get_property_native('demuxer-cache-time'),
|
||||
demuxer_cache_timer_interval)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
-- If there is no progress of demuxer_cache_time in
|
||||
-- settings.demuxer_cache_timer_timeout time interval switch state to
|
||||
-- 'stuck' and switch back to 'fetch' as soon as any progress is made
|
||||
function demuxer_cache.demuxer_cache_timer_tick(demuxer_cache_time, demuxer_cache_timer_interval)
|
||||
local event = nil
|
||||
local cache_has_progress = demuxer_cache.has_progress_since(demuxer_cache_time)
|
||||
|
||||
-- I miss pattern matching so much
|
||||
if demuxer_cache.is_state_fetch() then
|
||||
if cache_has_progress then
|
||||
event = demuxer_cache.events.continue_fetch
|
||||
else
|
||||
event = demuxer_cache.events.fetch_to_stale
|
||||
end
|
||||
elseif demuxer_cache.is_state_stale() then
|
||||
if cache_has_progress then
|
||||
event = demuxer_cache.events.stale_to_fetch
|
||||
elseif demuxer_cache.state.in_state_time < settings.demuxer_cache_timer_timeout then
|
||||
event = demuxer_cache.events.continue_stale
|
||||
else
|
||||
event = demuxer_cache.events.stale_to_stuck
|
||||
end
|
||||
elseif demuxer_cache.is_state_stuck() then
|
||||
if cache_has_progress then
|
||||
event = demuxer_cache.events.stuck_to_fetch
|
||||
else
|
||||
event = demuxer_cache.events.continue_stuck
|
||||
end
|
||||
end
|
||||
|
||||
event.demuxer_cache_time = demuxer_cache_time
|
||||
event.interval = demuxer_cache_timer_interval
|
||||
demuxer_cache.transition(event)
|
||||
end
|
||||
|
||||
|
||||
local paused_for_cache = {
|
||||
timer = nil,
|
||||
time = 0,
|
||||
}
|
||||
|
||||
function paused_for_cache.reset_timer()
|
||||
msg.debug('paused_for_cache.reset_timer', paused_for_cache.time)
|
||||
if paused_for_cache.timer then
|
||||
paused_for_cache.timer:kill()
|
||||
paused_for_cache.timer = nil
|
||||
paused_for_cache.time = 0
|
||||
end
|
||||
end
|
||||
|
||||
function paused_for_cache.start_timer(interval_seconds, timeout_seconds)
|
||||
msg.debug('paused_for_cache.start_timer', paused_for_cache.time)
|
||||
if not paused_for_cache.timer then
|
||||
paused_for_cache.timer = mp.add_periodic_timer(
|
||||
interval_seconds,
|
||||
function()
|
||||
paused_for_cache.time = paused_for_cache.time + interval_seconds
|
||||
if paused_for_cache.time >= timeout_seconds then
|
||||
paused_for_cache.reset_timer()
|
||||
reload_resume()
|
||||
end
|
||||
msg.debug('paused_for_cache', 'tick', paused_for_cache.time)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function paused_for_cache.handler(property, is_paused)
|
||||
if is_paused then
|
||||
|
||||
if demuxer_cache.is_state_stuck() then
|
||||
msg.info("demuxer cache has no progress")
|
||||
-- reset demuxer state to avoid immediate reload if
|
||||
-- paused_for_cache event triggered right after reload
|
||||
demuxer_cache.reset_state()
|
||||
reload_resume()
|
||||
end
|
||||
|
||||
paused_for_cache.start_timer(
|
||||
settings.paused_for_cache_timer_interval,
|
||||
settings.paused_for_cache_timer_timeout)
|
||||
else
|
||||
paused_for_cache.reset_timer()
|
||||
end
|
||||
end
|
||||
|
||||
function read_settings()
|
||||
options.read_options(settings, mp.get_script_name())
|
||||
msg.debug(utils.to_string(settings))
|
||||
end
|
||||
|
||||
function reload(path, time_pos)
|
||||
msg.debug("reload", path, time_pos)
|
||||
if time_pos == nil then
|
||||
mp.commandv("loadfile", path, "replace")
|
||||
else
|
||||
mp.commandv("loadfile", path, "replace", "start=+" .. time_pos)
|
||||
end
|
||||
end
|
||||
|
||||
function reload_resume()
|
||||
local path = mp.get_property("path", property_path)
|
||||
local time_pos = mp.get_property("time-pos")
|
||||
local reload_duration = mp.get_property_native("duration")
|
||||
|
||||
local playlist_count = mp.get_property_number("playlist/count")
|
||||
local playlist_pos = mp.get_property_number("playlist-pos")
|
||||
local playlist = {}
|
||||
for i = 0, playlist_count-1 do
|
||||
playlist[i] = mp.get_property("playlist/" .. i .. "/filename")
|
||||
end
|
||||
-- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
|
||||
-- duration property. When reloading VOD, to keep the current time position
|
||||
-- we should provide offset from the start. Stream doesn't have fixed start.
|
||||
-- Decent choice would be to reload stream from it's current 'live' positon.
|
||||
-- That's the reason we don't pass the offset when reloading streams.
|
||||
if reload_duration and reload_duration > 0 then
|
||||
msg.info("reloading video from", time_pos, "second")
|
||||
reload(path, time_pos)
|
||||
-- VODs get stuck when reload is called without a time_pos
|
||||
-- this is most noticeable in youtube videos whenever download gets stuck in the first frames
|
||||
-- video would stay paused without being actually paused
|
||||
-- issue surfaced in mpv 0.33, afaik
|
||||
elseif reload_duration and reload_duration == 0 then
|
||||
msg.info("reloading video from", time_pos, "second")
|
||||
reload(path, time_pos)
|
||||
else
|
||||
msg.info("reloading stream")
|
||||
reload(path, nil)
|
||||
end
|
||||
msg.info("file ", playlist_pos+1, "of", playlist_count, "in playlist")
|
||||
for i = 0, playlist_pos-1 do
|
||||
mp.commandv("loadfile", playlist[i], "append")
|
||||
end
|
||||
mp.commandv("playlist-move", 0, playlist_pos+1)
|
||||
for i = playlist_pos+1, playlist_count-1 do
|
||||
mp.commandv("loadfile", playlist[i], "append")
|
||||
end
|
||||
end
|
||||
|
||||
function reload_eof(property, eof_reached)
|
||||
msg.debug("reload_eof", property, eof_reached)
|
||||
local time_pos = mp.get_property_number("time-pos")
|
||||
local duration = mp.get_property_number("duration")
|
||||
|
||||
if eof_reached and math.floor(time_pos) == math.floor(duration) then
|
||||
msg.debug("property_time_pos", property_time_pos, "time_pos", time_pos)
|
||||
|
||||
-- Check that playback time_pos made progress after the last reload. When
|
||||
-- eof is reached we try to reload video, in case there is more content
|
||||
-- available. If time_pos stayed the same after reload, it means that vidkk
|
||||
-- to avoid infinite reload loop when playback ended
|
||||
-- math.floor function rounds time_pos to a second, to avoid inane reloads
|
||||
if math.floor(property_time_pos) == math.floor(time_pos) then
|
||||
msg.info("eof reached, playback ended")
|
||||
mp.set_property("keep-open", property_keep_open)
|
||||
else
|
||||
msg.info("eof reached, checking if more content available")
|
||||
reload_resume()
|
||||
mp.set_property_bool("pause", false)
|
||||
property_time_pos = time_pos
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_file_loaded(event)
|
||||
local debug_info = {
|
||||
event = event,
|
||||
time_pos = mp.get_property("time-pos"),
|
||||
stream_pos = mp.get_property("stream-pos"),
|
||||
stream_end = mp.get_property("stream-end"),
|
||||
duration = mp.get_property("duration"),
|
||||
seekable = mp.get_property("seekable"),
|
||||
pause = mp.get_property("pause"),
|
||||
paused_for_cache = mp.get_property("paused-for-cache"),
|
||||
cache_buffering_state = mp.get_property("cache-buffering-state"),
|
||||
}
|
||||
msg.debug("debug_info", utils.to_string(debug_info))
|
||||
|
||||
-- When the video is reloaded after being paused for cache, it won't start
|
||||
-- playing again while all properties looks fine:
|
||||
-- `pause=no`, `paused-for-cache=no` and `cache-buffering-state=100`.
|
||||
-- As a workaround, we cycle through the paused state by sending two SPACE
|
||||
-- keypresses.
|
||||
-- What didn't work:
|
||||
-- - Cycling through the `pause` property.
|
||||
-- - Run the `playlist-play-index current` command.
|
||||
mp.commandv("keypress", 'SPACE')
|
||||
mp.commandv("keypress", 'SPACE')
|
||||
end
|
||||
|
||||
-- main
|
||||
|
||||
read_settings()
|
||||
|
||||
if settings.reload_key_binding ~= "" then
|
||||
mp.add_key_binding(settings.reload_key_binding, "reload_resume", reload_resume)
|
||||
end
|
||||
|
||||
if settings.paused_for_cache_timer_enabled then
|
||||
mp.observe_property("paused-for-cache", "bool", paused_for_cache.handler)
|
||||
end
|
||||
|
||||
if settings.demuxer_cache_timer_enabled then
|
||||
demuxer_cache.initialize(settings.demuxer_cache_timer_interval)
|
||||
end
|
||||
|
||||
if settings.reload_eof_enabled then
|
||||
-- vo-configured == video output created && its configuration went ok
|
||||
mp.observe_property(
|
||||
"vo-configured",
|
||||
"bool",
|
||||
function(name, vo_configured)
|
||||
msg.debug(name, vo_configured)
|
||||
if vo_configured then
|
||||
property_path = mp.get_property("path")
|
||||
property_keep_open = mp.get_property("keep-open")
|
||||
mp.set_property("keep-open", "yes")
|
||||
mp.set_property("keep-open-pause", "no")
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
mp.observe_property("eof-reached", "bool", reload_eof)
|
||||
end
|
||||
|
||||
mp.register_event("file-loaded", on_file_loaded)
|
||||
151
scripts/skip-intro.lua
Normal file
151
scripts/skip-intro.lua
Normal file
@@ -0,0 +1,151 @@
|
||||
MAX_SPEED = 100
|
||||
NORMAL_SPEED = 1
|
||||
ONE_SECOND = 1
|
||||
skip = false
|
||||
-- Max noise (dB) and min silence duration (s) to trigger
|
||||
opts = { quietness = -45, duration = 0.85 }
|
||||
|
||||
|
||||
function setOptions()
|
||||
local options = require 'mp.options'
|
||||
options.read_options(opts)
|
||||
end
|
||||
|
||||
function setTime(time)
|
||||
mp.set_property_number('time-pos', time)
|
||||
end
|
||||
|
||||
function getTime()
|
||||
return mp.get_property_native('time-pos')
|
||||
end
|
||||
|
||||
function setSpeed(speed)
|
||||
mp.set_property('speed', speed)
|
||||
end
|
||||
|
||||
function setPause(state)
|
||||
mp.set_property_bool('pause', state)
|
||||
end
|
||||
|
||||
function setMute(state)
|
||||
mp.set_property_bool('mute', state)
|
||||
end
|
||||
|
||||
function initAudioFilter()
|
||||
local af_table = mp.get_property_native('af')
|
||||
af_table[#af_table + 1] = {
|
||||
enabled = false,
|
||||
label = 'silencedetect',
|
||||
name = 'lavfi',
|
||||
params = { graph = 'silencedetect=noise=' .. opts.quietness .. 'dB:d=' .. opts.duration }
|
||||
}
|
||||
mp.set_property_native('af', af_table)
|
||||
end
|
||||
|
||||
function initVideoFilter()
|
||||
local vf_table = mp.get_property_native('vf')
|
||||
vf_table[#vf_table + 1] = {
|
||||
enabled = false,
|
||||
label = 'blackout',
|
||||
name = 'lavfi',
|
||||
params = { graph = '' }
|
||||
}
|
||||
mp.set_property_native('vf', vf_table)
|
||||
end
|
||||
|
||||
function setAudioFilter(state)
|
||||
local af_table = mp.get_property_native('af')
|
||||
if #af_table > 0 then
|
||||
for i = #af_table, 1, -1 do
|
||||
if af_table[i].label == 'silencedetect' then
|
||||
af_table[i].enabled = state
|
||||
mp.set_property_native('af', af_table)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function dim(state)
|
||||
local dim = { width = 0, height = 0 }
|
||||
if state == true then
|
||||
dim.width = mp.get_property_native('width')
|
||||
dim.height = mp.get_property_native('height')
|
||||
end
|
||||
return dim.width .. 'x' .. dim.height
|
||||
end
|
||||
|
||||
function setVideoFilter(state)
|
||||
local vf_table = mp.get_property_native('vf')
|
||||
if #vf_table > 0 then
|
||||
for i = #vf_table, 1, -1 do
|
||||
if vf_table[i].label == 'blackout' then
|
||||
vf_table[i].enabled = state
|
||||
vf_table[i].params = { graph = 'nullsink,color=c=black:s=' .. dim(state) }
|
||||
mp.set_property_native('vf', vf_table)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function silenceTrigger(name, value)
|
||||
if value == '{}' or value == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local skipTime = tonumber(string.match(value, '%d+%.?%d+'))
|
||||
local currTime = getTime()
|
||||
|
||||
if skipTime == nil or skipTime < currTime + ONE_SECOND then
|
||||
return
|
||||
end
|
||||
|
||||
stopSkip()
|
||||
setTime(skipTime)
|
||||
skip = false
|
||||
end
|
||||
|
||||
function setAudioTrigger(state)
|
||||
if state == true then
|
||||
mp.observe_property('af-metadata/silencedetect', 'string', silenceTrigger)
|
||||
else
|
||||
mp.unobserve_property(silenceTrigger)
|
||||
end
|
||||
end
|
||||
|
||||
function startSkip()
|
||||
startTime = getTime()
|
||||
-- This audio filter detects moments of silence
|
||||
setAudioFilter(true)
|
||||
-- This video filter makes fast-forward faster
|
||||
setVideoFilter(true)
|
||||
setAudioTrigger(true)
|
||||
setPause(false)
|
||||
setMute(true)
|
||||
setSpeed(MAX_SPEED)
|
||||
end
|
||||
|
||||
function stopSkip()
|
||||
setAudioFilter(false)
|
||||
setVideoFilter(false)
|
||||
setAudioTrigger(false)
|
||||
setMute(false)
|
||||
setSpeed(NORMAL_SPEED)
|
||||
end
|
||||
|
||||
function keypress()
|
||||
skip = not skip
|
||||
if skip then
|
||||
startSkip()
|
||||
else
|
||||
stopSkip()
|
||||
setTime(startTime)
|
||||
end
|
||||
end
|
||||
|
||||
setOptions(opts)
|
||||
initAudioFilter()
|
||||
initVideoFilter()
|
||||
|
||||
mp.add_key_binding('Tab', 'skip-key', keypress)
|
||||
568
scripts/sponsorblock.lua
Normal file
568
scripts/sponsorblock.lua
Normal file
@@ -0,0 +1,568 @@
|
||||
-- sponsorblock.lua
|
||||
--
|
||||
-- This script skips sponsored segments of YouTube videos
|
||||
-- using data from https://github.com/ajayyy/SponsorBlock
|
||||
|
||||
local ON_WINDOWS = package.config:sub(1,1) ~= "/"
|
||||
|
||||
local options = {
|
||||
server_address = "https://sponsor.ajay.app",
|
||||
|
||||
python_path = ON_WINDOWS and "python" or "python3",
|
||||
|
||||
-- Categories to fetch
|
||||
categories = "sponsor,intro,outro,interaction,selfpromo,filler",
|
||||
|
||||
-- Categories to skip automatically
|
||||
skip_categories = "sponsor",
|
||||
|
||||
-- If true, sponsored segments will only be skipped once
|
||||
skip_once = true,
|
||||
|
||||
-- Note that sponsored segments may ocasionally be inaccurate if this is turned off
|
||||
-- see https://blog.ajay.app/voting-and-pseudo-randomness-or-sponsorblock-or-youtube-sponsorship-segment-blocker
|
||||
local_database = false,
|
||||
|
||||
-- Update database on first run, does nothing if local_database is false
|
||||
auto_update = true,
|
||||
|
||||
-- How long to wait between local database updates
|
||||
-- Format: "X[d,h,m]", leave blank to update on every mpv run
|
||||
auto_update_interval = "6h",
|
||||
|
||||
-- User ID used to submit sponsored segments, leave blank for random
|
||||
user_id = "",
|
||||
|
||||
-- Name to display on the stats page https://sponsor.ajay.app/stats/ leave blank to keep current name
|
||||
display_name = "",
|
||||
|
||||
-- Tell the server when a skip happens
|
||||
report_views = true,
|
||||
|
||||
-- Auto upvote skipped sponsors
|
||||
auto_upvote = false,
|
||||
|
||||
-- Use sponsor times from server if they're more up to date than our local database
|
||||
server_fallback = true,
|
||||
|
||||
-- Create chapters at sponsor boundaries for OSC display and manual skipping
|
||||
make_chapters = true,
|
||||
|
||||
-- Minimum duration for sponsors (in seconds), segments under that threshold will be ignored
|
||||
min_duration = 1,
|
||||
|
||||
-- Fade audio for smoother transitions
|
||||
audio_fade = false,
|
||||
|
||||
-- Audio fade step, applied once every 100ms until cap is reached
|
||||
audio_fade_step = 10,
|
||||
|
||||
-- Audio fade cap
|
||||
audio_fade_cap = 0,
|
||||
|
||||
-- Fast forward through sponsors instead of skipping
|
||||
fast_forward = false,
|
||||
|
||||
-- Playback speed modifier when fast forwarding, applied once every second until cap is reached
|
||||
fast_forward_increase = .2,
|
||||
|
||||
-- Playback speed cap
|
||||
fast_forward_cap = 2,
|
||||
|
||||
-- Length of the sha256 prefix (3-32) when querying server, 0 to disable
|
||||
sha256_length = 4,
|
||||
|
||||
-- Pattern for video id in local files, ignored if blank
|
||||
-- Recommended value for base youtube-dl is "-([%w-_]+)%.[mw][kpe][v4b]m?$"
|
||||
local_pattern = "",
|
||||
|
||||
-- Legacy option, use skip_categories instead
|
||||
skip = true
|
||||
}
|
||||
|
||||
mp.options = require "mp.options"
|
||||
mp.options.read_options(options, "sponsorblock")
|
||||
|
||||
local legacy = mp.command_native_async == nil
|
||||
--[[
|
||||
if legacy then
|
||||
options.local_database = false
|
||||
end
|
||||
--]]
|
||||
options.local_database = false
|
||||
|
||||
local utils = require "mp.utils"
|
||||
scripts_dir = mp.find_config_file("scripts")
|
||||
|
||||
local sponsorblock = utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.py")
|
||||
local uid_path = utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.txt")
|
||||
local database_file = options.local_database and utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.db") or ""
|
||||
local youtube_id = nil
|
||||
local ranges = {}
|
||||
local init = false
|
||||
local segment = {a = 0, b = 0, progress = 0, first = true}
|
||||
local retrying = false
|
||||
local last_skip = {uuid = "", dir = nil}
|
||||
local speed_timer = nil
|
||||
local fade_timer = nil
|
||||
local fade_dir = nil
|
||||
local volume_before = mp.get_property_number("volume")
|
||||
local categories = {}
|
||||
local all_categories = {"sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "filler"}
|
||||
local chapter_cache = {}
|
||||
|
||||
for category in string.gmatch(options.skip_categories, "([^,]+)") do
|
||||
categories[category] = true
|
||||
end
|
||||
|
||||
function file_exists(name)
|
||||
local f = io.open(name,"r")
|
||||
if f ~= nil then io.close(f) return true else return false end
|
||||
end
|
||||
|
||||
function t_count(t)
|
||||
local count = 0
|
||||
for _ in pairs(t) do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
function time_sort(a, b)
|
||||
if a.time == b.time then
|
||||
return string.match(a.title, "segment end")
|
||||
end
|
||||
return a.time < b.time
|
||||
end
|
||||
|
||||
function parse_update_interval()
|
||||
local s = options.auto_update_interval
|
||||
if s == "" then return 0 end -- Interval Disabled
|
||||
|
||||
local num, mod = s:match "^(%d+)([hdm])$"
|
||||
|
||||
if num == nil or mod == nil then
|
||||
mp.osd_message("[sponsorblock] auto_update_interval " .. s .. " is invalid", 5)
|
||||
return nil
|
||||
end
|
||||
|
||||
local time_table = {
|
||||
m = 60,
|
||||
h = 60 * 60,
|
||||
d = 60 * 60 * 24,
|
||||
}
|
||||
|
||||
return num * time_table[mod]
|
||||
end
|
||||
|
||||
function clean_chapters()
|
||||
local chapters = mp.get_property_native("chapter-list")
|
||||
local new_chapters = {}
|
||||
for _, chapter in pairs(chapters) do
|
||||
if chapter.title ~= "Preview segment start" and chapter.title ~= "Preview segment end" then
|
||||
table.insert(new_chapters, chapter)
|
||||
end
|
||||
end
|
||||
mp.set_property_native("chapter-list", new_chapters)
|
||||
end
|
||||
|
||||
function create_chapter(chapter_title, chapter_time)
|
||||
local chapters = mp.get_property_native("chapter-list")
|
||||
local duration = mp.get_property_native("duration")
|
||||
table.insert(chapters, {title=chapter_title, time=(duration == nil or duration > chapter_time) and chapter_time or duration - .001})
|
||||
table.sort(chapters, time_sort)
|
||||
mp.set_property_native("chapter-list", chapters)
|
||||
end
|
||||
|
||||
function process(uuid, t, new_ranges)
|
||||
start_time = tonumber(string.match(t, "[^,]+"))
|
||||
end_time = tonumber(string.sub(string.match(t, ",[^,]+"), 2))
|
||||
for o_uuid, o_t in pairs(ranges) do
|
||||
if (start_time >= o_t.start_time and start_time <= o_t.end_time) or (o_t.start_time >= start_time and o_t.start_time <= end_time) then
|
||||
new_ranges[o_uuid] = o_t
|
||||
return
|
||||
end
|
||||
end
|
||||
category = string.match(t, "[^,]+$")
|
||||
if categories[category] and end_time - start_time >= options.min_duration then
|
||||
new_ranges[uuid] = {
|
||||
start_time = start_time,
|
||||
end_time = end_time,
|
||||
category = category,
|
||||
skipped = false
|
||||
}
|
||||
end
|
||||
if options.make_chapters and not chapter_cache[uuid] then
|
||||
chapter_cache[uuid] = true
|
||||
local category_title = (category:gsub("^%l", string.upper):gsub("_", " "))
|
||||
create_chapter(category_title .. " segment start (" .. string.sub(uuid, 1, 6) .. ")", start_time)
|
||||
create_chapter(category_title .. " segment end (" .. string.sub(uuid, 1, 6) .. ")", end_time)
|
||||
end
|
||||
end
|
||||
|
||||
function getranges(_, exists, db, more)
|
||||
if type(exists) == "table" and exists["status"] == "1" then
|
||||
if options.server_fallback then
|
||||
mp.add_timeout(0, function() getranges(true, true, "") end)
|
||||
else
|
||||
return mp.osd_message("[sponsorblock] database update failed, gave up")
|
||||
end
|
||||
end
|
||||
if db ~= "" and db ~= database_file then db = database_file end
|
||||
if exists ~= true and not file_exists(db) then
|
||||
if not retrying then
|
||||
mp.osd_message("[sponsorblock] database update failed, retrying...")
|
||||
retrying = true
|
||||
end
|
||||
return update()
|
||||
end
|
||||
if retrying then
|
||||
mp.osd_message("[sponsorblock] database update succeeded")
|
||||
retrying = false
|
||||
end
|
||||
local sponsors
|
||||
local args = {
|
||||
options.python_path,
|
||||
sponsorblock,
|
||||
"ranges",
|
||||
db,
|
||||
options.server_address,
|
||||
youtube_id,
|
||||
options.categories,
|
||||
tostring(options.sha256_length)
|
||||
}
|
||||
if not legacy then
|
||||
sponsors = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
|
||||
else
|
||||
sponsors = utils.subprocess({args = args})
|
||||
end
|
||||
mp.msg.debug("Got: " .. string.gsub(sponsors.stdout, "[\n\r]", ""))
|
||||
if not string.match(sponsors.stdout, "^%s*(.*%S)") then return end
|
||||
if string.match(sponsors.stdout, "error") then return getranges(true, true) end
|
||||
local new_ranges = {}
|
||||
local r_count = 0
|
||||
if more then r_count = -1 end
|
||||
for t in string.gmatch(sponsors.stdout, "[^:%s]+") do
|
||||
uuid = string.match(t, "([^,]+),[^,]+$")
|
||||
if ranges[uuid] then
|
||||
new_ranges[uuid] = ranges[uuid]
|
||||
else
|
||||
process(uuid, t, new_ranges)
|
||||
end
|
||||
r_count = r_count + 1
|
||||
end
|
||||
local c_count = t_count(ranges)
|
||||
if c_count == 0 or r_count >= c_count then
|
||||
ranges = new_ranges
|
||||
end
|
||||
end
|
||||
|
||||
function fast_forward()
|
||||
if options.fast_forward and options.fast_forward == true then
|
||||
speed_timer = nil
|
||||
mp.set_property("speed", 1)
|
||||
end
|
||||
local last_speed = mp.get_property_number("speed")
|
||||
local new_speed = math.min(last_speed + options.fast_forward_increase, options.fast_forward_cap)
|
||||
if new_speed <= last_speed then return end
|
||||
mp.set_property("speed", new_speed)
|
||||
end
|
||||
|
||||
function fade_audio(step)
|
||||
local last_volume = mp.get_property_number("volume")
|
||||
local new_volume = math.max(options.audio_fade_cap, math.min(last_volume + step, volume_before))
|
||||
if new_volume == last_volume then
|
||||
if step >= 0 then fade_dir = nil end
|
||||
if fade_timer ~= nil then fade_timer:kill() end
|
||||
fade_timer = nil
|
||||
return
|
||||
end
|
||||
mp.set_property("volume", new_volume)
|
||||
end
|
||||
|
||||
function skip_ads(name, pos)
|
||||
if pos == nil then return end
|
||||
local sponsor_ahead = false
|
||||
for uuid, t in pairs(ranges) do
|
||||
if (options.fast_forward == uuid or not options.skip_once or not t.skipped) and t.start_time <= pos and t.end_time > pos then
|
||||
if options.fast_forward == uuid then return end
|
||||
if options.fast_forward == false then
|
||||
mp.osd_message("[sponsorblock] " .. t.category .. " skipped")
|
||||
mp.set_property("time-pos", t.end_time)
|
||||
else
|
||||
mp.osd_message("[sponsorblock] skipping " .. t.category)
|
||||
end
|
||||
t.skipped = true
|
||||
last_skip = {uuid = uuid, dir = nil}
|
||||
if options.report_views or options.auto_upvote then
|
||||
local args = {
|
||||
options.python_path,
|
||||
sponsorblock,
|
||||
"stats",
|
||||
database_file,
|
||||
options.server_address,
|
||||
youtube_id,
|
||||
uuid,
|
||||
options.report_views and "1" or "",
|
||||
uid_path,
|
||||
options.user_id,
|
||||
options.auto_upvote and "1" or ""
|
||||
}
|
||||
if not legacy then
|
||||
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
|
||||
else
|
||||
utils.subprocess_detached({args = args})
|
||||
end
|
||||
end
|
||||
if options.fast_forward ~= false then
|
||||
options.fast_forward = uuid
|
||||
if speed_timer ~= nil then speed_timer:kill() end
|
||||
speed_timer = mp.add_periodic_timer(1, fast_forward)
|
||||
end
|
||||
return
|
||||
elseif (not options.skip_once or not t.skipped) and t.start_time <= pos + 1 and t.end_time > pos + 1 then
|
||||
sponsor_ahead = true
|
||||
end
|
||||
end
|
||||
if options.audio_fade then
|
||||
if sponsor_ahead then
|
||||
if fade_dir ~= false then
|
||||
if fade_dir == nil then volume_before = mp.get_property_number("volume") end
|
||||
if fade_timer ~= nil then fade_timer:kill() end
|
||||
fade_dir = false
|
||||
fade_timer = mp.add_periodic_timer(.1, function() fade_audio(-options.audio_fade_step) end)
|
||||
end
|
||||
elseif fade_dir == false then
|
||||
fade_dir = true
|
||||
if fade_timer ~= nil then fade_timer:kill() end
|
||||
fade_timer = mp.add_periodic_timer(.1, function() fade_audio(options.audio_fade_step) end)
|
||||
end
|
||||
end
|
||||
if options.fast_forward and options.fast_forward ~= true then
|
||||
options.fast_forward = true
|
||||
speed_timer:kill()
|
||||
speed_timer = nil
|
||||
mp.set_property("speed", 1)
|
||||
end
|
||||
end
|
||||
|
||||
function vote(dir)
|
||||
if last_skip.uuid == "" then return mp.osd_message("[sponsorblock] no sponsors skipped, can't submit vote") end
|
||||
local updown = dir == "1" and "up" or "down"
|
||||
if last_skip.dir == dir then return mp.osd_message("[sponsorblock] " .. updown .. "vote already submitted") end
|
||||
last_skip.dir = dir
|
||||
local args = {
|
||||
options.python_path,
|
||||
sponsorblock,
|
||||
"stats",
|
||||
database_file,
|
||||
options.server_address,
|
||||
youtube_id,
|
||||
last_skip.uuid,
|
||||
"",
|
||||
uid_path,
|
||||
options.user_id,
|
||||
dir
|
||||
}
|
||||
if not legacy then
|
||||
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
|
||||
else
|
||||
utils.subprocess({args = args})
|
||||
end
|
||||
mp.osd_message("[sponsorblock] " .. updown .. "vote submitted")
|
||||
end
|
||||
|
||||
function update()
|
||||
mp.command_native_async({name = "subprocess", playback_only = false, args = {
|
||||
options.python_path,
|
||||
sponsorblock,
|
||||
"update",
|
||||
database_file,
|
||||
options.server_address
|
||||
}}, getranges)
|
||||
end
|
||||
|
||||
function file_loaded()
|
||||
local initialized = init
|
||||
ranges = {}
|
||||
segment = {a = 0, b = 0, progress = 0, first = true}
|
||||
last_skip = {uuid = "", dir = nil}
|
||||
chapter_cache = {}
|
||||
local video_path = mp.get_property("path", "")
|
||||
mp.msg.debug("Path: " .. video_path)
|
||||
local video_referer = string.match(mp.get_property("http-header-fields", ""), "Referer:([^,]+)") or ""
|
||||
mp.msg.debug("Referer: " .. video_referer)
|
||||
|
||||
local urls = {
|
||||
"https?://youtu%.be/([%w-_]+).*",
|
||||
"https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*",
|
||||
"/watch.*[?&]v=([%w-_]+).*",
|
||||
"/embed/([%w-_]+).*"
|
||||
}
|
||||
youtube_id = nil
|
||||
for i, url in ipairs(urls) do
|
||||
youtube_id = youtube_id or string.match(video_path, url) or string.match(video_referer, url)
|
||||
if youtube_id then break end
|
||||
end
|
||||
youtube_id = youtube_id or string.match(video_path, options.local_pattern)
|
||||
|
||||
if not youtube_id or string.len(youtube_id) < 11 or (local_pattern and string.len(youtube_id) ~= 11) then return end
|
||||
youtube_id = string.sub(youtube_id, 1, 11)
|
||||
mp.msg.debug("Found YouTube ID: " .. youtube_id)
|
||||
init = true
|
||||
if not options.local_database then
|
||||
getranges(true, true)
|
||||
else
|
||||
local exists = file_exists(database_file)
|
||||
if exists and options.server_fallback then
|
||||
getranges(true, true)
|
||||
mp.add_timeout(0, function() getranges(true, true, "", true) end)
|
||||
elseif exists then
|
||||
getranges(true, true)
|
||||
elseif options.server_fallback then
|
||||
mp.add_timeout(0, function() getranges(true, true, "") end)
|
||||
end
|
||||
end
|
||||
if initialized then return end
|
||||
if options.skip then
|
||||
mp.observe_property("time-pos", "native", skip_ads)
|
||||
end
|
||||
if options.display_name ~= "" then
|
||||
local args = {
|
||||
options.python_path,
|
||||
sponsorblock,
|
||||
"username",
|
||||
database_file,
|
||||
options.server_address,
|
||||
youtube_id,
|
||||
"",
|
||||
"",
|
||||
uid_path,
|
||||
options.user_id,
|
||||
options.display_name
|
||||
}
|
||||
if not legacy then
|
||||
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
|
||||
else
|
||||
utils.subprocess_detached({args = args})
|
||||
end
|
||||
end
|
||||
if not options.local_database or (not options.auto_update and file_exists(database_file)) then return end
|
||||
|
||||
if file_exists(database_file) then
|
||||
local db_info = utils.file_info(database_file)
|
||||
local cur_time = os.time(os.date("*t"))
|
||||
local upd_interval = parse_update_interval()
|
||||
if upd_interval == nil or os.difftime(cur_time, db_info.mtime) < upd_interval then return end
|
||||
end
|
||||
|
||||
update()
|
||||
end
|
||||
|
||||
function set_segment()
|
||||
if not youtube_id then return end
|
||||
local pos = mp.get_property_number("time-pos")
|
||||
if pos == nil then return end
|
||||
if segment.progress > 1 then
|
||||
segment.progress = segment.progress - 2
|
||||
end
|
||||
if segment.progress == 1 then
|
||||
segment.progress = 0
|
||||
segment.b = pos
|
||||
mp.osd_message("[sponsorblock] segment boundary B set, press again for boundary A", 3)
|
||||
else
|
||||
segment.progress = 1
|
||||
segment.a = pos
|
||||
mp.osd_message("[sponsorblock] segment boundary A set, press again for boundary B", 3)
|
||||
end
|
||||
if options.make_chapters and not segment.first then
|
||||
local start_time = math.min(segment.a, segment.b)
|
||||
local end_time = math.max(segment.a, segment.b)
|
||||
if end_time - start_time ~= 0 and end_time ~= 0 then
|
||||
clean_chapters()
|
||||
create_chapter("Preview segment start", start_time)
|
||||
create_chapter("Preview segment end", end_time)
|
||||
end
|
||||
end
|
||||
segment.first = false
|
||||
end
|
||||
|
||||
function select_category(selected)
|
||||
for category in string.gmatch(options.categories, "([^,]+)") do
|
||||
mp.remove_key_binding("select_category_"..category)
|
||||
mp.remove_key_binding("kp_select_category_"..category)
|
||||
end
|
||||
submit_segment(selected)
|
||||
end
|
||||
|
||||
function submit_segment(category)
|
||||
if not youtube_id then return end
|
||||
local start_time = math.min(segment.a, segment.b)
|
||||
local end_time = math.max(segment.a, segment.b)
|
||||
if end_time - start_time == 0 or end_time == 0 then
|
||||
mp.osd_message("[sponsorblock] empty segment, not submitting")
|
||||
elseif segment.progress <= 1 then
|
||||
segment.progress = segment.progress + 2
|
||||
local category_list = ""
|
||||
for category_id, category in pairs(all_categories) do
|
||||
local category_title = (category:gsub("^%l", string.upper):gsub("_", " "))
|
||||
category_list = category_list .. category_id .. ": " .. category_title .. "\n"
|
||||
mp.add_forced_key_binding(tostring(category_id), "select_category_"..category, function() select_category(category) end)
|
||||
mp.add_forced_key_binding("KP"..tostring(category_id), "kp_select_category_"..category, function() select_category(category) end)
|
||||
end
|
||||
mp.osd_message(string.format("[sponsorblock] press a number to select category for segment: %.2d:%.2d:%.2d to %.2d:%.2d:%.2d\n\n" .. category_list .. "\nyou can press Shift+G again for default (Sponsor) or hide this message with g", math.floor(start_time/(60*60)), math.floor(start_time/60%60), math.floor(start_time%60), math.floor(end_time/(60*60)), math.floor(end_time/60%60), math.floor(end_time%60)), 30)
|
||||
else
|
||||
mp.osd_message("[sponsorblock] submitting segment...", 30)
|
||||
local submit
|
||||
local args = {
|
||||
options.python_path,
|
||||
sponsorblock,
|
||||
"submit",
|
||||
database_file,
|
||||
options.server_address,
|
||||
youtube_id,
|
||||
tostring(start_time),
|
||||
tostring(end_time),
|
||||
uid_path,
|
||||
options.user_id,
|
||||
category or "sponsor"
|
||||
}
|
||||
if not legacy then
|
||||
submit = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
|
||||
else
|
||||
submit = utils.subprocess({args = args})
|
||||
end
|
||||
if string.match(submit.stdout, "success") then
|
||||
segment = {a = 0, b = 0, progress = 0, first = true}
|
||||
mp.osd_message("[sponsorblock] segment submitted")
|
||||
if options.make_chapters then
|
||||
clean_chapters()
|
||||
create_chapter("Submitted segment start", start_time)
|
||||
create_chapter("Submitted segment end", end_time)
|
||||
end
|
||||
elseif string.match(submit.stdout, "error") then
|
||||
mp.osd_message("[sponsorblock] segment submission failed, server may be down. try again", 5)
|
||||
elseif string.match(submit.stdout, "502") then
|
||||
mp.osd_message("[sponsorblock] segment submission failed, server is down. try again", 5)
|
||||
elseif string.match(submit.stdout, "400") then
|
||||
mp.osd_message("[sponsorblock] segment submission failed, impossible inputs", 5)
|
||||
segment = {a = 0, b = 0, progress = 0, first = true}
|
||||
elseif string.match(submit.stdout, "429") then
|
||||
mp.osd_message("[sponsorblock] segment submission failed, rate limited. try again", 5)
|
||||
elseif string.match(submit.stdout, "409") then
|
||||
mp.osd_message("[sponsorblock] segment already submitted", 3)
|
||||
segment = {a = 0, b = 0, progress = 0, first = true}
|
||||
else
|
||||
mp.osd_message("[sponsorblock] segment submission failed", 5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mp.register_event("file-loaded", file_loaded)
|
||||
mp.add_key_binding("g", "set_segment", set_segment)
|
||||
mp.add_key_binding("G", "submit_segment", submit_segment)
|
||||
mp.add_key_binding("h", "upvote_segment", function() return vote("1") end)
|
||||
mp.add_key_binding("H", "downvote_segment", function() return vote("0") end)
|
||||
-- Bindings below are for backwards compatibility and could be removed at any time
|
||||
mp.add_key_binding(nil, "sponsorblock_set_segment", set_segment)
|
||||
mp.add_key_binding(nil, "sponsorblock_submit_segment", submit_segment)
|
||||
mp.add_key_binding(nil, "sponsorblock_upvote", function() return vote("1") end)
|
||||
mp.add_key_binding(nil, "sponsorblock_downvote", function() return vote("0") end)
|
||||
3
scripts/sponsorblock_shared/main.lua
Normal file
3
scripts/sponsorblock_shared/main.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
-- This is a dummy main.lua
|
||||
-- required for mpv 0.33
|
||||
-- do not delete
|
||||
122
scripts/sponsorblock_shared/sponsorblock.py
Normal file
122
scripts/sponsorblock_shared/sponsorblock.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import hashlib
|
||||
import sqlite3
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
if sys.argv[1] in ["submit", "stats", "username"]:
|
||||
if not sys.argv[8]:
|
||||
if os.path.isfile(sys.argv[7]):
|
||||
with open(sys.argv[7]) as f:
|
||||
uid = f.read()
|
||||
else:
|
||||
uid = "".join(random.choices(string.ascii_letters + string.digits, k=36))
|
||||
with open(sys.argv[7], "w") as f:
|
||||
f.write(uid)
|
||||
else:
|
||||
uid = sys.argv[8]
|
||||
|
||||
opener = urllib.request.build_opener()
|
||||
opener.addheaders = [("User-Agent", "mpv_sponsorblock/1.0 (https://github.com/po5/mpv_sponsorblock)")]
|
||||
urllib.request.install_opener(opener)
|
||||
|
||||
if sys.argv[1] == "ranges" and (not sys.argv[2] or not os.path.isfile(sys.argv[2])):
|
||||
sha = None
|
||||
if 3 <= int(sys.argv[6]) <= 32:
|
||||
sha = hashlib.sha256(sys.argv[4].encode()).hexdigest()[:int(sys.argv[6])]
|
||||
times = []
|
||||
try:
|
||||
response = urllib.request.urlopen(sys.argv[3] + "/api/skipSegments" + ("/" + sha + "?" if sha else "?videoID=" + sys.argv[4] + "&") + urllib.parse.urlencode([("categories", json.dumps(sys.argv[5].split(",")))]))
|
||||
segments = json.load(response)
|
||||
for segment in segments:
|
||||
if sha and sys.argv[4] != segment["videoID"]:
|
||||
continue
|
||||
if sha:
|
||||
for s in segment["segments"]:
|
||||
times.append(str(s["segment"][0]) + "," + str(s["segment"][1]) + "," + s["UUID"] + "," + s["category"])
|
||||
else:
|
||||
times.append(str(segment["segment"][0]) + "," + str(segment["segment"][1]) + "," + segment["UUID"] + "," + segment["category"])
|
||||
print(":".join(times))
|
||||
except (TimeoutError, urllib.error.URLError) as e:
|
||||
print("error")
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
print("")
|
||||
else:
|
||||
print("error")
|
||||
elif sys.argv[1] == "ranges":
|
||||
conn = sqlite3.connect(sys.argv[2])
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
times = []
|
||||
for category in sys.argv[5].split(","):
|
||||
c.execute("SELECT startTime, endTime, votes, UUID, category FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1 AND category = ?", (sys.argv[4], category))
|
||||
sponsors = c.fetchall()
|
||||
best = list(sponsors)
|
||||
dealtwith = []
|
||||
similar = []
|
||||
for sponsor_a in sponsors:
|
||||
for sponsor_b in sponsors:
|
||||
if sponsor_a is not sponsor_b and sponsor_a["startTime"] >= sponsor_b["startTime"] and sponsor_a["startTime"] <= sponsor_b["endTime"]:
|
||||
similar.append([sponsor_a, sponsor_b])
|
||||
if sponsor_a in best:
|
||||
best.remove(sponsor_a)
|
||||
if sponsor_b in best:
|
||||
best.remove(sponsor_b)
|
||||
for sponsors_a in similar:
|
||||
if sponsors_a in dealtwith:
|
||||
continue
|
||||
group = set(sponsors_a)
|
||||
for sponsors_b in similar:
|
||||
if sponsors_b[0] in group or sponsors_b[1] in group:
|
||||
group.add(sponsors_b[0])
|
||||
group.add(sponsors_b[1])
|
||||
dealtwith.append(sponsors_b)
|
||||
best.append(max(group, key=lambda x:x["votes"]))
|
||||
for time in best:
|
||||
times.append(str(time["startTime"]) + "," + str(time["endTime"]) + "," + time["UUID"] + "," + time["category"])
|
||||
print(":".join(times))
|
||||
elif sys.argv[1] == "update":
|
||||
try:
|
||||
urllib.request.urlretrieve(sys.argv[3] + "/database.db", sys.argv[2] + ".tmp")
|
||||
os.replace(sys.argv[2] + ".tmp", sys.argv[2])
|
||||
except PermissionError:
|
||||
print("database update failed, file currently in use", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except ConnectionResetError:
|
||||
print("database update failed, connection reset", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except TimeoutError:
|
||||
print("database update failed, timed out", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError:
|
||||
print("database update failed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif sys.argv[1] == "submit":
|
||||
try:
|
||||
req = urllib.request.Request(sys.argv[3] + "/api/skipSegments", data=json.dumps({"videoID": sys.argv[4], "segments": [{"segment": [float(sys.argv[5]), float(sys.argv[6])], "category": sys.argv[9]}], "userID": uid}).encode(), headers={"Content-Type": "application/json"})
|
||||
response = urllib.request.urlopen(req)
|
||||
print("success")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(e.code)
|
||||
except:
|
||||
print("error")
|
||||
elif sys.argv[1] == "stats":
|
||||
try:
|
||||
if sys.argv[6]:
|
||||
urllib.request.urlopen(sys.argv[3] + "/api/viewedVideoSponsorTime?UUID=" + sys.argv[5])
|
||||
if sys.argv[9]:
|
||||
urllib.request.urlopen(sys.argv[3] + "/api/voteOnSponsorTime?UUID=" + sys.argv[5] + "&userID=" + uid + "&type=" + sys.argv[9])
|
||||
except:
|
||||
pass
|
||||
elif sys.argv[1] == "username":
|
||||
try:
|
||||
data = urllib.parse.urlencode({"userID": uid, "userName": sys.argv[9]}).encode()
|
||||
req = urllib.request.Request(sys.argv[3] + "/api/setUsername", data=data)
|
||||
urllib.request.urlopen(req)
|
||||
except:
|
||||
pass
|
||||
1
scripts/sponsorblock_shared/sponsorblock.txt
Normal file
1
scripts/sponsorblock_shared/sponsorblock.txt
Normal file
@@ -0,0 +1 @@
|
||||
dFlkoCOSK3BQhXGGhUsXSNU0sPr7AV7avndk
|
||||
754
scripts/stats.lua
Normal file
754
scripts/stats.lua
Normal file
@@ -0,0 +1,754 @@
|
||||
-- Display some stats.
|
||||
--
|
||||
-- Please consult the readme for information about usage and configuration:
|
||||
-- https://github.com/Argon-/mpv-stats
|
||||
--
|
||||
-- Please note: not every property is always available and therefore not always
|
||||
-- visible.
|
||||
|
||||
local mp = require 'mp'
|
||||
local options = require 'mp.options'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
-- Options
|
||||
local o = {
|
||||
-- Default key bindings
|
||||
key_oneshot = "i",
|
||||
key_toggle = "I",
|
||||
key_page_1 = "1",
|
||||
key_page_2 = "2",
|
||||
key_page_3 = "3",
|
||||
|
||||
duration = 4,
|
||||
redraw_delay = 1, -- acts as duration in the toggling case
|
||||
ass_formatting = true,
|
||||
persistent_overlay = false, -- whether the stats can be overwritten by other output
|
||||
print_perfdata_passes = false, -- when true, print the full information about all passes
|
||||
filter_params_max_length = 100, -- a filter list longer than this many characters will be shown one filter per line instead
|
||||
debug = false,
|
||||
|
||||
-- Graph options and style
|
||||
plot_perfdata = true,
|
||||
plot_vsync_ratio = true,
|
||||
plot_vsync_jitter = true,
|
||||
skip_frames = 5,
|
||||
global_max = true,
|
||||
flush_graph_data = true, -- clear data buffers when toggling
|
||||
plot_bg_border_color = "0000FF",
|
||||
plot_bg_color = "262626",
|
||||
plot_color = "FFFFFF",
|
||||
|
||||
-- Text style
|
||||
font = "Source Sans Pro",
|
||||
font_mono = "Source Sans Pro", -- monospaced digits are sufficient
|
||||
font_size = 8,
|
||||
font_color = "FFFFFF",
|
||||
border_size = 0.8,
|
||||
border_color = "262626",
|
||||
shadow_x_offset = 0.0,
|
||||
shadow_y_offset = 0.0,
|
||||
shadow_color = "000000",
|
||||
alpha = "11",
|
||||
|
||||
-- Custom header for ASS tags to style the text output.
|
||||
-- Specifying this will ignore the text style values above and just
|
||||
-- use this string instead.
|
||||
custom_header = "",
|
||||
|
||||
-- Text formatting
|
||||
-- With ASS
|
||||
ass_nl = "\\N",
|
||||
ass_indent = "\\h\\h\\h\\h\\h",
|
||||
ass_prefix_sep = "\\h\\h",
|
||||
ass_b1 = "{\\b1}",
|
||||
ass_b0 = "{\\b0}",
|
||||
ass_it1 = "{\\i1}",
|
||||
ass_it0 = "{\\i0}",
|
||||
-- Without ASS
|
||||
no_ass_nl = "\n",
|
||||
no_ass_indent = "\t",
|
||||
no_ass_prefix_sep = " ",
|
||||
no_ass_b1 = "\027[1m",
|
||||
no_ass_b0 = "\027[0m",
|
||||
no_ass_it1 = "\027[3m",
|
||||
no_ass_it0 = "\027[0m",
|
||||
}
|
||||
options.read_options(o)
|
||||
|
||||
local format = string.format
|
||||
local max = math.max
|
||||
local min = math.min
|
||||
|
||||
-- Function used to record performance data
|
||||
local recorder = nil
|
||||
-- Timer used for redrawing (toggling) and clearing the screen (oneshot)
|
||||
local display_timer = nil
|
||||
-- Current page and <page key>:<page function> mappings
|
||||
local curr_page = o.key_page_1
|
||||
local pages = {}
|
||||
-- Save these sequences locally as we'll need them a lot
|
||||
local ass_start = mp.get_property_osd("osd-ass-cc/0")
|
||||
local ass_stop = mp.get_property_osd("osd-ass-cc/1")
|
||||
-- Ring buffers for the values used to construct a graph.
|
||||
-- .pos denotes the current position, .len the buffer length
|
||||
-- .max is the max value in the corresponding buffer
|
||||
local vsratio_buf, vsjitter_buf
|
||||
local function init_buffers()
|
||||
vsratio_buf = {0, pos = 1, len = 50, max = 0}
|
||||
vsjitter_buf = {0, pos = 1, len = 50, max = 0}
|
||||
end
|
||||
-- Save all properties known to this version of mpv
|
||||
local property_list = {}
|
||||
for p in string.gmatch(mp.get_property("property-list"), "([^,]+)") do property_list[p] = true end
|
||||
-- Mapping of properties to their deprecated names
|
||||
local property_aliases = {
|
||||
["decoder-frame-drop-count"] = "drop-frame-count",
|
||||
["frame-drop-count"] = "vo-drop-frame-count",
|
||||
["container-fps"] = "fps",
|
||||
}
|
||||
|
||||
|
||||
-- Return deprecated name for the given property
|
||||
local function compat(p)
|
||||
while not property_list[p] and property_aliases[p] do
|
||||
p = property_aliases[p]
|
||||
end
|
||||
return p
|
||||
end
|
||||
|
||||
|
||||
local function set_ASS(b)
|
||||
if not o.use_ass or o.persistent_overlay then
|
||||
return ""
|
||||
end
|
||||
return b and ass_start or ass_stop
|
||||
end
|
||||
|
||||
|
||||
local function no_ASS(t)
|
||||
return set_ASS(false) .. t .. set_ASS(true)
|
||||
end
|
||||
|
||||
|
||||
local function b(t)
|
||||
return o.b1 .. t .. o.b0
|
||||
end
|
||||
|
||||
|
||||
local function it(t)
|
||||
return o.it1 .. t .. o.it0
|
||||
end
|
||||
|
||||
|
||||
local function text_style()
|
||||
if not o.use_ass then
|
||||
return ""
|
||||
end
|
||||
if o.custom_header and o.custom_header ~= "" then
|
||||
return set_ASS(true) .. o.custom_header
|
||||
else
|
||||
return format("%s{\\r}{\\an7}{\\fs%d}{\\fn%s}{\\bord%f}{\\3c&H%s&}" ..
|
||||
"{\\1c&H%s&}{\\alpha&H%s&}{\\xshad%f}{\\yshad%f}{\\4c&H%s&}",
|
||||
set_ASS(true), o.font_size, o.font, o.border_size,
|
||||
o.border_color, o.font_color, o.alpha, o.shadow_x_offset,
|
||||
o.shadow_y_offset, o.shadow_color)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function has_vo_window()
|
||||
return mp.get_property("vo-configured") == "yes"
|
||||
end
|
||||
|
||||
|
||||
local function has_ansi()
|
||||
local is_windows = type(package) == 'table'
|
||||
and type(package.config) == 'string'
|
||||
and package.config:sub(1, 1) == '\\'
|
||||
if is_windows then
|
||||
return os.getenv("ANSICON")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- Generate a graph from the given values.
|
||||
-- Returns an ASS formatted vector drawing as string.
|
||||
--
|
||||
-- values: Array/table of numbers representing the data. Used like a ring buffer
|
||||
-- it will get iterated backwards `len` times starting at position `i`.
|
||||
-- i : Index of the latest data value in `values`.
|
||||
-- len : The length/amount of numbers in `values`.
|
||||
-- v_max : The maximum number in `values`. It is used to scale all data
|
||||
-- values to a range of 0 to `v_max`.
|
||||
-- v_avg : The average number in `values`. It is used to try and center graphs
|
||||
-- if possible. May be left as nil
|
||||
-- scale : A value that will be multiplied with all data values.
|
||||
-- x_tics: Horizontal width multiplier for the steps
|
||||
local function generate_graph(values, i, len, v_max, v_avg, scale, x_tics)
|
||||
-- Check if at least one value exists
|
||||
if not values[i] then
|
||||
return ""
|
||||
end
|
||||
|
||||
local x_max = (len - 1) * x_tics
|
||||
local y_offset = o.border_size
|
||||
local y_max = o.font_size * 0.66
|
||||
local x = 0
|
||||
|
||||
-- try and center the graph if possible, but avoid going above `scale`
|
||||
if v_avg then
|
||||
scale = min(scale, v_max / (2 * v_avg))
|
||||
end
|
||||
|
||||
local s = {format("m 0 0 n %f %f l ", x, y_max - (y_max * values[i] / v_max * scale))}
|
||||
i = ((i - 2) % len) + 1
|
||||
|
||||
for p = 1, len - 1 do
|
||||
if values[i] then
|
||||
x = x - x_tics
|
||||
s[#s+1] = format("%f %f ", x, y_max - (y_max * values[i] / v_max * scale))
|
||||
end
|
||||
i = ((i - 2) % len) + 1
|
||||
end
|
||||
|
||||
s[#s+1] = format("%f %f %f %f", x, y_max, 0, y_max)
|
||||
|
||||
local bg_box = format("{\\bord0.5}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0",
|
||||
o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max)
|
||||
return format("%s{\\r}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s",
|
||||
o.prefix_sep, y_offset, bg_box, o.plot_color, table.concat(s), text_style())
|
||||
end
|
||||
|
||||
|
||||
local function append(s, str, attr)
|
||||
if not str then
|
||||
return false
|
||||
end
|
||||
attr.prefix_sep = attr.prefix_sep or o.prefix_sep
|
||||
attr.indent = attr.indent or o.indent
|
||||
attr.nl = attr.nl or o.nl
|
||||
attr.suffix = attr.suffix or ""
|
||||
attr.prefix = attr.prefix or ""
|
||||
attr.no_prefix_markup = attr.no_prefix_markup or false
|
||||
attr.prefix = attr.no_prefix_markup and attr.prefix or b(attr.prefix)
|
||||
s[#s+1] = format("%s%s%s%s%s%s", attr.nl, attr.indent,
|
||||
attr.prefix, attr.prefix_sep, no_ASS(str), attr.suffix)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- Format and append a property.
|
||||
-- A property whose value is either `nil` or empty (hereafter called "invalid")
|
||||
-- is skipped and not appended.
|
||||
-- Returns `false` in case nothing was appended, otherwise `true`.
|
||||
--
|
||||
-- s : Table containing strings.
|
||||
-- prop : The property to query and format (based on its OSD representation).
|
||||
-- attr : Optional table to overwrite certain (formatting) attributes for
|
||||
-- this property.
|
||||
-- exclude: Optional table containing keys which are considered invalid values
|
||||
-- for this property. Specifying this will replace empty string as
|
||||
-- default invalid value (nil is always invalid).
|
||||
local function append_property(s, prop, attr, excluded)
|
||||
excluded = excluded or {[""] = true}
|
||||
local ret = mp.get_property_osd(prop)
|
||||
if not ret or excluded[ret] then
|
||||
if o.debug then
|
||||
print("No value for property: " .. prop)
|
||||
end
|
||||
return false
|
||||
end
|
||||
return append(s, ret, attr)
|
||||
end
|
||||
|
||||
|
||||
local function append_perfdata(s, dedicated_page)
|
||||
local vo_p = mp.get_property_native("vo-passes")
|
||||
if not vo_p then
|
||||
return
|
||||
end
|
||||
|
||||
local ds = mp.get_property_bool("display-sync-active", false)
|
||||
local target_fps = ds and mp.get_property_number("display-fps", 0)
|
||||
or mp.get_property_number(compat("container-fps"), 0)
|
||||
if target_fps > 0 then target_fps = 1 / target_fps * 1e9 end
|
||||
|
||||
-- Sums of all last/avg/peak values
|
||||
local last_s, avg_s, peak_s = {}, {}, {}
|
||||
for frame, data in pairs(vo_p) do
|
||||
last_s[frame], avg_s[frame], peak_s[frame] = 0, 0, 0
|
||||
for _, pass in ipairs(data) do
|
||||
last_s[frame] = last_s[frame] + pass["last"]
|
||||
avg_s[frame] = avg_s[frame] + pass["avg"]
|
||||
peak_s[frame] = peak_s[frame] + pass["peak"]
|
||||
end
|
||||
end
|
||||
|
||||
-- Pretty print measured time
|
||||
local function pp(i)
|
||||
-- rescale to microseconds for a saner display
|
||||
return format("%05d", i / 1000)
|
||||
end
|
||||
|
||||
-- Format n/m with a font weight based on the ratio
|
||||
local function p(n, m)
|
||||
local i = 0
|
||||
if m > 0 then
|
||||
i = tonumber(n) / m
|
||||
end
|
||||
-- Calculate font weight. 100 is minimum, 400 is normal, 700 bold, 900 is max
|
||||
local w = (700 * math.sqrt(i)) + 200
|
||||
return format("{\\b%d}%02d%%{\\b0}", w, i * 100)
|
||||
end
|
||||
|
||||
s[#s+1] = format("%s%s%s%s{\\fs%s}%s{\\fs%s}",
|
||||
dedicated_page and "" or o.nl, dedicated_page and "" or o.indent,
|
||||
b("Frame Timings:"), o.prefix_sep, o.font_size * 0.66,
|
||||
"(last/average/peak μs)", o.font_size)
|
||||
|
||||
for frame, data in pairs(vo_p) do
|
||||
local f = "%s%s%s{\\fn%s}%s / %s / %s %s%s{\\fn%s}%s%s%s"
|
||||
|
||||
if dedicated_page then
|
||||
s[#s+1] = format("%s%s%s:", o.nl, o.indent,
|
||||
b(frame:gsub("^%l", string.upper)))
|
||||
|
||||
for _, pass in ipairs(data) do
|
||||
s[#s+1] = format(f, o.nl, o.indent, o.indent,
|
||||
o.font_mono, pp(pass["last"]),
|
||||
pp(pass["avg"]), pp(pass["peak"]),
|
||||
o.prefix_sep .. o.prefix_sep, p(pass["last"], last_s[frame]),
|
||||
o.font, o.prefix_sep, o.prefix_sep, pass["desc"])
|
||||
|
||||
if o.plot_perfdata and o.use_ass then
|
||||
s[#s+1] = generate_graph(pass["samples"], pass["count"],
|
||||
pass["count"], pass["peak"],
|
||||
pass["avg"], 0.9, 0.25)
|
||||
end
|
||||
end
|
||||
|
||||
-- Print sum of timing values as "Total"
|
||||
s[#s+1] = format(f, o.nl, o.indent, o.indent,
|
||||
o.font_mono, pp(last_s[frame]),
|
||||
pp(avg_s[frame]), pp(peak_s[frame]), "", "", o.font,
|
||||
o.prefix_sep, o.prefix_sep, b("Total"))
|
||||
else
|
||||
-- for the simplified view, we just print the sum of each pass
|
||||
s[#s+1] = format(f, o.nl, o.indent, o.indent, o.font_mono,
|
||||
pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]),
|
||||
"", "", o.font, o.prefix_sep, o.prefix_sep,
|
||||
frame:gsub("^%l", string.upper))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function append_display_sync(s)
|
||||
if not mp.get_property_bool("display-sync-active", false) then
|
||||
return
|
||||
end
|
||||
|
||||
local vspeed = append_property(s, "video-speed-correction", {prefix="DS:"})
|
||||
if vspeed then
|
||||
append_property(s, "audio-speed-correction",
|
||||
{prefix="/", nl="", indent=" ", prefix_sep=" ", no_prefix_markup=true})
|
||||
else
|
||||
append_property(s, "audio-speed-correction",
|
||||
{prefix="DS:" .. o.prefix_sep .. " - / ", prefix_sep=""})
|
||||
end
|
||||
|
||||
append_property(s, "mistimed-frame-count", {prefix="Mistimed:", nl=""})
|
||||
append_property(s, "vo-delayed-frame-count", {prefix="Delayed:", nl=""})
|
||||
|
||||
-- As we need to plot some graphs we print jitter and ratio on their own lines
|
||||
if not display_timer.oneshot and (o.plot_vsync_ratio or o.plot_vsync_jitter) and o.use_ass then
|
||||
local ratio_graph = ""
|
||||
local jitter_graph = ""
|
||||
if o.plot_vsync_ratio then
|
||||
ratio_graph = generate_graph(vsratio_buf, vsratio_buf.pos, vsratio_buf.len, vsratio_buf.max, nil, 0.8, 1)
|
||||
end
|
||||
if o.plot_vsync_jitter then
|
||||
jitter_graph = generate_graph(vsjitter_buf, vsjitter_buf.pos, vsjitter_buf.len, vsjitter_buf.max, nil, 0.8, 1)
|
||||
end
|
||||
append_property(s, "vsync-ratio", {prefix="VSync Ratio:", suffix=o.prefix_sep .. ratio_graph})
|
||||
append_property(s, "vsync-jitter", {prefix="VSync Jitter:", suffix=o.prefix_sep .. jitter_graph})
|
||||
else
|
||||
-- Since no graph is needed we can print ratio/jitter on the same line and save some space
|
||||
local vratio = append_property(s, "vsync-ratio", {prefix="VSync Ratio:"})
|
||||
append_property(s, "vsync-jitter", {prefix="VSync Jitter:", nl="" or o.nl})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function append_filters(s, prop, prefix)
|
||||
local length = 0
|
||||
local filters = {}
|
||||
|
||||
for _,f in ipairs(mp.get_property_native(prop, {})) do
|
||||
local n = f.name
|
||||
if f.enabled ~= nil and not f.enabled then
|
||||
n = n .. " (disabled)"
|
||||
end
|
||||
|
||||
local p = {}
|
||||
for key,value in pairs(f.params) do
|
||||
p[#p+1] = key .. "=" .. value
|
||||
end
|
||||
if #p > 0 then
|
||||
p = " [" .. table.concat(p, " ") .. "]"
|
||||
else
|
||||
p = ""
|
||||
end
|
||||
|
||||
length = length + n:len() + p:len()
|
||||
filters[#filters+1] = no_ASS(n) .. it(no_ASS(p))
|
||||
end
|
||||
|
||||
if #filters > 0 then
|
||||
local ret
|
||||
if length < o.filter_params_max_length then
|
||||
ret = table.concat(filters, ", ")
|
||||
else
|
||||
local sep = o.nl .. o.indent .. o.indent
|
||||
ret = sep .. table.concat(filters, sep)
|
||||
end
|
||||
s[#s+1] = o.nl .. o.indent .. b(prefix) .. o.prefix_sep .. ret
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function add_header(s)
|
||||
s[#s+1] = text_style()
|
||||
end
|
||||
|
||||
|
||||
local function add_file(s)
|
||||
append(s, "", {prefix="File:", nl="", indent=""})
|
||||
append_property(s, "filename", {prefix_sep="", nl="", indent=""})
|
||||
if not (mp.get_property_osd("filename") == mp.get_property_osd("media-title")) then
|
||||
append_property(s, "media-title", {prefix="Title:"})
|
||||
end
|
||||
|
||||
local ch_index = mp.get_property_number("chapter")
|
||||
if ch_index and ch_index >= 0 then
|
||||
append_property(s, "chapter-list/" .. tostring(ch_index) .. "/title", {prefix="Chapter:"})
|
||||
append_property(s, "chapter-list/count",
|
||||
{prefix="(" .. tostring(ch_index + 1) .. "/", suffix=")", nl="",
|
||||
indent=" ", prefix_sep=" ", no_prefix_markup=true})
|
||||
end
|
||||
|
||||
local demuxer_cache = mp.get_property_native("demuxer-cache-state", {})
|
||||
if demuxer_cache["fw-bytes"] then
|
||||
demuxer_cache = demuxer_cache["fw-bytes"] -- returns bytes
|
||||
else
|
||||
demuxer_cache = 0
|
||||
end
|
||||
local demuxer_secs = mp.get_property_number("demuxer-cache-duration", 0)
|
||||
local stream_cache = mp.get_property_number("cache-used", 0) * 1024 -- returns KiB
|
||||
if stream_cache + demuxer_cache + demuxer_secs > 0 then
|
||||
append(s, utils.format_bytes_humanized(stream_cache + demuxer_cache), {prefix="Total Cache:"})
|
||||
append(s, utils.format_bytes_humanized(demuxer_cache), {prefix="(Demuxer:",
|
||||
suffix=",", nl="", no_prefix_markup=true, indent=o.prefix_sep})
|
||||
append(s, format("%.1f", demuxer_secs), {suffix=" sec)", nl="", indent="",
|
||||
no_prefix_markup=true})
|
||||
local speed = mp.get_property_number("cache-speed", 0)
|
||||
if speed > 0 then
|
||||
append(s, utils.format_bytes_humanized(speed) .. "/s", {prefix="Speed:", nl="",
|
||||
indent=o.prefix_sep, no_prefix_markup=true})
|
||||
end
|
||||
end
|
||||
append_property(s, "file-size", {prefix="Size:"})
|
||||
end
|
||||
|
||||
|
||||
local function add_video(s)
|
||||
local r = mp.get_property_native("video-params")
|
||||
-- in case of e.g. lavi-complex there can be no input video, only output
|
||||
if not r then
|
||||
r = mp.get_property_native("video-out-params")
|
||||
end
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
append(s, "", {prefix=o.nl .. o.nl .. "Video:", nl="", indent=""})
|
||||
if append_property(s, "video-codec", {prefix_sep="", nl="", indent=""}) then
|
||||
append_property(s, "hwdec-current", {prefix="(hwdec:", nl="", indent=" ",
|
||||
no_prefix_markup=true, suffix=")"}, {no=true, [""]=true})
|
||||
end
|
||||
append_property(s, "avsync", {prefix="A-V:"})
|
||||
if append_property(s, compat("decoder-frame-drop-count"),
|
||||
{prefix="Dropped Frames:", suffix=" (decoder)"}) then
|
||||
append_property(s, compat("frame-drop-count"), {suffix=" (output)", nl="", indent=""})
|
||||
end
|
||||
if append_property(s, "display-fps", {prefix="Display FPS:", suffix=" (specified)"}) then
|
||||
append_property(s, "estimated-display-fps",
|
||||
{suffix=" (estimated)", nl="", indent=""})
|
||||
else
|
||||
append_property(s, "estimated-display-fps",
|
||||
{prefix="Display FPS:", suffix=" (estimated)"})
|
||||
end
|
||||
if append_property(s, compat("container-fps"), {prefix="FPS:", suffix=" (specified)"}) then
|
||||
append_property(s, "estimated-vf-fps",
|
||||
{suffix=" (estimated)", nl="", indent=""})
|
||||
else
|
||||
append_property(s, "estimated-vf-fps",
|
||||
{prefix="FPS:", suffix=" (estimated)"})
|
||||
end
|
||||
|
||||
append_display_sync(s)
|
||||
append_perfdata(s, o.print_perfdata_passes)
|
||||
|
||||
if append(s, r["w"], {prefix="Native Resolution:"}) then
|
||||
append(s, r["h"], {prefix="x", nl="", indent=" ", prefix_sep=" ", no_prefix_markup=true})
|
||||
end
|
||||
append_property(s, "window-scale", {prefix="Window Scale:"})
|
||||
append(s, format("%.2f", r["aspect"]), {prefix="Aspect Ratio:"})
|
||||
append(s, r["pixelformat"], {prefix="Pixel Format:"})
|
||||
|
||||
-- Group these together to save vertical space
|
||||
local prim = append(s, r["primaries"], {prefix="Primaries:"})
|
||||
local cmat = append(s, r["colormatrix"], {prefix="Colormatrix:", nl=prim and "" or o.nl})
|
||||
append(s, r["colorlevels"], {prefix="Levels:", nl=cmat and "" or o.nl})
|
||||
|
||||
-- Append HDR metadata conditionally (only when present and interesting)
|
||||
local hdrpeak = r["sig-peak"] or 0
|
||||
local hdrinfo = ""
|
||||
if hdrpeak > 1 then
|
||||
hdrinfo = " (HDR peak: " .. format("%.2f", hdrpeak) .. ")"
|
||||
end
|
||||
|
||||
append(s, r["gamma"], {prefix="Gamma:", suffix=hdrinfo})
|
||||
append_property(s, "packet-video-bitrate", {prefix="Bitrate:", suffix=" kbps"})
|
||||
append_filters(s, "vf", "Filters:")
|
||||
end
|
||||
|
||||
|
||||
local function add_audio(s)
|
||||
local r = mp.get_property_native("audio-params")
|
||||
-- in case of e.g. lavi-complex there can be no input audio, only output
|
||||
if not r then
|
||||
r = mp.get_property_native("audio-out-params")
|
||||
end
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
append(s, "", {prefix=o.nl .. o.nl .. "Audio:", nl="", indent=""})
|
||||
append_property(s, "audio-codec", {prefix_sep="", nl="", indent=""})
|
||||
append(s, r["format"], {prefix="Format:"})
|
||||
append(s, r["samplerate"], {prefix="Sample Rate:", suffix=" Hz"})
|
||||
append(s, r["channel-count"], {prefix="Channels:"})
|
||||
append_property(s, "packet-audio-bitrate", {prefix="Bitrate:", suffix=" kbps"})
|
||||
append_filters(s, "af", "Filters:")
|
||||
end
|
||||
|
||||
|
||||
-- Determine whether ASS formatting shall/can be used and set formatting sequences
|
||||
local function eval_ass_formatting()
|
||||
o.use_ass = o.ass_formatting and has_vo_window()
|
||||
if o.use_ass then
|
||||
o.nl = o.ass_nl
|
||||
o.indent = o.ass_indent
|
||||
o.prefix_sep = o.ass_prefix_sep
|
||||
o.b1 = o.ass_b1
|
||||
o.b0 = o.ass_b0
|
||||
o.it1 = o.ass_it1
|
||||
o.it0 = o.ass_it0
|
||||
else
|
||||
o.nl = o.no_ass_nl
|
||||
o.indent = o.no_ass_indent
|
||||
o.prefix_sep = o.no_ass_prefix_sep
|
||||
if not has_ansi() then
|
||||
o.b1 = ""
|
||||
o.b0 = ""
|
||||
o.it1 = ""
|
||||
o.it0 = ""
|
||||
else
|
||||
o.b1 = o.no_ass_b1
|
||||
o.b0 = o.no_ass_b0
|
||||
o.it1 = o.no_ass_it1
|
||||
o.it0 = o.no_ass_it0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Returns an ASS string with "normal" stats
|
||||
local function default_stats()
|
||||
local stats = {}
|
||||
eval_ass_formatting()
|
||||
add_header(stats)
|
||||
add_file(stats)
|
||||
add_video(stats)
|
||||
add_audio(stats)
|
||||
return table.concat(stats)
|
||||
end
|
||||
|
||||
|
||||
-- Returns an ASS string with extended VO stats
|
||||
local function vo_stats()
|
||||
local stats = {}
|
||||
eval_ass_formatting()
|
||||
add_header(stats)
|
||||
append_perfdata(stats, true)
|
||||
return table.concat(stats)
|
||||
end
|
||||
|
||||
|
||||
-- Returns an ASS string with stats about filters/profiles/shaders
|
||||
local function filter_stats()
|
||||
return "coming soon"
|
||||
end
|
||||
|
||||
|
||||
-- Current page and <page key>:<page function> mapping
|
||||
curr_page = o.key_page_1
|
||||
pages = {
|
||||
[o.key_page_1] = { f = default_stats, desc = "Default" },
|
||||
[o.key_page_2] = { f = vo_stats, desc = "Extended Frame Timings" },
|
||||
--[o.key_page_3] = { f = filter_stats, desc = "Dummy" },
|
||||
}
|
||||
|
||||
|
||||
-- Returns a function to record vsratio/jitter with the specified `skip` value
|
||||
local function record_data(skip)
|
||||
init_buffers()
|
||||
skip = max(skip, 0)
|
||||
local i = skip
|
||||
return function()
|
||||
if i < skip then
|
||||
i = i + 1
|
||||
return
|
||||
else
|
||||
i = 0
|
||||
end
|
||||
|
||||
if o.plot_vsync_jitter then
|
||||
local r = mp.get_property_number("vsync-jitter", nil)
|
||||
if r then
|
||||
vsjitter_buf.pos = (vsjitter_buf.pos % vsjitter_buf.len) + 1
|
||||
vsjitter_buf[vsjitter_buf.pos] = r
|
||||
vsjitter_buf.max = max(vsjitter_buf.max, r)
|
||||
end
|
||||
end
|
||||
|
||||
if o.plot_vsync_ratio then
|
||||
local r = mp.get_property_number("vsync-ratio", nil)
|
||||
if r then
|
||||
vsratio_buf.pos = (vsratio_buf.pos % vsratio_buf.len) + 1
|
||||
vsratio_buf[vsratio_buf.pos] = r
|
||||
vsratio_buf.max = max(vsratio_buf.max, r)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Call the function for `page` and print it to OSD
|
||||
local function print_page(page)
|
||||
if o.persistent_overlay then
|
||||
mp.set_osd_ass(0, 0, pages[page].f())
|
||||
else
|
||||
mp.osd_message(pages[page].f(), display_timer.oneshot and o.duration or o.redraw_delay + 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function clear_screen()
|
||||
if o.persistent_overlay then mp.set_osd_ass(0, 0, "") else mp.osd_message("", 0) end
|
||||
end
|
||||
|
||||
|
||||
-- Add keybindings for every page
|
||||
local function add_page_bindings()
|
||||
local function a(k)
|
||||
return function()
|
||||
curr_page = k
|
||||
print_page(k)
|
||||
if display_timer.oneshot then display_timer:kill() ; display_timer:resume() end
|
||||
end
|
||||
end
|
||||
for k, _ in pairs(pages) do
|
||||
mp.add_forced_key_binding(k, k, a(k), {repeatable=true})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Remove keybindings for every page
|
||||
local function remove_page_bindings()
|
||||
for k, _ in pairs(pages) do
|
||||
mp.remove_key_binding(k)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function process_key_binding(oneshot)
|
||||
-- Stats are already being displayed
|
||||
if display_timer:is_enabled() then
|
||||
-- Previous and current keys were oneshot -> restart timer
|
||||
if display_timer.oneshot and oneshot then
|
||||
display_timer:kill()
|
||||
print_page(curr_page)
|
||||
display_timer:resume()
|
||||
-- Previous and current keys were toggling -> end toggling
|
||||
elseif not display_timer.oneshot and not oneshot then
|
||||
display_timer:kill()
|
||||
clear_screen()
|
||||
remove_page_bindings()
|
||||
if recorder then
|
||||
mp.unregister_event(recorder)
|
||||
recorder = nil
|
||||
end
|
||||
end
|
||||
-- No stats are being displayed yet
|
||||
else
|
||||
if not oneshot and (o.plot_vsync_jitter or o.plot_vsync_ratio) then
|
||||
recorder = record_data(o.skip_frames)
|
||||
mp.register_event("tick", recorder)
|
||||
end
|
||||
display_timer:kill()
|
||||
display_timer.oneshot = oneshot
|
||||
display_timer.timeout = oneshot and o.duration or o.redraw_delay
|
||||
add_page_bindings()
|
||||
print_page(curr_page)
|
||||
display_timer:resume()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Create the timer used for redrawing (toggling) or clearing the screen (oneshot)
|
||||
-- The duration here is not important and always set in process_key_binding()
|
||||
display_timer = mp.add_periodic_timer(o.duration,
|
||||
function()
|
||||
if display_timer.oneshot then
|
||||
display_timer:kill() ; clear_screen() ; remove_page_bindings()
|
||||
else
|
||||
print_page(curr_page)
|
||||
end
|
||||
end)
|
||||
display_timer:kill()
|
||||
|
||||
-- Single invocation key binding
|
||||
mp.add_key_binding(o.key_oneshot, "display-stats", function() process_key_binding(true) end,
|
||||
{repeatable=true})
|
||||
|
||||
-- Toggling key binding
|
||||
mp.add_key_binding(o.key_toggle, "display-stats-toggle", function() process_key_binding(false) end,
|
||||
{repeatable=false})
|
||||
|
||||
-- Single invocation bindings without key, can be used in input.conf to create
|
||||
-- bindings for a specific page: "e script-binding stats/display-page-2"
|
||||
for k, _ in pairs(pages) do
|
||||
mp.add_key_binding(nil, "display-page-" .. k, function() process_key_binding(true) end,
|
||||
{repeatable=true})
|
||||
end
|
||||
|
||||
-- Reprint stats immediately when VO was reconfigured, only when toggled
|
||||
mp.register_event("video-reconfig",
|
||||
function()
|
||||
if display_timer:is_enabled() then
|
||||
print_page(curr_page)
|
||||
end
|
||||
end)
|
||||
92
scripts/status-line.lua
Executable file
92
scripts/status-line.lua
Executable file
@@ -0,0 +1,92 @@
|
||||
-- Rebuild the terminal status line as a lua script
|
||||
-- Be aware that this will require more cpu power!
|
||||
-- Also, this is based on a rather old version of the
|
||||
-- builtin mpv status line.
|
||||
|
||||
-- Add a string to the status line
|
||||
function atsl(s)
|
||||
newStatus = newStatus .. s
|
||||
end
|
||||
|
||||
function update_status_line()
|
||||
-- Reset the status line
|
||||
newStatus = ""
|
||||
|
||||
if mp.get_property_bool("pause") then
|
||||
atsl("(Paused) ")
|
||||
elseif mp.get_property_bool("paused-for-cache") then
|
||||
atsl("(Buffering) ")
|
||||
end
|
||||
|
||||
if mp.get_property("aid") ~= "no" then
|
||||
atsl("A")
|
||||
end
|
||||
if mp.get_property("vid") ~= "no" then
|
||||
atsl("V")
|
||||
end
|
||||
|
||||
atsl(": ")
|
||||
|
||||
atsl(mp.get_property_osd("time-pos"))
|
||||
|
||||
atsl(" / ");
|
||||
atsl(mp.get_property_osd("duration"));
|
||||
|
||||
atsl(" (")
|
||||
atsl(mp.get_property_osd("percent-pos", -1))
|
||||
atsl("%)")
|
||||
|
||||
local r = mp.get_property_number("speed", -1)
|
||||
if r ~= 1 then
|
||||
atsl(string.format(" x%4.2f", r))
|
||||
end
|
||||
|
||||
r = mp.get_property_number("avsync", nil)
|
||||
if r ~= nil then
|
||||
atsl(string.format(" A-V: %f", r))
|
||||
end
|
||||
|
||||
r = mp.get_property("total-avsync-change", 0)
|
||||
if math.abs(r) > 0.05 then
|
||||
atsl(string.format(" ct:%7.3f", r))
|
||||
end
|
||||
|
||||
r = mp.get_property_number("decoder-drop-frame-count", -1)
|
||||
if r > 0 then
|
||||
atsl(" Late: ")
|
||||
atsl(r)
|
||||
end
|
||||
|
||||
r = mp.get_property_osd("video-bitrate")
|
||||
if r ~= nil and r ~= "" then
|
||||
atsl(" Vb: ")
|
||||
atsl(r)
|
||||
end
|
||||
|
||||
r = mp.get_property_osd("audio-bitrate")
|
||||
if r ~= nil and r ~= "" then
|
||||
atsl(" Ab: ")
|
||||
atsl(r)
|
||||
end
|
||||
|
||||
r = mp.get_property_number("cache", 0)
|
||||
if r > 0 then
|
||||
atsl(string.format(" Cache: %d%% ", r))
|
||||
end
|
||||
|
||||
-- Set the new status line
|
||||
mp.set_property("options/term-status-msg", newStatus)
|
||||
end
|
||||
|
||||
timer = mp.add_periodic_timer(1, update_status_line)
|
||||
|
||||
function on_pause_change(name, value)
|
||||
if value == false then
|
||||
timer:resume()
|
||||
else
|
||||
timer:stop()
|
||||
end
|
||||
mp.add_timeout(0.1, update_status_line)
|
||||
end
|
||||
mp.observe_property("pause", "bool", on_pause_change)
|
||||
mp.register_event("seek", update_status_line)
|
||||
921
scripts/thumbfast.lua
Normal file
921
scripts/thumbfast.lua
Normal file
@@ -0,0 +1,921 @@
|
||||
-- thumbfast.lua
|
||||
--
|
||||
-- High-performance on-the-fly thumbnailer
|
||||
--
|
||||
-- Built for easy integration in third-party UIs.
|
||||
|
||||
local options = {
|
||||
-- Socket path (leave empty for auto)
|
||||
socket = "",
|
||||
|
||||
-- Thumbnail path (leave empty for auto)
|
||||
thumbnail = "",
|
||||
|
||||
-- Maximum thumbnail size in pixels (scaled down to fit)
|
||||
-- Values are scaled when hidpi is enabled
|
||||
max_height = 200,
|
||||
max_width = 200,
|
||||
|
||||
-- Apply tone-mapping, no to disable
|
||||
tone_mapping = "auto",
|
||||
|
||||
-- Overlay id
|
||||
overlay_id = 42,
|
||||
|
||||
-- Spawn thumbnailer on file load for faster initial thumbnails
|
||||
spawn_first = false,
|
||||
|
||||
-- Close thumbnailer process after an inactivity period in seconds, 0 to disable
|
||||
quit_after_inactivity = 0,
|
||||
|
||||
-- Enable on network playback
|
||||
network = false,
|
||||
|
||||
-- Enable on audio playback
|
||||
audio = false,
|
||||
|
||||
-- Enable hardware decoding
|
||||
hwdec = false,
|
||||
|
||||
-- Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
||||
direct_io = false,
|
||||
|
||||
-- Custom path to the mpv executable
|
||||
mpv_path = "mpv"
|
||||
}
|
||||
|
||||
mp.utils = require "mp.utils"
|
||||
mp.options = require "mp.options"
|
||||
mp.options.read_options(options, "thumbfast")
|
||||
|
||||
local properties = {}
|
||||
local pre_0_30_0 = mp.command_native_async == nil
|
||||
local pre_0_33_0 = true
|
||||
|
||||
function subprocess(args, async, callback)
|
||||
callback = callback or function() end
|
||||
|
||||
if not pre_0_30_0 then
|
||||
if async then
|
||||
return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback)
|
||||
else
|
||||
return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args})
|
||||
end
|
||||
else
|
||||
if async then
|
||||
return mp.utils.subprocess_detached({args = args}, callback)
|
||||
else
|
||||
return mp.utils.subprocess({args = args})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local winapi = {}
|
||||
if options.direct_io then
|
||||
local ffi_loaded, ffi = pcall(require, "ffi")
|
||||
if ffi_loaded then
|
||||
winapi = {
|
||||
ffi = ffi,
|
||||
C = ffi.C,
|
||||
bit = require("bit"),
|
||||
socket_wc = "",
|
||||
|
||||
-- WinAPI constants
|
||||
CP_UTF8 = 65001,
|
||||
GENERIC_WRITE = 0x40000000,
|
||||
OPEN_EXISTING = 3,
|
||||
FILE_FLAG_WRITE_THROUGH = 0x80000000,
|
||||
FILE_FLAG_NO_BUFFERING = 0x20000000,
|
||||
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
|
||||
|
||||
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
|
||||
|
||||
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
|
||||
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
|
||||
}
|
||||
-- cache flags used in run() to avoid bor() call
|
||||
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
|
||||
|
||||
ffi.cdef[[
|
||||
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
|
||||
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
|
||||
bool __stdcall CloseHandle(void *hObject);
|
||||
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
|
||||
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||
]]
|
||||
|
||||
winapi.MultiByteToWideChar = function(MultiByteStr)
|
||||
if MultiByteStr then
|
||||
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
|
||||
if utf16_len > 0 then
|
||||
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
|
||||
return utf16_str
|
||||
end
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
else
|
||||
options.direct_io = false
|
||||
end
|
||||
end
|
||||
|
||||
local file = nil
|
||||
local file_bytes = 0
|
||||
local spawned = false
|
||||
local disabled = false
|
||||
local force_disabled = false
|
||||
local spawn_waiting = false
|
||||
local spawn_working = false
|
||||
local script_written = false
|
||||
|
||||
local dirty = false
|
||||
|
||||
local x = nil
|
||||
local y = nil
|
||||
local last_x = x
|
||||
local last_y = y
|
||||
|
||||
local last_seek_time = nil
|
||||
|
||||
local effective_w = options.max_width
|
||||
local effective_h = options.max_height
|
||||
local real_w = nil
|
||||
local real_h = nil
|
||||
local last_real_w = nil
|
||||
local last_real_h = nil
|
||||
|
||||
local script_name = nil
|
||||
|
||||
local show_thumbnail = false
|
||||
|
||||
local filters_reset = {["lavfi-crop"]=true, ["crop"]=true}
|
||||
local filters_runtime = {["hflip"]=true, ["vflip"]=true}
|
||||
local filters_all = {["hflip"]=true, ["vflip"]=true, ["lavfi-crop"]=true, ["crop"]=true}
|
||||
|
||||
local tone_mappings = {["none"]=true, ["clip"]=true, ["linear"]=true, ["gamma"]=true, ["reinhard"]=true, ["hable"]=true, ["mobius"]=true}
|
||||
local last_tone_mapping = nil
|
||||
|
||||
local last_vf_reset = ""
|
||||
local last_vf_runtime = ""
|
||||
|
||||
local last_rotate = 0
|
||||
|
||||
local par = ""
|
||||
local last_par = ""
|
||||
|
||||
local last_has_vid = 0
|
||||
local has_vid = 0
|
||||
|
||||
local file_timer = nil
|
||||
local file_check_period = 1/60
|
||||
|
||||
local allow_fast_seek = true
|
||||
|
||||
local client_script = [=[
|
||||
#!/usr/bin/env bash
|
||||
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
|
||||
trap "kill 0" EXIT
|
||||
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
|
||||
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
|
||||
]=]
|
||||
|
||||
local function get_os()
|
||||
local raw_os_name = ""
|
||||
|
||||
if jit and jit.os and jit.arch then
|
||||
raw_os_name = jit.os
|
||||
else
|
||||
if package.config:sub(1,1) == "\\" then
|
||||
-- Windows
|
||||
local env_OS = os.getenv("OS")
|
||||
if env_OS then
|
||||
raw_os_name = env_OS
|
||||
end
|
||||
else
|
||||
raw_os_name = subprocess({"uname", "-s"}).stdout
|
||||
end
|
||||
end
|
||||
|
||||
raw_os_name = (raw_os_name):lower()
|
||||
|
||||
local os_patterns = {
|
||||
["windows"] = "windows",
|
||||
["linux"] = "linux",
|
||||
|
||||
["osx"] = "darwin",
|
||||
["mac"] = "darwin",
|
||||
["darwin"] = "darwin",
|
||||
|
||||
["^mingw"] = "windows",
|
||||
["^cygwin"] = "windows",
|
||||
|
||||
["bsd$"] = "darwin",
|
||||
["sunos"] = "darwin"
|
||||
}
|
||||
|
||||
-- Default to linux
|
||||
local str_os_name = "linux"
|
||||
|
||||
for pattern, name in pairs(os_patterns) do
|
||||
if raw_os_name:match(pattern) then
|
||||
str_os_name = name
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return str_os_name
|
||||
end
|
||||
|
||||
local os_name = mp.get_property("platform") or get_os()
|
||||
|
||||
local path_separator = os_name == "windows" and "\\" or "/"
|
||||
|
||||
if options.socket == "" then
|
||||
if os_name == "windows" then
|
||||
options.socket = "thumbfast"
|
||||
else
|
||||
options.socket = "/tmp/thumbfast"
|
||||
end
|
||||
end
|
||||
|
||||
if options.thumbnail == "" then
|
||||
if os_name == "windows" then
|
||||
options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
|
||||
else
|
||||
options.thumbnail = "/tmp/thumbfast.out"
|
||||
end
|
||||
end
|
||||
|
||||
local unique = mp.utils.getpid()
|
||||
|
||||
options.socket = options.socket .. unique
|
||||
options.thumbnail = options.thumbnail .. unique
|
||||
|
||||
if options.direct_io then
|
||||
if os_name == "windows" then
|
||||
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
|
||||
end
|
||||
|
||||
if winapi.socket_wc == "" then
|
||||
options.direct_io = false
|
||||
end
|
||||
end
|
||||
|
||||
local mpv_path = options.mpv_path
|
||||
|
||||
if mpv_path == "mpv" and os_name == "darwin" and unique then
|
||||
-- TODO: look into ~~osxbundle/
|
||||
mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
|
||||
if mpv_path ~= "mpv" then
|
||||
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
||||
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
|
||||
if mpv_bin and mpv_bin.is_file then
|
||||
mpv_path = "/usr/local/mpv"
|
||||
else
|
||||
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
|
||||
if mpv_app and mpv_app.is_file then
|
||||
mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||
else
|
||||
mp.msg.warn("drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function vo_tone_mapping()
|
||||
local passes = mp.get_property_native("vo-passes")
|
||||
if passes and passes["fresh"] then
|
||||
for k, v in pairs(passes["fresh"]) do
|
||||
for k2, v2 in pairs(v) do
|
||||
if k2 == "desc" and v2 then
|
||||
local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
|
||||
if tone_mapping then
|
||||
return tone_mapping
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function vf_string(filters, full)
|
||||
local vf = ""
|
||||
local vf_table = properties["vf"]
|
||||
|
||||
if vf_table and #vf_table > 0 then
|
||||
for i = #vf_table, 1, -1 do
|
||||
if filters[vf_table[i].name] then
|
||||
local args = ""
|
||||
for key, value in pairs(vf_table[i].params) do
|
||||
if args ~= "" then
|
||||
args = args .. ":"
|
||||
end
|
||||
args = args .. key .. "=" .. value
|
||||
end
|
||||
vf = vf .. vf_table[i].name .. "=" .. args .. ","
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
|
||||
if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
|
||||
local tone_mapping = options.tone_mapping
|
||||
if tone_mapping == "auto" then
|
||||
tone_mapping = last_tone_mapping or properties["tone-mapping"]
|
||||
if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
|
||||
tone_mapping = vo_tone_mapping()
|
||||
end
|
||||
end
|
||||
if not tone_mappings[tone_mapping] then
|
||||
tone_mapping = "hable"
|
||||
end
|
||||
last_tone_mapping = tone_mapping
|
||||
vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap="..tone_mapping..",zscale=transfer=bt709,"
|
||||
end
|
||||
end
|
||||
|
||||
if full then
|
||||
vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra"
|
||||
end
|
||||
|
||||
return vf
|
||||
end
|
||||
|
||||
local function calc_dimensions()
|
||||
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
|
||||
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
|
||||
if not width or not height then return end
|
||||
|
||||
local scale = properties["display-hidpi-scale"] or 1
|
||||
|
||||
if width / height > options.max_width / options.max_height then
|
||||
effective_w = math.floor(options.max_width * scale + 0.5)
|
||||
effective_h = math.floor(height / width * effective_w + 0.5)
|
||||
else
|
||||
effective_h = math.floor(options.max_height * scale + 0.5)
|
||||
effective_w = math.floor(width / height * effective_h + 0.5)
|
||||
end
|
||||
|
||||
local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
|
||||
if v_par == 1 then
|
||||
par = ":force_original_aspect_ratio=decrease"
|
||||
else
|
||||
par = ""
|
||||
end
|
||||
end
|
||||
|
||||
local info_timer = nil
|
||||
|
||||
local function info(w, h)
|
||||
local rotate = properties["video-params"] and properties["video-params"]["rotate"]
|
||||
local image = properties["current-tracks"] and properties["current-tracks"]["video"] and properties["current-tracks"]["video"]["image"]
|
||||
local albumart = image and properties["current-tracks"]["video"]["albumart"]
|
||||
|
||||
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
||||
has_vid == 0 or
|
||||
(properties["demuxer-via-network"] and not options.network) or
|
||||
(albumart and not options.audio) or
|
||||
(image and not albumart) or
|
||||
force_disabled
|
||||
|
||||
if info_timer then
|
||||
info_timer:kill()
|
||||
info_timer = nil
|
||||
elseif has_vid == 0 or (rotate == nil and not disabled) then
|
||||
info_timer = mp.add_timeout(0.05, function() info(w, h) end)
|
||||
end
|
||||
|
||||
local json, err = mp.utils.format_json({width=w, height=h, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||
if pre_0_30_0 then
|
||||
mp.command_native({"script-message", "thumbfast-info", json})
|
||||
else
|
||||
mp.command_native_async({"script-message", "thumbfast-info", json}, function() end)
|
||||
end
|
||||
end
|
||||
|
||||
local function remove_thumbnail_files()
|
||||
if file then
|
||||
file:close()
|
||||
file = nil
|
||||
file_bytes = 0
|
||||
end
|
||||
os.remove(options.thumbnail)
|
||||
os.remove(options.thumbnail..".bgra")
|
||||
end
|
||||
|
||||
local activity_timer
|
||||
|
||||
local function spawn(time)
|
||||
if disabled then return end
|
||||
|
||||
local path = properties["path"]
|
||||
if path == nil then return end
|
||||
|
||||
if options.quit_after_inactivity > 0 then
|
||||
if show_thumbnail or activity_timer:is_enabled() then
|
||||
activity_timer:kill()
|
||||
end
|
||||
activity_timer:resume()
|
||||
end
|
||||
|
||||
local open_filename = properties["stream-open-filename"]
|
||||
local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
|
||||
if ytdl then
|
||||
path = open_filename
|
||||
end
|
||||
|
||||
remove_thumbnail_files()
|
||||
|
||||
local vid = properties["vid"]
|
||||
has_vid = vid or 0
|
||||
|
||||
local args = {
|
||||
mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
|
||||
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", "--load-auto-profiles=no",
|
||||
"--edition="..(properties["edition"] or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio",
|
||||
"--start="..time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
|
||||
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
||||
"--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", "--hwdec="..(options.hwdec and "auto" or "no"),
|
||||
"--vf="..vf_string(filters_all, true),
|
||||
"--sws-scaler=fast-bilinear",
|
||||
"--video-rotate="..last_rotate,
|
||||
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail
|
||||
}
|
||||
|
||||
if not pre_0_30_0 then
|
||||
table.insert(args, "--sws-allow-zimg=no")
|
||||
end
|
||||
|
||||
if os_name == "darwin" and properties["macos-app-activation-policy"] then
|
||||
table.insert(args, "--macos-app-activation-policy=accessory")
|
||||
end
|
||||
|
||||
if os_name == "windows" or pre_0_33_0 then
|
||||
table.insert(args, "--input-ipc-server="..options.socket)
|
||||
elseif not script_written then
|
||||
local client_script_path = options.socket..".run"
|
||||
local script = io.open(client_script_path, "w+")
|
||||
if script == nil then
|
||||
mp.msg.error("client script write failed")
|
||||
return
|
||||
else
|
||||
script_written = true
|
||||
script:write(string.format(client_script, options.socket))
|
||||
script:close()
|
||||
subprocess({"chmod", "+x", client_script_path}, true)
|
||||
table.insert(args, "--scripts="..client_script_path)
|
||||
end
|
||||
else
|
||||
local client_script_path = options.socket..".run"
|
||||
table.insert(args, "--scripts="..client_script_path)
|
||||
end
|
||||
|
||||
table.insert(args, "--")
|
||||
table.insert(args, path)
|
||||
|
||||
spawned = true
|
||||
spawn_waiting = true
|
||||
|
||||
subprocess(args, true,
|
||||
function(success, result)
|
||||
if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
|
||||
spawned = false
|
||||
spawn_waiting = false
|
||||
options.tone_mapping = "no"
|
||||
mp.msg.error("mpv subprocess create failed")
|
||||
if not spawn_working then -- notify users of required configuration
|
||||
if options.mpv_path == "mpv" then
|
||||
if properties["current-vo"] == "libmpv" then
|
||||
if options.mpv_path == mpv_path then -- attempt to locate ImPlay
|
||||
mpv_path = "ImPlay"
|
||||
spawn(time)
|
||||
else -- ImPlay not in path
|
||||
if os_name ~= "darwin" then
|
||||
force_disabled = true
|
||||
info(real_w or effective_w, real_h or effective_h)
|
||||
end
|
||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||
end
|
||||
else
|
||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||
if os_name == "windows" then
|
||||
mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||
mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||
end
|
||||
end
|
||||
else
|
||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||
-- found ImPlay but not defined in config
|
||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||
end
|
||||
end
|
||||
elseif success == true and (result.status == 0 or result.status == -2) then
|
||||
if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
|
||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||
end
|
||||
spawn_working = true
|
||||
spawn_waiting = false
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local function run(command)
|
||||
if not spawned then return end
|
||||
|
||||
if options.direct_io then
|
||||
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil)
|
||||
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
|
||||
local buf = command .. "\n"
|
||||
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
|
||||
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
|
||||
winapi.C.CloseHandle(hPipe)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local command_n = command.."\n"
|
||||
|
||||
if os_name == "windows" then
|
||||
if file and file_bytes + #command_n >= 4096 then
|
||||
file:close()
|
||||
file = nil
|
||||
file_bytes = 0
|
||||
end
|
||||
if not file then
|
||||
file = io.open("\\\\.\\pipe\\"..options.socket, "r+b")
|
||||
end
|
||||
elseif pre_0_33_0 then
|
||||
subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket})
|
||||
return
|
||||
elseif not file then
|
||||
file = io.open(options.socket, "r+")
|
||||
end
|
||||
if file then
|
||||
file_bytes = file:seek("end")
|
||||
file:write(command_n)
|
||||
file:flush()
|
||||
end
|
||||
end
|
||||
|
||||
local function draw(w, h, script)
|
||||
if not w or not show_thumbnail then return end
|
||||
if x ~= nil then
|
||||
if pre_0_30_0 then
|
||||
mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)})
|
||||
else
|
||||
mp.command_native_async({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)}, function() end)
|
||||
end
|
||||
elseif script then
|
||||
local json, err = mp.utils.format_json({width=w, height=h, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||
mp.commandv("script-message-to", script, "thumbfast-render", json)
|
||||
end
|
||||
end
|
||||
|
||||
local function real_res(req_w, req_h, filesize)
|
||||
local count = filesize / 4
|
||||
local diff = (req_w * req_h) - count
|
||||
|
||||
if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
|
||||
req_w, req_h = req_h, req_w
|
||||
end
|
||||
|
||||
if diff == 0 then
|
||||
return req_w, req_h
|
||||
else
|
||||
local threshold = 5 -- throw out results that change too much
|
||||
local long_side, short_side = req_w, req_h
|
||||
if req_h > req_w then
|
||||
long_side, short_side = req_h, req_w
|
||||
end
|
||||
for a = short_side, short_side - threshold, -1 do
|
||||
if count % a == 0 then
|
||||
local b = count / a
|
||||
if long_side - b < threshold then
|
||||
if req_h < req_w then return b, a else return a, b end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local function move_file(from, to)
|
||||
if os_name == "windows" then
|
||||
os.remove(to)
|
||||
end
|
||||
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
|
||||
os.rename(from, to)
|
||||
end
|
||||
|
||||
local function seek(fast)
|
||||
if last_seek_time then
|
||||
run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
|
||||
end
|
||||
end
|
||||
|
||||
local seek_period = 3/60
|
||||
local seek_period_counter = 0
|
||||
local seek_timer
|
||||
seek_timer = mp.add_periodic_timer(seek_period, function()
|
||||
if seek_period_counter == 0 then
|
||||
seek(allow_fast_seek)
|
||||
seek_period_counter = 1
|
||||
else
|
||||
if seek_period_counter == 2 then
|
||||
if allow_fast_seek then
|
||||
seek_timer:kill()
|
||||
seek()
|
||||
end
|
||||
else seek_period_counter = seek_period_counter + 1 end
|
||||
end
|
||||
end)
|
||||
seek_timer:kill()
|
||||
|
||||
local function request_seek()
|
||||
if seek_timer:is_enabled() then
|
||||
seek_period_counter = 0
|
||||
else
|
||||
seek_timer:resume()
|
||||
seek(allow_fast_seek)
|
||||
seek_period_counter = 1
|
||||
end
|
||||
end
|
||||
|
||||
local function check_new_thumb()
|
||||
-- the slave might start writing to the file after checking existance and
|
||||
-- validity but before actually moving the file, so move to a temporary
|
||||
-- location before validity check to make sure everything stays consistant
|
||||
-- and valid thumbnails don't get overwritten by invalid ones
|
||||
local tmp = options.thumbnail..".tmp"
|
||||
move_file(options.thumbnail, tmp)
|
||||
local finfo = mp.utils.file_info(tmp)
|
||||
if not finfo then return false end
|
||||
spawn_waiting = false
|
||||
local w, h = real_res(effective_w, effective_h, finfo.size)
|
||||
if w then -- only accept valid thumbnails
|
||||
move_file(tmp, options.thumbnail..".bgra")
|
||||
|
||||
real_w, real_h = w, h
|
||||
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
|
||||
last_real_w, last_real_h = real_w, real_h
|
||||
info(real_w, real_h)
|
||||
end
|
||||
if not show_thumbnail then
|
||||
file_timer:kill()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
file_timer = mp.add_periodic_timer(file_check_period, function()
|
||||
if check_new_thumb() then
|
||||
draw(real_w, real_h, script_name)
|
||||
end
|
||||
end)
|
||||
file_timer:kill()
|
||||
|
||||
local function clear()
|
||||
file_timer:kill()
|
||||
seek_timer:kill()
|
||||
if options.quit_after_inactivity > 0 then
|
||||
if show_thumbnail or activity_timer:is_enabled() then
|
||||
activity_timer:kill()
|
||||
end
|
||||
activity_timer:resume()
|
||||
end
|
||||
last_seek_time = nil
|
||||
show_thumbnail = false
|
||||
last_x = nil
|
||||
last_y = nil
|
||||
if script_name then return end
|
||||
if pre_0_30_0 then
|
||||
mp.command_native({"overlay-remove", options.overlay_id})
|
||||
else
|
||||
mp.command_native_async({"overlay-remove", options.overlay_id}, function() end)
|
||||
end
|
||||
end
|
||||
|
||||
local function quit()
|
||||
activity_timer:kill()
|
||||
if show_thumbnail then
|
||||
activity_timer:resume()
|
||||
return
|
||||
end
|
||||
run("quit")
|
||||
spawned = false
|
||||
real_w, real_h = nil, nil
|
||||
clear()
|
||||
end
|
||||
|
||||
activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
|
||||
activity_timer:kill()
|
||||
|
||||
local function thumb(time, r_x, r_y, script)
|
||||
if disabled then return end
|
||||
|
||||
time = tonumber(time)
|
||||
if time == nil then return end
|
||||
|
||||
if r_x == "" or r_y == "" then
|
||||
x, y = nil, nil
|
||||
else
|
||||
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
|
||||
end
|
||||
|
||||
script_name = script
|
||||
if last_x ~= x or last_y ~= y or not show_thumbnail then
|
||||
show_thumbnail = true
|
||||
last_x = x
|
||||
last_y = y
|
||||
draw(real_w, real_h, script)
|
||||
end
|
||||
|
||||
if options.quit_after_inactivity > 0 then
|
||||
if show_thumbnail or activity_timer:is_enabled() then
|
||||
activity_timer:kill()
|
||||
end
|
||||
activity_timer:resume()
|
||||
end
|
||||
|
||||
if time == last_seek_time then return end
|
||||
last_seek_time = time
|
||||
if not spawned then spawn(time) end
|
||||
request_seek()
|
||||
if not file_timer:is_enabled() then file_timer:resume() end
|
||||
end
|
||||
|
||||
local function watch_changes()
|
||||
if not dirty or not properties["video-out-params"] then return end
|
||||
dirty = false
|
||||
|
||||
local old_w = effective_w
|
||||
local old_h = effective_h
|
||||
|
||||
calc_dimensions()
|
||||
|
||||
local vf_reset = vf_string(filters_reset)
|
||||
local rotate = properties["video-rotate"] or 0
|
||||
|
||||
local resized = old_w ~= effective_w or
|
||||
old_h ~= effective_h or
|
||||
last_vf_reset ~= vf_reset or
|
||||
(last_rotate % 180) ~= (rotate % 180) or
|
||||
par ~= last_par
|
||||
|
||||
if resized then
|
||||
last_rotate = rotate
|
||||
info(effective_w, effective_h)
|
||||
elseif last_has_vid ~= has_vid and has_vid ~= 0 then
|
||||
info(effective_w, effective_h)
|
||||
end
|
||||
|
||||
if spawned then
|
||||
if resized then
|
||||
-- mpv doesn't allow us to change output size
|
||||
local seek_time = last_seek_time
|
||||
run("quit")
|
||||
clear()
|
||||
spawned = false
|
||||
spawn(seek_time or mp.get_property_number("time-pos", 0))
|
||||
file_timer:resume()
|
||||
else
|
||||
if rotate ~= last_rotate then
|
||||
run("set video-rotate "..rotate)
|
||||
end
|
||||
local vf_runtime = vf_string(filters_runtime)
|
||||
if vf_runtime ~= last_vf_runtime then
|
||||
run("vf set "..vf_string(filters_all, true))
|
||||
last_vf_runtime = vf_runtime
|
||||
end
|
||||
end
|
||||
else
|
||||
last_vf_runtime = vf_string(filters_runtime)
|
||||
end
|
||||
|
||||
last_vf_reset = vf_reset
|
||||
last_rotate = rotate
|
||||
last_par = par
|
||||
last_has_vid = has_vid
|
||||
|
||||
if not spawned and not disabled and options.spawn_first and resized then
|
||||
spawn(mp.get_property_number("time-pos", 0))
|
||||
file_timer:resume()
|
||||
end
|
||||
end
|
||||
|
||||
local function update_property(name, value)
|
||||
properties[name] = value
|
||||
end
|
||||
|
||||
local function update_property_dirty(name, value)
|
||||
properties[name] = value
|
||||
dirty = true
|
||||
if name == "tone-mapping" then
|
||||
last_tone_mapping = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function update_tracklist(name, value)
|
||||
-- current-tracks shim
|
||||
for _, track in ipairs(value) do
|
||||
if track.type == "video" and track.selected then
|
||||
properties["current-tracks/video/image"] = track.image
|
||||
properties["current-tracks/video/albumart"] = track.albumart
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function sync_changes(prop, val)
|
||||
update_property(prop, val)
|
||||
if val == nil then return end
|
||||
|
||||
if type(val) == "boolean" then
|
||||
if prop == "vid" then
|
||||
has_vid = 0
|
||||
last_has_vid = 0
|
||||
info(effective_w, effective_h)
|
||||
clear()
|
||||
return
|
||||
end
|
||||
val = val and "yes" or "no"
|
||||
end
|
||||
|
||||
if prop == "vid" then
|
||||
has_vid = 1
|
||||
end
|
||||
|
||||
if not spawned then return end
|
||||
|
||||
run("set "..prop.." "..val)
|
||||
dirty = true
|
||||
end
|
||||
|
||||
local function file_load()
|
||||
clear()
|
||||
spawned = false
|
||||
real_w, real_h = nil, nil
|
||||
last_real_w, last_real_h = nil, nil
|
||||
last_tone_mapping = nil
|
||||
last_seek_time = nil
|
||||
if info_timer then
|
||||
info_timer:kill()
|
||||
info_timer = nil
|
||||
end
|
||||
|
||||
calc_dimensions()
|
||||
info(effective_w, effective_h)
|
||||
end
|
||||
|
||||
local function shutdown()
|
||||
run("quit")
|
||||
remove_thumbnail_files()
|
||||
if os_name ~= "windows" then
|
||||
os.remove(options.socket)
|
||||
os.remove(options.socket..".run")
|
||||
end
|
||||
end
|
||||
|
||||
local function on_duration(prop, val)
|
||||
allow_fast_seek = (val or 30) >= 30
|
||||
end
|
||||
|
||||
mp.observe_property("current-tracks", "native", function(name, value)
|
||||
if pre_0_33_0 then
|
||||
mp.unobserve_property(update_tracklist)
|
||||
pre_0_33_0 = false
|
||||
end
|
||||
update_property(name, value)
|
||||
end)
|
||||
|
||||
mp.observe_property("track-list", "native", update_tracklist)
|
||||
mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
|
||||
mp.observe_property("video-out-params", "native", update_property_dirty)
|
||||
mp.observe_property("video-params", "native", update_property_dirty)
|
||||
mp.observe_property("vf", "native", update_property_dirty)
|
||||
mp.observe_property("tone-mapping", "native", update_property_dirty)
|
||||
mp.observe_property("demuxer-via-network", "native", update_property)
|
||||
mp.observe_property("stream-open-filename", "native", update_property)
|
||||
mp.observe_property("macos-app-activation-policy", "native", update_property)
|
||||
mp.observe_property("current-vo", "native", update_property)
|
||||
mp.observe_property("video-rotate", "native", update_property)
|
||||
mp.observe_property("path", "native", update_property)
|
||||
mp.observe_property("vid", "native", sync_changes)
|
||||
mp.observe_property("edition", "native", sync_changes)
|
||||
mp.observe_property("duration", "native", on_duration)
|
||||
|
||||
mp.register_script_message("thumb", thumb)
|
||||
mp.register_script_message("clear", clear)
|
||||
|
||||
mp.register_event("file-loaded", file_load)
|
||||
mp.register_event("shutdown", shutdown)
|
||||
|
||||
mp.register_idle(watch_changes)
|
||||
1
scripts/youtube-upnext.lua
Symbolic link
1
scripts/youtube-upnext.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../mpv-youtube-upnext/youtube-upnext.lua
|
||||
Reference in New Issue
Block a user