summaryrefslogtreecommitdiffstats
path: root/TOOLS/lua
diff options
context:
space:
mode:
Diffstat (limited to 'TOOLS/lua')
-rw-r--r--TOOLS/lua/README.md20
-rw-r--r--TOOLS/lua/acompressor.lua155
-rw-r--r--TOOLS/lua/ao-null-reload.lua20
-rw-r--r--TOOLS/lua/audio-hotplug-test.lua8
-rw-r--r--TOOLS/lua/autocrop.lua298
-rw-r--r--TOOLS/lua/autodeint.lua156
-rw-r--r--TOOLS/lua/autoload.lua328
-rw-r--r--TOOLS/lua/command-test.lua124
-rw-r--r--TOOLS/lua/cycle-deinterlace-pullup.lua56
-rw-r--r--TOOLS/lua/nan-test.lua37
-rw-r--r--TOOLS/lua/observe-all.lua22
-rw-r--r--TOOLS/lua/ontop-playback.lua19
-rw-r--r--TOOLS/lua/osd-test.lua35
-rw-r--r--TOOLS/lua/pause-when-minimize.lua20
-rw-r--r--TOOLS/lua/skip-logo.lua265
-rw-r--r--TOOLS/lua/status-line.lua92
-rw-r--r--TOOLS/lua/test-hooks.lua32
17 files changed, 1687 insertions, 0 deletions
diff --git a/TOOLS/lua/README.md b/TOOLS/lua/README.md
new file mode 100644
index 0000000..79b67d6
--- /dev/null
+++ b/TOOLS/lua/README.md
@@ -0,0 +1,20 @@
+mpv lua scripts
+===============
+
+The lua scripts in this folder can be loaded on a one-time basis by
+adding the option
+
+ --script=/path/to/script.lua
+
+to mpv's command line.
+
+Where appropriate, they may also be placed in ~/.config/mpv/scripts/ from
+where they will be automatically loaded when mpv starts.
+
+This is only a small selection of internally maintained scripts. Some of them
+are just for testing mpv internals, or serve as examples. An extensive
+user-edited list of 3rd party scripts is available here:
+
+ https://github.com/mpv-player/mpv/wiki/User-Scripts
+
+(Anyone can add their own scripts to that list.)
diff --git a/TOOLS/lua/acompressor.lua b/TOOLS/lua/acompressor.lua
new file mode 100644
index 0000000..6a69140
--- /dev/null
+++ b/TOOLS/lua/acompressor.lua
@@ -0,0 +1,155 @@
+-- This script adds control to the dynamic range compression ffmpeg
+-- filter including key bindings for adjusting parameters.
+--
+-- See https://ffmpeg.org/ffmpeg-filters.html#acompressor for explanation
+-- of the parameters.
+
+local mp = require 'mp'
+local options = require 'mp.options'
+
+local o = {
+ default_enable = false,
+ show_osd = true,
+ osd_timeout = 4000,
+ filter_label = mp.get_script_name(),
+
+ key_toggle = 'n',
+ key_increase_threshold = 'F1',
+ key_decrease_threshold = 'Shift+F1',
+ key_increase_ratio = 'F2',
+ key_decrease_ratio = 'Shift+F2',
+ key_increase_knee = 'F3',
+ key_decrease_knee = 'Shift+F3',
+ key_increase_makeup = 'F4',
+ key_decrease_makeup = 'Shift+F4',
+ key_increase_attack = 'F5',
+ key_decrease_attack = 'Shift+F5',
+ key_increase_release = 'F6',
+ key_decrease_release = 'Shift+F6',
+
+ default_threshold = -25.0,
+ default_ratio = 3.0,
+ default_knee = 2.0,
+ default_makeup = 8.0,
+ default_attack = 20.0,
+ default_release = 250.0,
+
+ step_threshold = -2.5,
+ step_ratio = 1.0,
+ step_knee = 1.0,
+ step_makeup = 1.0,
+ step_attack = 10.0,
+ step_release = 10.0,
+}
+options.read_options(o)
+
+local params = {
+ { name = 'attack', min=0.01, max=2000, hide_default=true, dB='' },
+ { name = 'release', min=0.01, max=9000, hide_default=true, dB='' },
+ { name = 'threshold', min= -30, max= 0, hide_default=false, dB='dB' },
+ { name = 'ratio', min= 1, max= 20, hide_default=false, dB='' },
+ { name = 'knee', min= 1, max= 10, hide_default=true, dB='dB' },
+ { name = 'makeup', min= 0, max= 24, hide_default=false, dB='dB' },
+}
+
+local function parse_value(value)
+ -- Using nil here because tonumber differs between lua 5.1 and 5.2 when parsing fractions in combination with explicit base argument set to 10.
+ -- And we can't omit it because gsub returns 2 values which would get unpacked and cause more problems. Gotta love scripting languages.
+ return tonumber(value:gsub('dB$', ''), nil)
+end
+
+local function format_value(value, dB)
+ return string.format('%g%s', value, dB)
+end
+
+local function show_osd(filter)
+ if not o.show_osd then
+ return
+ end
+
+ if not filter.enabled then
+ mp.commandv('show-text', 'Dynamic range compressor: disabled', o.osd_timeout)
+ return
+ end
+
+ local pretty = {}
+ for _,param in ipairs(params) do
+ local value = parse_value(filter.params[param.name])
+ if not (param.hide_default and value == o['default_' .. param.name]) then
+ pretty[#pretty+1] = string.format('%s: %g%s', param.name:gsub("^%l", string.upper), value, param.dB)
+ end
+ end
+
+ if #pretty == 0 then
+ pretty = ''
+ else
+ pretty = '\n(' .. table.concat(pretty, ', ') .. ')'
+ end
+
+ mp.commandv('show-text', 'Dynamic range compressor: enabled' .. pretty, o.osd_timeout)
+end
+
+local function get_filter()
+ local af = mp.get_property_native('af', {})
+
+ for i = 1, #af do
+ if af[i].label == o.filter_label then
+ return af, i
+ end
+ end
+
+ af[#af+1] = {
+ name = 'acompressor',
+ label = o.filter_label,
+ enabled = false,
+ params = {},
+ }
+
+ for _,param in pairs(params) do
+ af[#af].params[param.name] = format_value(o['default_' .. param.name], param.dB)
+ end
+
+ return af, #af
+end
+
+local function toggle_acompressor()
+ local af, i = get_filter()
+ af[i].enabled = not af[i].enabled
+ mp.set_property_native('af', af)
+ show_osd(af[i])
+end
+
+local function update_param(name, increment)
+ for _,param in pairs(params) do
+ if param.name == string.lower(name) then
+ local af, i = get_filter()
+ local value = parse_value(af[i].params[param.name])
+ value = math.max(param.min, math.min(value + increment, param.max))
+ af[i].params[param.name] = format_value(value, param.dB)
+ af[i].enabled = true
+ mp.set_property_native('af', af)
+ show_osd(af[i])
+ return
+ end
+ end
+
+ mp.msg.error('Unknown parameter "' .. name .. '"')
+end
+
+mp.add_key_binding(o.key_toggle, "toggle-acompressor", toggle_acompressor)
+mp.register_script_message('update-param', update_param)
+
+for _,param in pairs(params) do
+ for direction,step in pairs({increase=1, decrease=-1}) do
+ mp.add_key_binding(o['key_' .. direction .. '_' .. param.name],
+ 'acompressor-' .. direction .. '-' .. param.name,
+ function() update_param(param.name, step*o['step_' .. param.name]); end,
+ { repeatable = true })
+ end
+end
+
+if o.default_enable then
+ local af, i = get_filter()
+ af[i].enabled = true
+ mp.set_property_native('af', af)
+end
diff --git a/TOOLS/lua/ao-null-reload.lua b/TOOLS/lua/ao-null-reload.lua
new file mode 100644
index 0000000..5b2330b
--- /dev/null
+++ b/TOOLS/lua/ao-null-reload.lua
@@ -0,0 +1,20 @@
+-- Handles the edge case where previous attempts to init audio have failed, but
+-- might start working due to a newly added device. This is required in
+-- particular for ao=wasapi, since the internal IMMNotificationClient code that
+-- normally triggers ao-reload will not be running in this case.
+
+function do_reload()
+ mp.command("ao-reload")
+ reloading = nil
+end
+
+function on_audio_device_list_change()
+ if mp.get_property("current-ao") == "null" and not reloading then
+ mp.msg.verbose("audio-device-list changed: reloading audio")
+ -- avoid calling ao-reload too often
+ reloading = mp.add_timeout(0.5, do_reload)
+ end
+end
+
+mp.set_property("options/audio-fallback-to-null", "yes")
+mp.observe_property("audio-device-list", "native", on_audio_device_list_change)
diff --git a/TOOLS/lua/audio-hotplug-test.lua b/TOOLS/lua/audio-hotplug-test.lua
new file mode 100644
index 0000000..8dedc68
--- /dev/null
+++ b/TOOLS/lua/audio-hotplug-test.lua
@@ -0,0 +1,8 @@
+local utils = require("mp.utils")
+
+mp.observe_property("audio-device-list", "native", function(name, val)
+ print("Audio device list changed:")
+ for index, e in ipairs(val) do
+ print(" - '" .. e.name .. "' (" .. e.description .. ")")
+ end
+end)
diff --git a/TOOLS/lua/autocrop.lua b/TOOLS/lua/autocrop.lua
new file mode 100644
index 0000000..b9e1120
--- /dev/null
+++ b/TOOLS/lua/autocrop.lua
@@ -0,0 +1,298 @@
+--[[
+This script uses the lavfi cropdetect filter and the video-crop property to
+automatically crop the currently playing video with appropriate parameters.
+
+It automatically crops the video when playback starts.
+
+You can also manually crop the video by pressing the "C" (shift+c) key.
+Pressing it again undoes the crop.
+
+The workflow is as follows: First, it inserts the cropdetect filter. After
+<detect_seconds> (default is 1) seconds, it then sets video-crop based on the
+vf-metadata values gathered by cropdetect. The cropdetect filter is removed
+after video-crop is set as it is no longer needed.
+
+Since the crop parameters are determined from the 1 second of video between
+inserting the cropdetect filter and setting video-crop, the "C" key should be
+pressed at a position in the video where the crop region is unambiguous (i.e.,
+not a black frame, black background title card, or dark scene).
+
+If non-copy-back hardware decoding is in use, hwdec is temporarily disabled for
+the duration of cropdetect as the filter would fail otherwise.
+
+These are the default options. They can be overridden by adding
+script-opts-append=autocrop-<parameter>=<value> to mpv.conf.
+--]]
+local options = {
+ -- Whether to automatically apply crop at the start of playback. If you
+ -- don't want to crop automatically, add
+ -- script-opts-append=autocrop-auto=no to mpv.conf.
+ auto = true,
+ -- Delay before starting crop in auto mode. You can try to increase this
+ -- value to avoid dark scenes or fade ins at beginning. Automatic cropping
+ -- will not occur if the value is larger than the remaining playback time.
+ auto_delay = 4,
+ -- Black threshold for cropdetect. Smaller values will generally result in
+ -- less cropping. See limit of
+ -- https://ffmpeg.org/ffmpeg-filters.html#cropdetect
+ detect_limit = "24/255",
+ -- The value which the width/height should be divisible by. Smaller
+ -- values have better detection accuracy. If you have problems with
+ -- other filters, you can try to set it to 4 or 16. See round of
+ -- https://ffmpeg.org/ffmpeg-filters.html#cropdetect
+ detect_round = 2,
+ -- The ratio of the minimum clip size to the original. A number from 0 to
+ -- 1. If the picture is over cropped, try adjusting this value.
+ detect_min_ratio = 0.5,
+ -- How long to gather cropdetect data. Increasing this may be desirable to
+ -- allow cropdetect more time to collect data.
+ detect_seconds = 1,
+ -- Whether the OSD shouldn't be used when cropdetect and video-crop are
+ -- applied and removed.
+ suppress_osd = false,
+}
+
+require "mp.options".read_options(options)
+
+local cropdetect_label = mp.get_script_name() .. "-cropdetect"
+
+timers = {
+ auto_delay = nil,
+ detect_crop = nil
+}
+
+local hwdec_backup
+
+local command_prefix = options.suppress_osd and 'no-osd' or ''
+
+function is_enough_time(seconds)
+
+ -- Plus 1 second for deviation.
+ local time_needed = seconds + 1
+ local playtime_remaining = mp.get_property_native("playtime-remaining")
+
+ return playtime_remaining and time_needed < playtime_remaining
+end
+
+function is_cropable(time_needed)
+ if mp.get_property_native('current-tracks/video/image') ~= false then
+ mp.msg.warn("autocrop only works for videos.")
+ return false
+ end
+
+ if not is_enough_time(time_needed) then
+ mp.msg.warn("Not enough time to detect crop.")
+ return false
+ end
+
+ return true
+end
+
+function remove_cropdetect()
+ for _, filter in pairs(mp.get_property_native("vf")) do
+ if filter.label == cropdetect_label then
+ mp.command(
+ string.format("%s vf remove @%s", command_prefix, filter.label))
+
+ return
+ end
+ end
+end
+
+function restore_hwdec()
+ if hwdec_backup then
+ mp.set_property("hwdec", hwdec_backup)
+ hwdec_backup = nil
+ end
+end
+
+function cleanup()
+ remove_cropdetect()
+
+ -- Kill all timers.
+ for index, timer in pairs(timers) do
+ if timer then
+ timer:kill()
+ timers[index] = nil
+ end
+ end
+
+ restore_hwdec()
+end
+
+function detect_crop()
+ local time_needed = options.detect_seconds
+
+ if not is_cropable(time_needed) then
+ return
+ end
+
+ local hwdec_current = mp.get_property("hwdec-current")
+ if hwdec_current:find("-copy$") == nil and hwdec_current ~= "no" and
+ hwdec_current ~= "crystalhd" and hwdec_current ~= "rkmpp" then
+ hwdec_backup = mp.get_property("hwdec")
+ mp.set_property("hwdec", "no")
+ end
+
+ -- Insert the cropdetect filter.
+ local limit = options.detect_limit
+ local round = options.detect_round
+
+ mp.command(
+ string.format(
+ '%s vf pre @%s:cropdetect=limit=%s:round=%d:reset=0',
+ command_prefix, cropdetect_label, limit, round
+ )
+ )
+
+ -- Wait to gather data.
+ timers.detect_crop = mp.add_timeout(time_needed, detect_end)
+end
+
+function detect_end()
+
+ -- Get the metadata and remove the cropdetect filter.
+ local cropdetect_metadata = mp.get_property_native(
+ "vf-metadata/" .. cropdetect_label)
+ remove_cropdetect()
+
+ -- Remove the timer of detect crop.
+ if timers.detect_crop then
+ timers.detect_crop:kill()
+ timers.detect_crop = nil
+ end
+
+ restore_hwdec()
+
+ local meta = {}
+
+ -- Verify the existence of metadata.
+ if cropdetect_metadata then
+ meta = {
+ w = cropdetect_metadata["lavfi.cropdetect.w"],
+ h = cropdetect_metadata["lavfi.cropdetect.h"],
+ x = cropdetect_metadata["lavfi.cropdetect.x"],
+ y = cropdetect_metadata["lavfi.cropdetect.y"],
+ }
+ else
+ mp.msg.error("No crop data.")
+ mp.msg.info("Was the cropdetect filter successfully inserted?")
+ mp.msg.info("Does your version of ffmpeg/libav support AVFrame metadata?")
+ return
+ end
+
+ -- Verify that the metadata meets the requirements and convert it.
+ if meta.w and meta.h and meta.x and meta.y then
+ local width = mp.get_property_native("width")
+ local height = mp.get_property_native("height")
+
+ meta = {
+ w = tonumber(meta.w),
+ h = tonumber(meta.h),
+ x = tonumber(meta.x),
+ y = tonumber(meta.y),
+ min_w = width * options.detect_min_ratio,
+ min_h = height * options.detect_min_ratio,
+ max_w = width,
+ max_h = height
+ }
+ else
+ mp.msg.error("Got empty crop data.")
+ mp.msg.info("You might need to increase detect_seconds.")
+ end
+
+ apply_crop(meta)
+end
+
+function apply_crop(meta)
+
+ -- Verify if it is necessary to crop.
+ local is_effective = meta.w and meta.h and meta.x and meta.y and
+ (meta.x > 0 or meta.y > 0
+ or meta.w < meta.max_w or meta.h < meta.max_h)
+
+ -- Verify it is not over cropped.
+ local is_excessive = false
+ if is_effective and (meta.w < meta.min_w or meta.h < meta.min_h) then
+ mp.msg.info("The area to be cropped is too large.")
+ mp.msg.info("You might need to decrease detect_min_ratio.")
+ is_excessive = true
+ end
+
+ if not is_effective or is_excessive then
+ -- Clear any existing crop.
+ mp.command(string.format("%s set file-local-options/video-crop ''", command_prefix))
+ return
+ end
+
+ -- Apply crop.
+ mp.command(string.format("%s set file-local-options/video-crop %sx%s+%s+%s",
+ command_prefix, meta.w, meta.h, meta.x, meta.y))
+end
+
+function on_start()
+
+ -- Clean up at the beginning.
+ cleanup()
+
+ -- If auto is not true, exit.
+ if not options.auto then
+ return
+ end
+
+ -- If it is the beginning, wait for detect_crop
+ -- after auto_delay seconds, otherwise immediately.
+ local playback_time = mp.get_property_native("playback-time")
+ local is_delay_needed = playback_time
+ and options.auto_delay > playback_time
+
+ if is_delay_needed then
+
+ -- Verify if there is enough time for autocrop.
+ local time_needed = options.auto_delay + options.detect_seconds
+
+ if not is_cropable(time_needed) then
+ return
+ end
+
+ timers.auto_delay = mp.add_timeout(time_needed,
+ function()
+ detect_crop()
+
+ -- Remove the timer of auto delay.
+ timers.auto_delay:kill()
+ timers.auto_delay = nil
+ end
+ )
+ else
+ detect_crop()
+ end
+end
+
+function on_toggle()
+
+ -- If it is during auto_delay, kill the timer.
+ if timers.auto_delay then
+ timers.auto_delay:kill()
+ timers.auto_delay = nil
+ end
+
+ -- Cropped => Remove it.
+ if mp.get_property("video-crop") ~= "" then
+ mp.command(string.format("%s set file-local-options/video-crop ''", command_prefix))
+ return
+ end
+
+ -- Detecting => Leave it.
+ if timers.detect_crop then
+ mp.msg.warn("Already cropdetecting!")
+ return
+ end
+
+ -- Neither => Detect crop.
+ detect_crop()
+end
+
+mp.add_key_binding("C", "toggle_crop", on_toggle)
+mp.register_event("end-file", cleanup)
+mp.register_event("file-loaded", on_start)
diff --git a/TOOLS/lua/autodeint.lua b/TOOLS/lua/autodeint.lua
new file mode 100644
index 0000000..b891c9a
--- /dev/null
+++ b/TOOLS/lua/autodeint.lua
@@ -0,0 +1,156 @@
+-- This script uses the lavfi idet filter to automatically insert the
+-- appropriate deinterlacing filter based on a short section of the
+-- currently playing video.
+--
+-- It registers the key-binding ctrl+d, which when pressed, inserts the filters
+-- ``vf=idet,lavfi-pullup,idet``. After 4 seconds, it removes these
+-- filters and decides whether the content is progressive, interlaced, or
+-- telecined and the interlacing field dominance.
+--
+-- Based on this information, it may set mpv's ``deinterlace`` property (which
+-- usually inserts the yadif filter), or insert the ``pullup`` filter if the
+-- content is telecined. It also sets field dominance with lavfi setfield.
+--
+-- OPTIONS:
+-- The default detection time may be overridden by adding
+--
+-- --script-opts=autodeint.detect_seconds=<number of seconds>
+--
+-- to mpv's arguments. This may be desirable to allow idet more
+-- time to collect data.
+--
+-- To see counts of the various types of frames for each detection phase,
+-- the verbosity can be increased with
+--
+-- --msg-level=autodeint=v
+
+require "mp.msg"
+
+script_name = mp.get_script_name()
+detect_label = string.format("%s-detect", script_name)
+pullup_label = string.format("%s", script_name)
+dominance_label = string.format("%s-dominance", script_name)
+ivtc_detect_label = string.format("%s-ivtc-detect", script_name)
+
+-- number of seconds to gather cropdetect data
+detect_seconds = tonumber(mp.get_opt(string.format("%s.detect_seconds", script_name)))
+if not detect_seconds then
+ detect_seconds = 4
+end
+
+function del_filter_if_present(label)
+ -- necessary because mp.command('vf del @label:filter') raises an
+ -- error if the filter doesn't exist
+ local vfs = mp.get_property_native("vf")
+
+ for i,vf in pairs(vfs) do
+ if vf["label"] == label then
+ table.remove(vfs, i)
+ mp.set_property_native("vf", vfs)
+ return true
+ end
+ end
+ return false
+end
+
+local function add_vf(label, filter)
+ return mp.command(('vf add @%s:%s'):format(label, filter))
+end
+
+function start_detect()
+ -- exit if detection is already in progress
+ if timer then
+ mp.msg.warn("already detecting!")
+ return
+ end
+
+ mp.set_property("deinterlace","no")
+ del_filter_if_present(pullup_label)
+ del_filter_if_present(dominance_label)
+
+ -- insert the detection filters
+ if not (add_vf(detect_label, 'idet') and
+ add_vf(dominance_label, 'setfield=mode=auto') and
+ add_vf(pullup_label, 'lavfi-pullup') and
+ add_vf(ivtc_detect_label, 'idet')) then
+ mp.msg.error("failed to insert detection filters")
+ return
+ end
+
+ -- wait to gather data
+ timer = mp.add_timeout(detect_seconds, select_filter)
+end
+
+function stop_detect()
+ del_filter_if_present(detect_label)
+ del_filter_if_present(ivtc_detect_label)
+ timer = nil
+end
+
+progressive, interlaced_tff, interlaced_bff, interlaced = 0, 1, 2, 3, 4
+
+function judge(label)
+ -- get the metadata
+ local result = mp.get_property_native(string.format("vf-metadata/%s", label))
+ local num_tff = tonumber(result["lavfi.idet.multiple.tff"])
+ local num_bff = tonumber(result["lavfi.idet.multiple.bff"])
+ local num_progressive = tonumber(result["lavfi.idet.multiple.progressive"])
+ local num_undetermined = tonumber(result["lavfi.idet.multiple.undetermined"])
+ local num_interlaced = num_tff + num_bff
+ local num_determined = num_interlaced + num_progressive
+
+ mp.msg.verbose(label.." progressive = "..num_progressive)
+ mp.msg.verbose(label.." interlaced-tff = "..num_tff)
+ mp.msg.verbose(label.." interlaced-bff = "..num_bff)
+ mp.msg.verbose(label.." undetermined = "..num_undetermined)
+
+ if num_determined < num_undetermined then
+ mp.msg.warn("majority undetermined frames")
+ end
+ if num_progressive > 20*num_interlaced then
+ return progressive
+ elseif num_tff > 10*num_bff then
+ return interlaced_tff
+ elseif num_bff > 10*num_tff then
+ return interlaced_bff
+ else
+ return interlaced
+ end
+end
+
+function select_filter()
+ -- handle the first detection filter results
+ local verdict = judge(detect_label)
+ local ivtc_verdict = judge(ivtc_detect_label)
+ local dominance = "auto"
+ if verdict == progressive then
+ mp.msg.info("progressive: doing nothing")
+ stop_detect()
+ del_filter_if_present(dominance_label)
+ del_filter_if_present(pullup_label)
+ return
+ else
+ if verdict == interlaced_tff then
+ dominance = "tff"
+ add_vf(dominance_label, 'setfield=mode='..dominance)
+ elseif verdict == interlaced_bff then
+ dominance = "bff"
+ add_vf(dominance_label, 'setfield=mode='..dominance)
+ else
+ del_filter_if_present(dominance_label)
+ end
+ end
+
+ -- handle the ivtc detection filter results
+ if ivtc_verdict == progressive then
+ mp.msg.info(string.format("telecined with %s field dominance: using pullup", dominance))
+ stop_detect()
+ else
+ mp.msg.info(string.format("interlaced with %s field dominance: setting deinterlace property", dominance))
+ del_filter_if_present(pullup_label)
+ mp.set_property("deinterlace","yes")
+ stop_detect()
+ end
+end
+
+mp.add_key_binding("ctrl+d", script_name, start_detect)
diff --git a/TOOLS/lua/autoload.lua b/TOOLS/lua/autoload.lua
new file mode 100644
index 0000000..4003cbc
--- /dev/null
+++ b/TOOLS/lua/autoload.lua
@@ -0,0 +1,328 @@
+-- This script automatically loads playlist entries before and after the
+-- the currently played file. It does so by scanning the directory a file is
+-- located in when starting playback. It sorts the directory entries
+-- alphabetically, and adds entries before and after the current file to
+-- the internal playlist. (It stops if it would add an already existing
+-- playlist entry at the same position - this makes it "stable".)
+-- Add at most 5000 * 2 files when starting a file (before + after).
+
+--[[
+To configure this script use file autoload.conf in directory script-opts (the "script-opts"
+directory must be in the mpv configuration directory, typically ~/.config/mpv/).
+
+Example configuration would be:
+
+disabled=no
+images=no
+videos=yes
+audio=yes
+additional_image_exts=list,of,ext
+additional_video_exts=list,of,ext
+additional_audio_exts=list,of,ext
+ignore_hidden=yes
+same_type=yes
+directory_mode=recursive
+
+--]]
+
+MAXENTRIES = 5000
+MAXDIRSTACK = 20
+
+local msg = require 'mp.msg'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+o = {
+ disabled = false,
+ images = true,
+ videos = true,
+ audio = true,
+ additional_image_exts = "",
+ additional_video_exts = "",
+ additional_audio_exts = "",
+ ignore_hidden = true,
+ same_type = false,
+ directory_mode = "auto"
+}
+options.read_options(o, nil, function(list)
+ split_option_exts(list.additional_video_exts, list.additional_audio_exts, list.additional_image_exts)
+ if list.videos or list.additional_video_exts or
+ list.audio or list.additional_audio_exts or
+ list.images or list.additional_image_exts then
+ create_extensions()
+ end
+ if list.directory_mode then
+ validate_directory_mode()
+ end
+end)
+
+function Set (t)
+ local set = {}
+ for _, v in pairs(t) do set[v] = true end
+ return set
+end
+
+function SetUnion (a,b)
+ for k in pairs(b) do a[k] = true end
+ return a
+end
+
+function Split (s)
+ local set = {}
+ for v in string.gmatch(s, '([^,]+)') do set[v] = true end
+ return set
+end
+
+EXTENSIONS_VIDEO = Set {
+ '3g2', '3gp', 'avi', 'flv', 'm2ts', 'm4v', 'mj2', 'mkv', 'mov',
+ 'mp4', 'mpeg', 'mpg', 'ogv', 'rmvb', 'webm', 'wmv', 'y4m'
+}
+
+EXTENSIONS_AUDIO = Set {
+ 'aiff', 'ape', 'au', 'flac', 'm4a', 'mka', 'mp3', 'oga', 'ogg',
+ 'ogm', 'opus', 'wav', 'wma'
+}
+
+EXTENSIONS_IMAGES = Set {
+ 'avif', 'bmp', 'gif', 'j2k', 'jp2', 'jpeg', 'jpg', 'jxl', 'png',
+ 'svg', 'tga', 'tif', 'tiff', 'webp'
+}
+
+function split_option_exts(video, audio, image)
+ if video then o.additional_video_exts = Split(o.additional_video_exts) end
+ if audio then o.additional_audio_exts = Split(o.additional_audio_exts) end
+ if image then o.additional_image_exts = Split(o.additional_image_exts) end
+end
+split_option_exts(true, true, true)
+
+function create_extensions()
+ EXTENSIONS = {}
+ if o.videos then SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_VIDEO), o.additional_video_exts) end
+ if o.audio then SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_AUDIO), o.additional_audio_exts) end
+ if o.images then SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_IMAGES), o.additional_image_exts) end
+end
+create_extensions()
+
+function validate_directory_mode()
+ if o.directory_mode ~= "recursive" and o.directory_mode ~= "lazy" and o.directory_mode ~= "ignore" then
+ o.directory_mode = nil
+ end
+end
+validate_directory_mode()
+
+function add_files(files)
+ local oldcount = mp.get_property_number("playlist-count", 1)
+ for i = 1, #files do
+ mp.commandv("loadfile", files[i][1], "append")
+ mp.commandv("playlist-move", oldcount + i - 1, files[i][2])
+ end
+end
+
+function get_extension(path)
+ match = string.match(path, "%.([^%.]+)$" )
+ if match == nil then
+ return "nomatch"
+ else
+ return match
+ end
+end
+
+table.filter = function(t, iter)
+ for i = #t, 1, -1 do
+ if not iter(t[i]) then
+ table.remove(t, i)
+ end
+ end
+end
+
+table.append = function(t1, t2)
+ local t1_size = #t1
+ for i = 1, #t2 do
+ t1[t1_size + i] = t2[i]
+ end
+end
+
+-- alphanum sorting for humans in Lua
+-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
+
+function alphanumsort(filenames)
+ local function padnum(n, d)
+ return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d))
+ or ("%03d%s"):format(#n, n)
+ end
+
+ local tuples = {}
+ for i, f in ipairs(filenames) do
+ tuples[i] = {f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f}
+ end
+ table.sort(tuples, function(a, b)
+ return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
+ end)
+ for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end
+ return filenames
+end
+
+local autoloaded = nil
+local added_entries = {}
+local autoloaded_dir = nil
+
+function scan_dir(path, current_file, dir_mode, separator, dir_depth, total_files, extensions)
+ if dir_depth == MAXDIRSTACK then
+ return
+ end
+ msg.trace("scanning: " .. path)
+ local files = utils.readdir(path, "files") or {}
+ local dirs = dir_mode ~= "ignore" and utils.readdir(path, "dirs") or {}
+ local prefix = path == "." and "" or path
+ table.filter(files, function (v)
+ -- The current file could be a hidden file, ignoring it doesn't load other
+ -- files from the current directory.
+ if (o.ignore_hidden and not (prefix .. v == current_file) and string.match(v, "^%.")) then
+ return false
+ end
+ local ext = get_extension(v)
+ if ext == nil then
+ return false
+ end
+ return extensions[string.lower(ext)]
+ end)
+ table.filter(dirs, function(d)
+ return not ((o.ignore_hidden and string.match(d, "^%.")))
+ end)
+ alphanumsort(files)
+ alphanumsort(dirs)
+
+ for i, file in ipairs(files) do
+ files[i] = prefix .. file
+ end
+
+ table.append(total_files, files)
+ if dir_mode == "recursive" then
+ for _, dir in ipairs(dirs) do
+ scan_dir(prefix .. dir .. separator, current_file, dir_mode,
+ separator, dir_depth + 1, total_files, extensions)
+ end
+ else
+ for i, dir in ipairs(dirs) do
+ dirs[i] = prefix .. dir
+ end
+ table.append(total_files, dirs)
+ end
+end
+
+function find_and_add_entries()
+ local path = mp.get_property("path", "")
+ local dir, filename = utils.split_path(path)
+ msg.trace(("dir: %s, filename: %s"):format(dir, filename))
+ if o.disabled then
+ msg.debug("stopping: autoload disabled")
+ return
+ elseif #dir == 0 then
+ msg.debug("stopping: not a local path")
+ return
+ end
+
+ local pl_count = mp.get_property_number("playlist-count", 1)
+ this_ext = get_extension(filename)
+ -- check if this is a manually made playlist
+ if (pl_count > 1 and autoloaded == nil) or
+ (pl_count == 1 and EXTENSIONS[string.lower(this_ext)] == nil) then
+ msg.debug("stopping: manually made playlist")
+ return
+ else
+ if pl_count == 1 then
+ autoloaded = true
+ autoloaded_dir = dir
+ added_entries = {}
+ end
+ end
+
+ local extensions = {}
+ if o.same_type then
+ if EXTENSIONS_VIDEO[string.lower(this_ext)] ~= nil then
+ extensions = EXTENSIONS_VIDEO
+ elseif EXTENSIONS_AUDIO[string.lower(this_ext)] ~= nil then
+ extensions = EXTENSIONS_AUDIO
+ else
+ extensions = EXTENSIONS_IMAGES
+ end
+ else
+ extensions = EXTENSIONS
+ end
+
+ local pl = mp.get_property_native("playlist", {})
+ local pl_current = mp.get_property_number("playlist-pos-1", 1)
+ msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current,
+ utils.to_string(pl)))
+
+ local files = {}
+ do
+ local dir_mode = o.directory_mode or mp.get_property("directory-mode", "lazy")
+ local separator = mp.get_property_native("platform") == "windows" and "\\" or "/"
+ scan_dir(autoloaded_dir, path, dir_mode, separator, 0, files, extensions)
+ end
+
+ if next(files) == nil then
+ msg.debug("no other files or directories in directory")
+ return
+ end
+
+ -- Find the current pl entry (dir+"/"+filename) in the sorted dir list
+ local current
+ for i = 1, #files do
+ if files[i] == path then
+ current = i
+ break
+ end
+ end
+ if current == nil then
+ return
+ end
+ msg.trace("current file position in files: "..current)
+
+ -- treat already existing playlist entries, independent of how they got added
+ -- as if they got added by autoload
+ for _, entry in ipairs(pl) do
+ added_entries[entry.filename] = true
+ end
+
+ local append = {[-1] = {}, [1] = {}}
+ for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1
+ for i = 1, MAXENTRIES do
+ local pos = current + i * direction
+ local file = files[pos]
+ if file == nil or file[1] == "." then
+ break
+ end
+
+ -- skip files that are/were already in the playlist
+ if not added_entries[file] then
+ if direction == -1 then
+ msg.verbose("Prepending " .. file)
+ table.insert(append[-1], 1, {file, pl_current + i * direction + 1})
+ else
+ msg.verbose("Adding " .. file)
+ if pl_count > 1 then
+ table.insert(append[1], {file, pl_current + i * direction - 1})
+ else
+ mp.commandv("loadfile", file, "append")
+ end
+ end
+ end
+ added_entries[file] = true
+ end
+ if pl_count == 1 and direction == -1 and #append[-1] > 0 then
+ for i = 1, #append[-1] do
+ mp.commandv("loadfile", append[-1][i][1], "append")
+ end
+ mp.commandv("playlist-move", 0, current)
+ end
+ end
+
+ if pl_count > 1 then
+ add_files(append[1])
+ add_files(append[-1])
+ end
+end
+
+mp.register_event("start-file", find_and_add_entries)
diff --git a/TOOLS/lua/command-test.lua b/TOOLS/lua/command-test.lua
new file mode 100644
index 0000000..877cacd
--- /dev/null
+++ b/TOOLS/lua/command-test.lua
@@ -0,0 +1,124 @@
+-- Test script for some command API details.
+
+local utils = require("mp.utils")
+
+function join(sep, arr, count)
+ local r = ""
+ if count == nil then
+ count = #arr
+ end
+ for i = 1, count do
+ if i > 1 then
+ r = r .. sep
+ end
+ r = r .. utils.to_string(arr[i])
+ end
+ return r
+end
+
+mp.observe_property("vo-configured", "bool", function(_, v)
+ if v ~= true then
+ return
+ end
+
+ print("async expand-text")
+ mp.command_native_async({"expand-text", "hello ${path}!"},
+ function(res, val, err)
+ print("done async expand-text: " .. join(" ", {res, val, err}))
+ end)
+
+ -- make screenshot writing very slow
+ mp.set_property("screenshot-format", "png")
+ mp.set_property("screenshot-png-compression", "9")
+
+ timer = mp.add_periodic_timer(0.1, function() print("I'm alive") end)
+ timer:resume()
+
+ print("Slow screenshot command...")
+ res, err = mp.command_native({"screenshot"})
+ print("done, res: " .. utils.to_string(res))
+
+ print("Slow screenshot async command...")
+ res, err = mp.command_native_async({"screenshot"}, function(res)
+ print("done (async), res: " .. utils.to_string(res))
+ timer:kill()
+ end)
+ print("done (sending), res: " .. utils.to_string(res))
+
+ print("Broken screenshot async command...")
+ mp.command_native_async({"screenshot-to-file", "/nonexistent/bogus.png"},
+ function(res, val, err)
+ print("done err scr.: " .. join(" ", {res, val, err}))
+ end)
+
+ mp.command_native_async({name = "subprocess", args = {"sh", "-c", "echo hi && sleep 10s"}, capture_stdout = true},
+ function(res, val, err)
+ print("done subprocess: " .. join(" ", {res, val, err}))
+ end)
+
+ local x = mp.command_native_async({name = "subprocess", args = {"sleep", "inf"}},
+ function(res, val, err)
+ print("done sleep inf subprocess: " .. join(" ", {res, val, err}))
+ end)
+ mp.add_timeout(15, function()
+ print("aborting sleep inf subprocess after timeout")
+ mp.abort_async_command(x)
+ end)
+
+ -- (assuming this "freezes")
+ local y = mp.command_native_async({name = "sub-add", url = "-"},
+ function(res, val, err)
+ print("done sub-add stdin: " .. join(" ", {res, val, err}))
+ end)
+ mp.add_timeout(20, function()
+ print("aborting sub-add stdin after timeout")
+ mp.abort_async_command(y)
+ end)
+
+
+ mp.command_native_async({name = "subprocess", args = {"wc", "-c"},
+ stdin_data = "hello", capture_stdout = true},
+ function(res, val, err)
+ print("Should be '5': " .. val.stdout)
+ end)
+ -- blocking stdin by default
+ mp.command_native_async({name = "subprocess", args = {"cat"},
+ capture_stdout = true},
+ function(res, val, err)
+ print("Should be 0: " .. #val.stdout)
+ end)
+ -- stdin + detached
+ mp.command_native_async({name = "subprocess",
+ args = {"bash", "-c", "(sleep 5s ; cat)"},
+ stdin_data = "this should appear after 5s.\n",
+ detach = true},
+ function(res, val, err)
+ print("5s test: " .. val.status)
+ end)
+
+ -- This should get killed on script exit.
+ mp.command_native_async({name = "subprocess", playback_only = false,
+ args = {"sleep", "inf"}}, function()end)
+
+ -- Runs detached; should be killed on player exit (forces timeout)
+ mp.command_native({_flags={"async"}, name = "subprocess",
+ playback_only = false, args = {"sleep", "inf"}})
+end)
+
+function freeze_test(playback_only)
+ -- This "freezes" the script, should be killed via timeout.
+ counter = counter and counter + 1 or 0
+ print("freeze! " .. counter)
+ local x = mp.command_native({name = "subprocess",
+ playback_only = playback_only,
+ args = {"sleep", "inf"}})
+ print("done, killed=" .. utils.to_string(x.killed_by_us))
+end
+
+mp.register_event("shutdown", function()
+ freeze_test(false)
+end)
+
+mp.register_event("idle", function()
+ freeze_test(true)
+end)
diff --git a/TOOLS/lua/cycle-deinterlace-pullup.lua b/TOOLS/lua/cycle-deinterlace-pullup.lua
new file mode 100644
index 0000000..2902e40
--- /dev/null
+++ b/TOOLS/lua/cycle-deinterlace-pullup.lua
@@ -0,0 +1,56 @@
+-- This script cycles between deinterlacing, pullup (inverse
+-- telecine), and both filters off. It uses the "deinterlace" property
+-- so that a hardware deinterlacer will be used if available.
+--
+-- It overrides the default deinterlace toggle keybinding "D"
+-- (shift+d), so that rather than merely cycling the "deinterlace" property
+-- between on and off, it adds a "pullup" step to the cycle.
+--
+-- It provides OSD feedback as to the actual state of the two filters
+-- after each cycle step/keypress.
+--
+-- Note: if hardware decoding is enabled, pullup filter will likely
+-- fail to insert.
+--
+-- TODO: It might make sense to use hardware assisted vdpaupp=pullup,
+-- if available, but I don't have hardware to test it. Patch welcome.
+
+script_name = mp.get_script_name()
+pullup_label = string.format("%s-pullup", script_name)
+
+function pullup_on()
+ for i,vf in pairs(mp.get_property_native('vf')) do
+ if vf['label'] == pullup_label then
+ return "yes"
+ end
+ end
+ return "no"
+end
+
+function do_cycle()
+ if pullup_on() == "yes" then
+ -- if pullup is on remove it
+ mp.command(string.format("vf del @%s:pullup", pullup_label))
+ return
+ elseif mp.get_property("deinterlace") == "yes" then
+ -- if deinterlace is on, turn it off and insert pullup filter
+ mp.set_property("deinterlace", "no")
+ mp.command(string.format("vf add @%s:pullup", pullup_label))
+ return
+ else
+ -- if neither is on, turn on deinterlace
+ mp.set_property("deinterlace", "yes")
+ return
+ end
+end
+
+function cycle_deinterlace_pullup_handler()
+ do_cycle()
+ -- independently determine current state and give user feedback
+ mp.osd_message(string.format("deinterlace: %s\n"..
+ "pullup: %s",
+ mp.get_property("deinterlace"),
+ pullup_on()))
+end
+
+mp.add_key_binding("D", "cycle-deinterlace-pullup", cycle_deinterlace_pullup_handler)
diff --git a/TOOLS/lua/nan-test.lua b/TOOLS/lua/nan-test.lua
new file mode 100644
index 0000000..d3f1c8c
--- /dev/null
+++ b/TOOLS/lua/nan-test.lua
@@ -0,0 +1,37 @@
+-- Test a float property which internally uses NaN.
+-- Run with --no-config (or just scale-param1 not set).
+
+local utils = require 'mp.utils'
+
+prop_name = "scale-param1"
+
+-- internal NaN, return string "default" instead of NaN
+v = mp.get_property_native(prop_name, "fail")
+print("Exp:", "string", "\"default\"")
+print("Got:", type(v), utils.to_string(v))
+
+v = mp.get_property(prop_name)
+print("Exp:", "default")
+print("Got:", v)
+
+-- not representable -> return provided fallback value
+v = mp.get_property_number(prop_name, -100)
+print("Exp:", -100)
+print("Got:", v)
+
+mp.set_property_native(prop_name, 123)
+v = mp.get_property_number(prop_name, -100)
+print("Exp:", "number", 123)
+print("Got:", type(v), utils.to_string(v))
+
+-- try to set an actual NaN
+st, msg = mp.set_property_number(prop_name, 0.0/0)
+print("Exp:", nil, "<message>")
+print("Got:", st, msg)
+
+-- set default
+mp.set_property(prop_name, "default")
+
+v = mp.get_property(prop_name)
+print("Exp:", "default")
+print("Got:", v)
diff --git a/TOOLS/lua/observe-all.lua b/TOOLS/lua/observe-all.lua
new file mode 100644
index 0000000..0037439
--- /dev/null
+++ b/TOOLS/lua/observe-all.lua
@@ -0,0 +1,22 @@
+-- Test script for property change notification mechanism.
+-- Note that watching/reading some properties can be very expensive, or
+-- require the player to synchronously wait on network (when playing
+-- remote files), so you should in general only watch properties you
+-- are interested in.
+
+local utils = require("mp.utils")
+
+function observe(name)
+ mp.observe_property(name, "native", function(name, val)
+ print("property '" .. name .. "' changed to '" ..
+ utils.to_string(val) .. "'")
+ end)
+end
+
+for i,name in ipairs(mp.get_property_native("property-list")) do
+ observe(name)
+end
+
+for i,name in ipairs(mp.get_property_native("options")) do
+ observe("options/" .. name)
+end
diff --git a/TOOLS/lua/ontop-playback.lua b/TOOLS/lua/ontop-playback.lua
new file mode 100644
index 0000000..b02716c
--- /dev/null
+++ b/TOOLS/lua/ontop-playback.lua
@@ -0,0 +1,19 @@
+--makes mpv disable ontop when pausing and re-enable it again when resuming playback
+--please note that this won't do anything if ontop was not enabled before pausing
+
+local was_ontop = false
+
+mp.observe_property("pause", "bool", function(name, value)
+ local ontop = mp.get_property_native("ontop")
+ if value then
+ if ontop then
+ mp.set_property_native("ontop", false)
+ was_ontop = true
+ end
+ else
+ if was_ontop and not ontop then
+ mp.set_property_native("ontop", true)
+ end
+ was_ontop = false
+ end
+end)
diff --git a/TOOLS/lua/osd-test.lua b/TOOLS/lua/osd-test.lua
new file mode 100644
index 0000000..1b17819
--- /dev/null
+++ b/TOOLS/lua/osd-test.lua
@@ -0,0 +1,35 @@
+local assdraw = require 'mp.assdraw'
+local utils = require 'mp.utils'
+
+things = {}
+for i = 1, 2 do
+ things[i] = {
+ osd1 = mp.create_osd_overlay("ass-events"),
+ osd2 = mp.create_osd_overlay("ass-events")
+ }
+end
+things[1].text = "{\\an5}hello\\Nworld"
+things[2].text = "{\\pos(400, 200)}something something"
+
+mp.add_periodic_timer(2, function()
+ for i, thing in ipairs(things) do
+ thing.osd1.data = thing.text
+ thing.osd1.compute_bounds = true
+ --thing.osd1.hidden = true
+ local res = thing.osd1:update()
+ print("res " .. i .. ": " .. utils.to_string(res))
+
+ thing.osd2.hidden = true
+ if res ~= nil and res.x0 ~= nil then
+ local draw = assdraw.ass_new()
+ draw:append("{\\alpha&H80}")
+ draw:draw_start()
+ draw:pos(0, 0)
+ draw:rect_cw(res.x0, res.y0, res.x1, res.y1)
+ draw:draw_stop()
+ thing.osd2.hidden = false
+ thing.osd2.data = draw.text
+ end
+ thing.osd2:update()
+ end
+end)
diff --git a/TOOLS/lua/pause-when-minimize.lua b/TOOLS/lua/pause-when-minimize.lua
new file mode 100644
index 0000000..99add70
--- /dev/null
+++ b/TOOLS/lua/pause-when-minimize.lua
@@ -0,0 +1,20 @@
+-- This script pauses playback when minimizing the window, and resumes playback
+-- if it's brought back again. If the player was already paused when minimizing,
+-- then try not to mess with the pause state.
+
+local did_minimize = false
+
+mp.observe_property("window-minimized", "bool", function(name, value)
+ local pause = mp.get_property_native("pause")
+ if value == true then
+ if pause == false then
+ mp.set_property_native("pause", true)
+ did_minimize = true
+ end
+ elseif value == false then
+ if did_minimize and (pause == true) then
+ mp.set_property_native("pause", false)
+ end
+ did_minimize = false
+ end
+end)
diff --git a/TOOLS/lua/skip-logo.lua b/TOOLS/lua/skip-logo.lua
new file mode 100644
index 0000000..8e1f9da
--- /dev/null
+++ b/TOOLS/lua/skip-logo.lua
@@ -0,0 +1,265 @@
+--[[
+
+Automatically skip in files if video frames with pre-supplied fingerprints are
+detected. This will skip ahead by a pre-configured amount of time if a matching
+video frame is detected.
+
+This requires the vf_fingerprint video filter to be compiled in. Read the
+documentation of this filter for caveats (which will automatically apply to
+this script as well), such as no support for zero-copy hardware decoding.
+
+You need to manually gather and provide fingerprints for video frames and add
+them to a configuration file in script-opts/skip-logo.conf (the "script-opts"
+directory must be in the mpv configuration directory, typically ~/.config/mpv/).
+
+Example script-opts/skip-logo.conf:
+
+
+ cases = {
+ {
+ -- Skip ahead 10 seconds if a black frame was detected
+ -- Note: this is dangerous non-sense. It's just for demonstration.
+ name = "black frame", -- print if matched
+ skip = 10, -- number of seconds to skip forward
+ score = 0.3, -- required score
+ fingerprint = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ },
+ {
+ -- Skip ahead 20 seconds if a white frame was detected
+ -- Note: this is dangerous non-sense. It's just for demonstration.
+ name = "fun2",
+ skip = 20,
+ fingerprint = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ },
+ }
+
+This is actually a lua file. Lua was chosen because it seemed less of a pain to
+parse. Future versions of this script may change the format.
+
+The fingerprint is a video frame, converted to "gray" (8 bit per pixels), full
+range, each pixel concatenated into an array, converted to a hex string. You
+can produce these fingerprints by running this manually:
+
+ mpv --vf=fingerprint:print yourfile.mkv
+
+This will log the fingerprint of each video frame to the console, along with its
+timestamp. You find the fingerprint of a unique-enough looking frame, and add
+it as entry to skip-logo.conf.
+
+You can provide a score for "fuzziness". If no score is provided, a default
+value of 0.3 is used. The score is inverse: 0 means exactly the same, while a
+higher score means a higher difference. Currently, the score is computed as
+euclidean distance between the video frame and the pre-provided fingerprint,
+thus the highest score is 16. You probably want a score lower than 1 at least.
+(This algorithm is very primitive, but also simple and fast to compute.)
+
+There's always the danger of false positives, which might be quite annoying.
+It's up to you what you hate more, the logo, or random skips if false positives
+are detected. Also, it's always active, and might eat too much CPU with files
+that have a high resolution or framerate. To temporarily disable the script,
+having a keybind like this in your input.conf will be helpful:
+
+ ctrl+k vf toggle @skip-logo
+
+This will disable/enable the fingerprint filter, which the script automatically
+adds at start.
+
+Another important caveat is that the script currently disables matching during
+seeking or playback initialization, which means it cannot match the first few
+frames of a video. This could be fixed, but the author was too lazy to do so.
+
+--]]
+
+local utils = require "mp.utils"
+local msg = require "mp.msg"
+
+local label = "skip-logo"
+local meta_property = string.format("vf-metadata/%s", label)
+
+local config = {}
+local cases = {}
+local cur_bmp
+local seeking = false
+local playback_start_pts = nil
+
+-- Convert a hex string to an array. Convert each byte to a [0,1] float by
+-- interpreting it as normalized uint8_t.
+-- The data parameter, if not nil, may be used as storage (avoiding garbage).
+local function hex_to_norm8(hex, data)
+ local size = math.floor(#hex / 2)
+ if #hex ~= size * 2 then
+ return nil
+ end
+ local res
+ if (data ~= nil) and (#data == size) then
+ res = data
+ else
+ res = {}
+ end
+ for i = 1, size do
+ local num = tonumber(hex:sub(i * 2, i * 2 + 1), 16)
+ if num == nil then
+ return nil
+ end
+ res[i] = num / 255.0
+ end
+ return res
+end
+
+local function compare_bmp(a, b)
+ if #a ~= #b then
+ return nil -- can't compare
+ end
+ local sum = 0
+ for i = 1, #a do
+ local diff = a[i] - b[i]
+ sum = sum + diff * diff
+ end
+ return math.sqrt(sum)
+end
+
+local function load_config()
+ local conf_file = mp.find_config_file("script-opts/skip-logo.conf")
+ local conf_fn
+ local err = nil
+ if conf_file then
+ if setfenv then
+ conf_fn, err = loadfile(conf_file)
+ if conf_fn then
+ setfenv(conf_fn, config)
+ end
+ else
+ conf_fn, err = loadfile(conf_file, "t", config)
+ end
+ else
+ err = "config file not found"
+ end
+
+ if conf_fn and (not err) then
+ local ok, err2 = pcall(conf_fn)
+ err = err2
+ end
+
+ if err then
+ msg.error("Failed to load config file:", err)
+ end
+
+ if config.cases then
+ for n, case in ipairs(config.cases) do
+ local err = nil
+ case.bitmap = hex_to_norm8(case.fingerprint)
+ if case.bitmap == nil then
+ err = "invalid or missing fingerprint field"
+ end
+ if case.score == nil then
+ case.score = 0.3
+ end
+ if type(case.score) ~= "number" then
+ err = "score field is not a number"
+ end
+ if type(case.skip) ~= "number" then
+ err = "skip field is not a number or missing"
+ end
+ if case.name == nil then
+ case.name = ("Entry %d"):format(n)
+ end
+ if err == nil then
+ cases[#cases + 1] = case
+ else
+ msg.error(("Entry %s: %s, ignoring."):format(case.name, err))
+ end
+ end
+ end
+end
+
+load_config()
+
+-- Returns true on match and if something was done.
+local function check_fingerprint(hex, pts)
+ local bmp = hex_to_norm8(hex, cur_bmp)
+ cur_bmp = bmp
+
+ -- If parsing the filter's result failed (well, it shouldn't).
+ assert(bmp ~= nil, "filter returned nonsense")
+
+ for _, case in ipairs(cases) do
+ local score = compare_bmp(case.bitmap, bmp)
+ if (score ~= nil) and (score <= case.score) then
+ msg.warn(("Matching %s: score=%f (required: %f) at %s, skipping %f seconds"):
+ format(case.name, score, case.score, mp.format_time(pts), case.skip))
+ mp.commandv("seek", pts + case.skip, "absolute+exact")
+ return true
+ end
+ end
+
+ return false
+end
+
+local function read_frames()
+ local result = mp.get_property_native(meta_property)
+ if result == nil then
+ return
+ end
+
+ -- Try to get all entries. Out of laziness, assume that there are at most
+ -- 100 entries. (In fact, vf_fingerprint limits it to 10.)
+ for i = 0, 99 do
+ local prefix = string.format("fp%d.", i)
+ local hex = result[prefix .. "hex"]
+
+ local pts = tonumber(result[prefix .. "pts"])
+ if (hex == nil) or (pts == nil) then
+ break
+ end
+
+ local skip = false -- blame Lua for not having "continue" or "goto", not me
+
+ -- If seeking just stopped, there will be frames before the seek target,
+ -- ignore them by checking the timestamps.
+ if playback_start_pts ~= nil then
+ if pts >= playback_start_pts then
+ playback_start_pts = nil -- just for robustness
+ else
+ skip = true
+ end
+ end
+
+ if not skip then
+ if check_fingerprint(hex, pts) then
+ break
+ end
+ end
+ end
+end
+
+mp.observe_property(meta_property, "none", function()
+ -- Ignore frames that are decoded/filtered during seeking.
+ if seeking then
+ return
+ end
+
+ read_frames()
+end)
+
+mp.observe_property("seeking", "bool", function(name, val)
+ seeking = val
+ if seeking == false then
+ playback_start_pts = mp.get_property_number("playback-time")
+ read_frames()
+ end
+end)
+
+local filters = mp.get_property_native("option-info/vf/choices", {})
+local found = false
+for _, f in ipairs(filters) do
+ if f == "fingerprint" then
+ found = true
+ break
+ end
+end
+
+if found then
+ mp.command(("no-osd vf add @%s:fingerprint"):format(label, filter))
+else
+ msg.warn("vf_fingerprint not found")
+end
diff --git a/TOOLS/lua/status-line.lua b/TOOLS/lua/status-line.lua
new file mode 100644
index 0000000..e40dce2
--- /dev/null
+++ b/TOOLS/lua/status-line.lua
@@ -0,0 +1,92 @@
+-- Rebuild the terminal status line as a lua script
+-- Be aware that this will require more cpu power!
+-- Also, this is based on a rather old version of the
+-- builtin mpv status line.
+
+-- Add a string to the status line
+function atsl(s)
+ newStatus = newStatus .. s
+end
+
+function update_status_line()
+ -- Reset the status line
+ newStatus = ""
+
+ if mp.get_property_bool("pause") then
+ atsl("(Paused) ")
+ elseif mp.get_property_bool("paused-for-cache") then
+ atsl("(Buffering) ")
+ end
+
+ if mp.get_property("aid") ~= "no" then
+ atsl("A")
+ end
+ if mp.get_property("vid") ~= "no" then
+ atsl("V")
+ end
+
+ atsl(": ")
+
+ atsl(mp.get_property_osd("time-pos"))
+
+ atsl(" / ");
+ atsl(mp.get_property_osd("duration"));
+
+ atsl(" (")
+ atsl(mp.get_property_osd("percent-pos", -1))
+ atsl("%)")
+
+ local r = mp.get_property_number("speed", -1)
+ if r ~= 1 then
+ atsl(string.format(" x%4.2f", r))
+ end
+
+ r = mp.get_property_number("avsync", nil)
+ if r ~= nil then
+ atsl(string.format(" A-V: %f", r))
+ end
+
+ r = mp.get_property("total-avsync-change", 0)
+ if math.abs(r) > 0.05 then
+ atsl(string.format(" ct:%7.3f", r))
+ end
+
+ r = mp.get_property_number("decoder-drop-frame-count", -1)
+ if r > 0 then
+ atsl(" Late: ")
+ atsl(r)
+ end
+
+ r = mp.get_property_osd("video-bitrate")
+ if r ~= nil and r ~= "" then
+ atsl(" Vb: ")
+ atsl(r)
+ end
+
+ r = mp.get_property_osd("audio-bitrate")
+ if r ~= nil and r ~= "" then
+ atsl(" Ab: ")
+ atsl(r)
+ end
+
+ r = mp.get_property_number("cache", 0)
+ if r > 0 then
+ atsl(string.format(" Cache: %d%% ", r))
+ end
+
+ -- Set the new status line
+ mp.set_property("options/term-status-msg", newStatus)
+end
+
+timer = mp.add_periodic_timer(1, update_status_line)
+
+function on_pause_change(name, value)
+ if value == false then
+ timer:resume()
+ else
+ timer:stop()
+ end
+ mp.add_timeout(0.1, update_status_line)
+end
+mp.observe_property("pause", "bool", on_pause_change)
+mp.register_event("seek", update_status_line)
diff --git a/TOOLS/lua/test-hooks.lua b/TOOLS/lua/test-hooks.lua
new file mode 100644
index 0000000..4e84d9e
--- /dev/null
+++ b/TOOLS/lua/test-hooks.lua
@@ -0,0 +1,32 @@
+local utils = require("mp.utils")
+
+function hardsleep()
+ os.execute("sleep 1s")
+end
+
+local hooks = {"on_before_start_file", "on_load", "on_load_fail",
+ "on_preloaded", "on_unload", "on_after_end_file"}
+
+for _, name in ipairs(hooks) do
+ mp.add_hook(name, 0, function()
+ print("--- hook: " .. name)
+ hardsleep()
+ print(" ... continue")
+ end)
+end
+
+local events = {"start-file", "end-file", "file-loaded", "seek",
+ "playback-restart", "idle", "shutdown"}
+for _, name in ipairs(events) do
+ mp.register_event(name, function()
+ print("--- event: " .. name)
+ end)
+end
+
+local props = {"path", "metadata"}
+for _, name in ipairs(props) do
+ mp.observe_property(name, "native", function(name, val)
+ print("property '" .. name .. "' changed to '" ..
+ utils.to_string(val) .. "'")
+ end)
+end