From 0acea11d35aab1417a498d6c2d4406c37716d6af Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 18 Jan 2025 15:37:11 -0800 Subject: [PATCH] update reload and add config file --- script-opts/reload.conf | 34 +++ scripts/reload.lua | 478 ++++++++++++++++++++-------------------- 2 files changed, 272 insertions(+), 240 deletions(-) create mode 100644 script-opts/reload.conf diff --git a/script-opts/reload.conf b/script-opts/reload.conf new file mode 100644 index 0000000..2ba6517 --- /dev/null +++ b/script-opts/reload.conf @@ -0,0 +1,34 @@ +# 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 diff --git a/scripts/reload.lua b/scripts/reload.lua index 8b9e355..7c935bf 100644 --- a/scripts/reload.lua +++ b/scripts/reload.lua @@ -57,21 +57,19 @@ -- `--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", + 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 @@ -97,322 +95,322 @@ local property_keep_open = nil -- | ^ | ^ | ^ -- +---+ +---+ +---+ local demuxer_cache = { - timer = nil, + timer = nil, - state = { - name = 'uninitialized', - demuxer_cache_time = 0, - in_state_time = 0, - }, + 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' }, - }, + 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, - } + 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 + 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 + 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 + 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 + 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 + if demuxer_cache.state.name == event.from then + -- state setup + demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time - -- 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 - 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 + -- 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 - - -- 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 - ) + 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) +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 + -- 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 - 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) + 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, -} +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 + 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 + 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 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 - 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() + paused_for_cache.start_timer(settings.paused_for_cache_timer_interval, + settings.paused_for_cache_timer_timeout) + else + paused_for_cache.reset_timer() 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)) + 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 + msg.debug("reload", path, time_pos) + if time_pos == nil then + mp.commandv("loadfile", path, "replace") + else + local success = mp.commandv("loadfile", path, "replace", -1, + "start=+" .. time_pos) + -- fallback to old syntax of loadfile for compatibility + if success == nil then + msg.warn( + "old loadfile syntax detected. falling back to using old syntax. update mpv to remove this warning") + mp.commandv("loadfile", path, "replace", "start=+" .. time_pos) + end + 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 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 + 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") + 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) + if eof_reached and round(time_pos) == round(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 + -- Check that playback time_pos made progress after the last reload. When + -- eof is reached we try to reload the video, in case there is more content + -- available. If time_pos stayed the same after reload, it means that the + -- video length stayed the same, and we can end the playback. + if round(property_time_pos) == round(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 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)) + 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') + -- 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 +-- Round positive numbers. +function round(num) return math.floor(num + 0.5) end + -- main read_settings() if settings.reload_key_binding ~= "" then - mp.add_key_binding(settings.reload_key_binding, "reload_resume", reload_resume) + 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) + 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) + 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 - ) + -- 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) + mp.observe_property("eof-reached", "bool", reload_eof) end mp.register_event("file-loaded", on_file_loaded) +