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)