diff --git a/skiptosilence.lua b/skiptosilence.lua index cb60a5d..ab25489 100644 --- a/skiptosilence.lua +++ b/skiptosilence.lua @@ -1,7 +1,7 @@ --[[ - * skiptosilence.lua v.2022-02-27 + * skiptosilence.lua v.2023-08-27 * - * AUTHORS: detuur, microraptor + * AUTHORS: detuur, microraptor, Eisa01 * License: MIT * link: https://github.com/detuur/mpv-scripts * @@ -19,110 +19,173 @@ * script-opts/skiptosilence.conf in mpv's user folder. The * parameters will be automatically loaded on start. * + * Dev note about the used filters: + * - `silencedetect` is an audio filter that listens for silence and + * emits text output with details whenever silence is detected. + * Filter documentation: https://ffmpeg.org/ffmpeg-filters.html ****************** TEMPLATE FOR skiptosilence.conf ****************** -# Maximum amount of noise to trigger, in terms of dB. -# The default is -30 (yes, negative). -60 is very sensitive, -# -10 is more tolerant to noise. -quietness = -30 +#--(#number). Maximum amount of noise to trigger, in terms of dB. Lower is more sensitive. +silence_audio_level=-40 -# Minimum duration of silence to trigger. -duration = 0.1 +#--(#number). Duration of the silence that will be detected to trigger skipping. +silence_duration=0.7 -# The fast-forwarded audio can sound jarring. Set to 'yes' -# to mute it while skipping. -mutewhileskipping = no +#--(0/#number). The first detcted silence_duration will be ignored for the defined seconds in this option, and it will continue skipping until the next silence_duration. +# (0 for disabled, or specify seconds). +ignore_silence_duration=1 + +#--(0/#number). Minimum amount of seconds accepted to skip until the configured silence_duration. +# (0 for disabled, or specify seconds) +min_skip_duration=0 + +#--(0/#number). Maximum amount of seconds accepted to skip until the configured silence_duration. +# (0 for disabled, or specify seconds) +max_skip_duration=120 + +#--(yes/no). Default is muted, however if audio was enabled due to custom mpv settings, the fast-forwarded audio can sound jarring. +force_mute_on_skip=no + +#--(yes/no). Display osd messages when actions occur. +osd_msg=yes ************************** END OF TEMPLATE ************************** --]] -local opts = { - quietness = -30, - duration = 0.1, - mutewhileskipping = false +local o = { + silence_audio_level = -40, + silence_duration = 0.7, + ignore_silence_duration=1, + min_skip_duration = 0, + max_skip_duration = 120, + force_mute_on_skip = false, + osd_msg = true, } +(require 'mp.options').read_options(o) local mp = require 'mp' local msg = require 'mp.msg' -local options = require 'mp.options' -old_speed = 1 -was_paused = false -was_muted = false +speed_state = 1 +pause_state = false +mute_state = false +sub_state = nil +secondary_sub_state = nil +vid_state = nil +window_state = nil +skip_flag = false +initial_skip_time = 0 + +function restoreProp(timepos,pause) + if not timepos then timepos = mp.get_property_number("time-pos") end + if not pause then pause = pause_state end + + mp.set_property("vid", vid_state) + mp.set_property("force-window", window_state) + mp.set_property_bool("mute", mute_state) + mp.set_property("speed", speed_state) + mp.unobserve_property(foundSilence) + mp.command("no-osd af remove @skiptosilence") + mp.set_property_bool("pause", pause) + mp.set_property_number("time-pos", timepos) + mp.set_property("sub-visibility", sub_state) + mp.set_property("secondary-sub-visibility", secondary_sub_state) + timer:kill() + skip_flag = false +end ---[[ -Dev note about the used filters: -- `silencedetect` is an audio filter that listens for silence and - emits text output with details whenever silence is detected. -- `nullsink` interrupts the video stream requests to the decoder, - which stops it from bogging down the fast-forward. -- `color` generates a blank image, which renders very quickly and is - good for fast-forwarding. -- Filter documentation: https://ffmpeg.org/ffmpeg-filters.html ---]] +function handleMinMaxDuration(timepos) + if not skip_flag then return end + if not timepos then timepos = mp.get_property_number("time-pos") end + + skip_duration = timepos - initial_skip_time + if o.min_skip_duration > 0 and skip_duration <= o.min_skip_duration then + restoreProp(initial_skip_time) + if o.osd_msg then mp.osd_message('Skipping Cancelled\nSilence is less than configured minimum') end + msg.info('Skipping Cancelled\nSilence is less than configured minimum') + return true + end + if o.max_skip_duration > 0 and skip_duration >= o.max_skip_duration then + restoreProp(initial_skip_time) + if o.osd_msg then mp.osd_message('Skipping Cancelled\nSilence is more than configured maximum') end + msg.info('Skipping Cancelled\nSilence is more than configured maximum') + return true + end + return false +end + +function skippedMessage() + if o.osd_msg then mp.osd_message("Skipped to silence at " .. mp.get_property_osd("time-pos")) end + msg.info("Skipped to silence at " .. mp.get_property_osd("time-pos")) +end function doSkip() - -- Get video dimensions - local width = mp.get_property_native("width"); - local height = mp.get_property_native("height") - - -- Create audio and video filters - mp.command( - "no-osd af add @skiptosilence:lavfi=[silencedetect=noise=" .. - opts.quietness .. "dB:d=" .. opts.duration .. "]" - ) - mp.command( - "no-osd vf add @skiptosilence-blackout:lavfi=" .. - "[nullsink,color=c=black:s=" .. width .. "x" .. height .. "]" - ) - - -- Triggers whenever the `silencedetect` filter emits output - mp.observe_property("af-metadata/skiptosilence", "string", foundSilence) - - was_muted = mp.get_property_native("mute") - if opts.mutewhileskipping then + if skip_flag then return end + initial_skip_time = (mp.get_property_native("time-pos") or 0) + if math.floor(initial_skip_time) == math.floor(mp.get_property_native('duration') or 0) then return end + + local width = mp.get_property_native("osd-width") + local height = mp.get_property_native("osd-height") + mp.set_property_native("geometry", ("%dx%d"):format(width, height)) + + mp.command( + "no-osd af add @skiptosilence:lavfi=[silencedetect=noise=" .. + o.silence_audio_level .. "dB:d=" .. o.silence_duration .. "]" + ) + + mp.observe_property("af-metadata/skiptosilence", "string", foundSilence) + + sub_state = mp.get_property("sub-visibility") + mp.set_property("sub-visibility", "no") + secondary_sub_state = mp.get_property("secondary-sub-visibility") + mp.set_property("secondary-sub-visibility", "no") + window_state = mp.get_property("force-window") + mp.set_property("force-window", "yes") + vid_state = mp.get_property("vid") + mp.set_property("vid", "no") + mute_state = mp.get_property_native("mute") + if o.force_mute_on_skip then mp.set_property_bool("mute", true) end - - was_paused = mp.get_property_native("pause") - mp.set_property_bool("pause", false) - old_speed = mp.get_property_native("speed") - mp.set_property("speed", 100) + pause_state = mp.get_property_native("pause") + mp.set_property_bool("pause", false) + speed_state = mp.get_property_native("speed") + mp.set_property("speed", 100) + skip_flag = true + + timer = mp.add_periodic_timer(0.5, function() + local video_time = (mp.get_property_native("time-pos") or 0) + handleMinMaxDuration(video_time) + end) end function foundSilence(name, value) - if value == "{}" or value == nil then - return -- For some reason these are sometimes emitted. Ignore. - end - - timecode = tonumber(string.match(value, "%d+%.?%d+")) - time_pos = mp.get_property_native("time-pos") - if timecode == nil or timecode < time_pos + 1 then - return -- Ignore anything less than a second ahead. - end - - mp.set_property_bool("mute", was_muted) - mp.set_property_bool("pause", was_paused) - mp.set_property("speed", old_speed) - mp.unobserve_property(foundSilence) - - -- Remove used audio and video filters - mp.command("no-osd af remove @skiptosilence") - mp.command("no-osd vf remove @skiptosilence-blackout") - - -- Seeking to the exact moment even though we've already - -- fast forwarded here allows the video decoder to skip - -- the missed video. This prevents massive A-V lag. - mp.set_property_number("time-pos", timecode) - - -- If we don't wait at least 50ms before messaging the user, we - -- end up displaying an old value for time-pos. - mp.add_timeout(0.05, skippedMessage) + if value == "{}" or value == nil then + return + end + + timecode = tonumber(string.match(value, "%d+%.?%d+")) + if timecode == nil or timecode < initial_skip_time + o.ignore_silence_duration then + return + end + + if handleMinMaxDuration(timecode) then return end + + restoreProp(timecode) + + mp.add_timeout(0.05, skippedMessage) + skip_flag = false end -function skippedMessage() - msg.info("Skipped to silence at " .. mp.get_property_osd("time-pos")) - mp.osd_message("Skipped to silence at " .. mp.get_property_osd("time-pos")) -end +mp.observe_property('pause', 'bool', function(name, value) + if value and skip_flag then + restoreProp(initial_skip_time, true) + end +end) + -options.read_options(opts) +mp.add_hook('on_unload', 9, function() + if skip_flag then + restoreProp() + end +end) mp.add_key_binding("F3", "skip-to-silence", doSkip)