dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

reload.lua (13233B)


      1 -- reload.lua
      2 --
      3 -- When an online video is stuck buffering or got very slow CDN
      4 -- source, restarting often helps. This script provides automatic
      5 -- reloading of videos that doesn't have buffering progress for some
      6 -- time while keeping the current time position. It also adds `Ctrl+r`
      7 -- keybinding to reload video manually.
      8 --
      9 -- SETTINGS
     10 --
     11 -- To override default setting put `lua-settings/reload.conf` file in
     12 -- mpv user folder, on linux it is `~/.config/mpv`.  NOTE: config file
     13 -- name should match the name of the script.
     14 --
     15 -- Default `reload.conf` settings:
     16 --
     17 -- ```
     18 -- # enable automatic reload on timeout
     19 -- # when paused-for-cache event fired, we will wait
     20 -- # paused_for_cache_timer_timeout sedonds and then reload the video
     21 -- paused_for_cache_timer_enabled=yes
     22 --
     23 -- # checking paused_for_cache property interval in seconds,
     24 -- # can not be less than 0.05 (50 ms)
     25 -- paused_for_cache_timer_interval=1
     26 --
     27 -- # time in seconds to wait until reload
     28 -- paused_for_cache_timer_timeout=10
     29 --
     30 -- # enable automatic reload based on demuxer cache
     31 -- # if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout
     32 -- # time interval, the video will be reloaded as soon as demuxer cache depleated
     33 -- demuxer_cache_timer_enabled=yes
     34 --
     35 -- # checking demuxer-cache-time property interval in seconds,
     36 -- # can not be less than 0.05 (50 ms)
     37 -- demuxer_cache_timer_interval=2
     38 --
     39 -- # if demuxer cache didn't receive any data during demuxer_cache_timer_timeout
     40 -- # we decide that it has no progress and will reload the stream when
     41 -- # paused_for_cache event happens
     42 -- demuxer_cache_timer_timeout=20
     43 --
     44 -- # when the end-of-file is reached, reload the stream to check
     45 -- # if there is more content available.
     46 -- reload_eof_enabled=no
     47 --
     48 -- # keybinding to reload stream from current time position
     49 -- # you can disable keybinding by setting it to empty value
     50 -- # reload_key_binding=
     51 -- reload_key_binding=Ctrl+r
     52 -- ```
     53 --
     54 -- DEBUGGING
     55 --
     56 -- Debug messages will be printed to stdout with mpv command line option
     57 -- `--msg-level='reload=debug'`
     58 
     59 local msg = require 'mp.msg'
     60 local options = require 'mp.options'
     61 local utils = require 'mp.utils'
     62 
     63 
     64 local settings = {
     65   paused_for_cache_timer_enabled = true,
     66   paused_for_cache_timer_interval = 1,
     67   paused_for_cache_timer_timeout = 10,
     68   demuxer_cache_timer_enabled = true,
     69   demuxer_cache_timer_interval = 2,
     70   demuxer_cache_timer_timeout = 20,
     71   reload_eof_enabled = false,
     72   reload_key_binding = "Ctrl+r",
     73 }
     74 
     75 -- global state stores properties between reloads
     76 local property_path = nil
     77 local property_time_pos = 0
     78 local property_keep_open = nil
     79 
     80 -- FSM managing the demuxer cache.
     81 --
     82 -- States:
     83 --
     84 -- * fetch - fetching new data
     85 -- * stale - unable to fetch new data for time <  'demuxer_cache_timer_timeout'
     86 -- * stuck - unable to fetch new data for time >= 'demuxer_cache_timer_timeout'
     87 --
     88 -- State transitions:
     89 --
     90 --    +---------------------------+
     91 --    v                           |
     92 -- +-------+     +-------+     +-------+
     93 -- + fetch +<--->+ stale +---->+ stuck |
     94 -- +-------+     +-------+     +-------+
     95 --   |   ^         |   ^         |   ^
     96 --   +---+         +---+         +---+
     97 local demuxer_cache = {
     98   timer = nil,
     99 
    100   state = {
    101     name = 'uninitialized',
    102     demuxer_cache_time = 0,
    103     in_state_time = 0,
    104   },
    105 
    106   events = {
    107     continue_fetch = { name = 'continue_fetch', from = 'fetch', to = 'fetch' },
    108     continue_stale = { name = 'continue_stale', from = 'stale', to = 'stale' },
    109     continue_stuck = { name = 'continue_stuck', from = 'stuck', to = 'stuck' },
    110     fetch_to_stale = { name = 'fetch_to_stale', from = 'fetch', to = 'stale' },
    111     stale_to_fetch = { name = 'stale_to_fetch', from = 'stale', to = 'fetch' },
    112     stale_to_stuck = { name = 'stale_to_stuck', from = 'stale', to = 'stuck' },
    113     stuck_to_fetch = { name = 'stuck_to_fetch', from = 'stuck', to = 'fetch' },
    114   },
    115 
    116 }
    117 
    118 -- Always start with 'fetch' state
    119 function demuxer_cache.reset_state()
    120   demuxer_cache.state = {
    121     name = demuxer_cache.events.continue_fetch.to,
    122     demuxer_cache_time = 0,
    123     in_state_time = 0,
    124   }
    125 end
    126 
    127 -- Has 'demuxer_cache_time' changed
    128 function demuxer_cache.has_progress_since(t)
    129   return demuxer_cache.state.demuxer_cache_time ~= t
    130 end
    131 
    132 function demuxer_cache.is_state_fetch()
    133   return demuxer_cache.state.name == demuxer_cache.events.continue_fetch.to
    134 end
    135 
    136 function demuxer_cache.is_state_stale()
    137   return demuxer_cache.state.name == demuxer_cache.events.continue_stale.to
    138 end
    139 
    140 function demuxer_cache.is_state_stuck()
    141   return demuxer_cache.state.name == demuxer_cache.events.continue_stuck.to
    142 end
    143 
    144 function demuxer_cache.transition(event)
    145   if demuxer_cache.state.name == event.from then
    146 
    147     -- state setup
    148     demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time
    149 
    150     if event.name == 'continue_fetch' then
    151       demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
    152     elseif event.name == 'continue_stale' then
    153       demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
    154     elseif event.name == 'continue_stuck' then
    155       demuxer_cache.state.in_state_time = demuxer_cache.state.in_state_time + event.interval
    156     elseif event.name == 'fetch_to_stale' then
    157       demuxer_cache.state.in_state_time = 0
    158     elseif event.name == 'stale_to_fetch' then
    159       demuxer_cache.state.in_state_time = 0
    160     elseif event.name == 'stale_to_stuck' then
    161       demuxer_cache.state.in_state_time = 0
    162     elseif event.name == 'stuck_to_fetch' then
    163       demuxer_cache.state.in_state_time = 0
    164     end
    165 
    166     -- state transition
    167     demuxer_cache.state.name = event.to
    168 
    169     msg.debug('demuxer_cache.transition', event.name, utils.to_string(demuxer_cache.state))
    170   else
    171     msg.error(
    172       'demuxer_cache.transition',
    173       'illegal transition', event.name,
    174       'from state', demuxer_cache.state.name)
    175   end
    176 end
    177 
    178 function demuxer_cache.initialize(demuxer_cache_timer_interval)
    179   demuxer_cache.reset_state()
    180   demuxer_cache.timer = mp.add_periodic_timer(
    181     demuxer_cache_timer_interval,
    182     function()
    183       demuxer_cache.demuxer_cache_timer_tick(
    184         mp.get_property_native('demuxer-cache-time'),
    185         demuxer_cache_timer_interval)
    186     end
    187   )
    188 end
    189 
    190 -- If there is no progress of demuxer_cache_time in
    191 -- settings.demuxer_cache_timer_timeout time interval switch state to
    192 -- 'stuck' and switch back to 'fetch' as soon as any progress is made
    193 function demuxer_cache.demuxer_cache_timer_tick(demuxer_cache_time, demuxer_cache_timer_interval)
    194   local event = nil
    195   local cache_has_progress = demuxer_cache.has_progress_since(demuxer_cache_time)
    196 
    197   -- I miss pattern matching so much
    198   if demuxer_cache.is_state_fetch() then
    199     if cache_has_progress then
    200       event = demuxer_cache.events.continue_fetch
    201     else
    202       event = demuxer_cache.events.fetch_to_stale
    203     end
    204   elseif demuxer_cache.is_state_stale() then
    205     if cache_has_progress then
    206       event = demuxer_cache.events.stale_to_fetch
    207     elseif demuxer_cache.state.in_state_time < settings.demuxer_cache_timer_timeout then
    208       event = demuxer_cache.events.continue_stale
    209     else
    210       event = demuxer_cache.events.stale_to_stuck
    211     end
    212   elseif demuxer_cache.is_state_stuck() then
    213     if cache_has_progress then
    214       event = demuxer_cache.events.stuck_to_fetch
    215     else
    216       event = demuxer_cache.events.continue_stuck
    217     end
    218   end
    219 
    220   event.demuxer_cache_time = demuxer_cache_time
    221   event.interval = demuxer_cache_timer_interval
    222   demuxer_cache.transition(event)
    223 end
    224 
    225 
    226 local paused_for_cache = {
    227   timer = nil,
    228   time = 0,
    229 }
    230 
    231 function paused_for_cache.reset_timer()
    232   msg.debug('paused_for_cache.reset_timer', paused_for_cache.time)
    233   if paused_for_cache.timer then
    234     paused_for_cache.timer:kill()
    235     paused_for_cache.timer = nil
    236     paused_for_cache.time = 0
    237   end
    238 end
    239 
    240 function paused_for_cache.start_timer(interval_seconds, timeout_seconds)
    241   msg.debug('paused_for_cache.start_timer', paused_for_cache.time)
    242   if not paused_for_cache.timer then
    243     paused_for_cache.timer = mp.add_periodic_timer(
    244       interval_seconds,
    245       function()
    246         paused_for_cache.time = paused_for_cache.time + interval_seconds
    247         if paused_for_cache.time >= timeout_seconds then
    248           paused_for_cache.reset_timer()
    249           reload_resume()
    250         end
    251         msg.debug('paused_for_cache', 'tick', paused_for_cache.time)
    252       end
    253     )
    254   end
    255 end
    256 
    257 function paused_for_cache.handler(property, is_paused)
    258   if is_paused then
    259 
    260     if demuxer_cache.is_state_stuck() then
    261       msg.info("demuxer cache has no progress")
    262       -- reset demuxer state to avoid immediate reload if
    263       -- paused_for_cache event triggered right after reload
    264       demuxer_cache.reset_state()
    265       reload_resume()
    266     end
    267 
    268     paused_for_cache.start_timer(
    269       settings.paused_for_cache_timer_interval,
    270       settings.paused_for_cache_timer_timeout)
    271   else
    272     paused_for_cache.reset_timer()
    273   end
    274 end
    275 
    276 function read_settings()
    277   options.read_options(settings, mp.get_script_name())
    278   msg.debug(utils.to_string(settings))
    279 end
    280 
    281 function debug_info(event)
    282   msg.debug("event =", utils.to_string(event))
    283   msg.debug("path =", mp.get_property("path"))
    284   msg.debug("time-pos =", mp.get_property("time-pos"))
    285   msg.debug("paused-for-cache =", mp.get_property("paused-for-cache"))
    286   msg.debug("stream-path =", mp.get_property("stream-path"))
    287   msg.debug("stream-pos =", mp.get_property("stream-pos"))
    288   msg.debug("stream-end =", mp.get_property("stream-end"))
    289   msg.debug("duration =", mp.get_property("duration"))
    290   msg.debug("seekable =", mp.get_property("seekable"))
    291 end
    292 
    293 function reload(path, time_pos)
    294   msg.debug("reload", path, time_pos)
    295   if time_pos == nil then
    296     mp.commandv("loadfile", path, "replace")
    297   else
    298     mp.commandv("loadfile", path, "replace", "start=+" .. time_pos)
    299   end
    300 end
    301 
    302 function reload_resume()
    303   local path = mp.get_property("path", property_path)
    304   local time_pos = mp.get_property("time-pos")
    305   local reload_duration = mp.get_property_native("duration")
    306 
    307   local playlist_count = mp.get_property_number("playlist/count")
    308   local playlist_pos = mp.get_property_number("playlist-pos")
    309   local playlist = {}
    310   for i = 0, playlist_count-1 do
    311       playlist[i] = mp.get_property("playlist/" .. i .. "/filename")
    312   end
    313   -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
    314   -- duration property. When reloading VOD, to keep the current time position
    315   -- we should provide offset from the start. Stream doesn't have fixed start.
    316   -- Decent choice would be to reload stream from it's current 'live' positon.
    317   -- That's the reason we don't pass the offset when reloading streams.
    318   if reload_duration and reload_duration > 0 then
    319     msg.info("reloading video from", time_pos, "second")
    320     reload(path, time_pos)
    321   else
    322     msg.info("reloading stream")
    323     reload(path, nil)
    324   end
    325   msg.info("file ", playlist_pos+1, " of ", playlist_count, "in playlist")
    326   for i = 0, playlist_pos-1 do
    327     mp.commandv("loadfile", playlist[i], "append")
    328   end
    329   mp.commandv("playlist-move", 0, playlist_pos+1)
    330   for i = playlist_pos+1, playlist_count-1 do
    331     mp.commandv("loadfile", playlist[i], "append")
    332   end
    333 end
    334 
    335 function reload_eof(property, eof_reached)
    336   msg.debug("reload_eof", property, eof_reached)
    337   local time_pos = mp.get_property_number("time-pos")
    338   local duration = mp.get_property_number("duration")
    339 
    340   if eof_reached and math.floor(time_pos) == math.floor(duration) then
    341     msg.debug("property_time_pos", property_time_pos, "time_pos", time_pos)
    342 
    343     -- Check that playback time_pos made progress after the last reload. When
    344     -- eof is reached we try to reload video, in case there is more content
    345     -- available. If time_pos stayed the same after reload, it means that vidkk
    346     -- to avoid infinite reload loop when playback ended
    347     -- math.floor function rounds time_pos to a second, to avoid inane reloads
    348     if math.floor(property_time_pos) == math.floor(time_pos) then
    349       msg.info("eof reached, playback ended")
    350       mp.set_property("keep-open", property_keep_open)
    351     else
    352       msg.info("eof reached, checking if more content available")
    353       reload_resume()
    354       mp.set_property_bool("pause", false)
    355       property_time_pos = time_pos
    356     end
    357   end
    358 end
    359 
    360 -- main
    361 
    362 read_settings()
    363 
    364 if settings.reload_key_binding ~= "" then
    365   mp.add_key_binding(settings.reload_key_binding, "reload_resume", reload_resume)
    366 end
    367 
    368 if settings.paused_for_cache_timer_enabled then
    369   mp.observe_property("paused-for-cache", "bool", paused_for_cache.handler)
    370 end
    371 
    372 if settings.demuxer_cache_timer_enabled then
    373   demuxer_cache.initialize(settings.demuxer_cache_timer_interval)
    374 end
    375 
    376 if settings.reload_eof_enabled then
    377   -- vo-configured == video output created && its configuration went ok
    378   mp.observe_property(
    379     "vo-configured",
    380     "bool",
    381     function(name, vo_configured)
    382       msg.debug(name, vo_configured)
    383       if vo_configured then
    384         property_path = mp.get_property("path")
    385         property_keep_open = mp.get_property("keep-open")
    386         mp.set_property("keep-open", "yes")
    387         mp.set_property("keep-open-pause", "no")
    388       end
    389     end
    390   )
    391 
    392   mp.observe_property("eof-reached", "bool", reload_eof)
    393 end
    394 
    395 --mp.register_event("file-loaded", debug_info)