diff options
| author | A Farzat <a@farzat.xyz> | 2024-11-08 10:44:46 +0900 | 
|---|---|---|
| committer | A Farzat <a@farzat.xyz> | 2024-11-13 10:44:46 +0900 | 
| commit | d3ea797dd503a88e17025dc113a5c23a1c69ae81 (patch) | |
| tree | 8d0d5eace8431ccdeb4c613f8b46588f0186dbb7 /.config/mpv/scripts | |
| parent | 7ec1fa7385c1b88a2efdad72c805b254c52084bd (diff) | |
| download | dotfiles-d3ea797dd503a88e17025dc113a5c23a1c69ae81.tar.gz dotfiles-d3ea797dd503a88e17025dc113a5c23a1c69ae81.zip | |
Use mpv script directory instead of a symlink
Diffstat (limited to '.config/mpv/scripts')
| l--------- | .config/mpv/scripts/reload.lua | 1 | ||||
| -rw-r--r-- | .config/mpv/scripts/reload/LICENSE | 21 | ||||
| -rw-r--r-- | .config/mpv/scripts/reload/README.md | 75 | ||||
| -rw-r--r-- | .config/mpv/scripts/reload/main.lua | 427 | 
4 files changed, 523 insertions, 1 deletions
| diff --git a/.config/mpv/scripts/reload.lua b/.config/mpv/scripts/reload.lua deleted file mode 120000 index 35d9cc0..0000000 --- a/.config/mpv/scripts/reload.lua +++ /dev/null @@ -1 +0,0 @@ -../script-repos/mpv-reload/reload.lua
\ No newline at end of file diff --git a/.config/mpv/scripts/reload/LICENSE b/.config/mpv/scripts/reload/LICENSE new file mode 100644 index 0000000..3716cf8 --- /dev/null +++ b/.config/mpv/scripts/reload/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Dmitrii Bushev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/mpv/scripts/reload/README.md b/.config/mpv/scripts/reload/README.md new file mode 100644 index 0000000..3fd6350 --- /dev/null +++ b/.config/mpv/scripts/reload/README.md @@ -0,0 +1,75 @@ +# reload.lua + +When an online video is stuck during buffering or got slow CDN source, +restarting often helps. This script provides automatic reloading of videos that +didn't have buffering progress for some time, keeping the current time position +while preserving entries in the playlist. It also adds `Ctrl+r` keybinding to +reload video manually. + +## Install + +Mpv reads its configuration from `MPV_HOME` directory. On Unix it is +`~/.config/mpv`, see [files](https://mpv.io/manual/stable/#files) section of +the manual for the Windows configuration files. + +To install the script, you can either: + +* clone this repository to the `MPV_HOME/scripts` directory and name it +  appropriately: `git clone <repo-url> -- <MPV_HOME>/scripts/reload` +* or copy `main.lua` to `MPV_HOME/scripts` and rename it `reload.lua`. + +To override default settings, create `reload.conf` file in the script-opts +directory `MPV_HOME/script-opts`. + +NOTE: config file name should match the name of the script. + +For configuration example you can also check +[4e6/dotfiles](https://github.com/4e6/dotfiles/tree/master/.config/mpv) repo. + +## Settings + +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. diff --git a/.config/mpv/scripts/reload/main.lua b/.config/mpv/scripts/reload/main.lua new file mode 100644 index 0000000..36e2e5b --- /dev/null +++ b/.config/mpv/scripts/reload/main.lua @@ -0,0 +1,427 @@ +-- 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 +    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 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 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 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 + +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 + +-- 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) +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) | 
