diff options
Diffstat (limited to 'TOOLS/lua')
-rw-r--r-- | TOOLS/lua/README.md | 20 | ||||
-rw-r--r-- | TOOLS/lua/acompressor.lua | 155 | ||||
-rw-r--r-- | TOOLS/lua/ao-null-reload.lua | 20 | ||||
-rw-r--r-- | TOOLS/lua/audio-hotplug-test.lua | 8 | ||||
-rw-r--r-- | TOOLS/lua/autocrop.lua | 298 | ||||
-rw-r--r-- | TOOLS/lua/autodeint.lua | 156 | ||||
-rw-r--r-- | TOOLS/lua/autoload.lua | 328 | ||||
-rw-r--r-- | TOOLS/lua/command-test.lua | 124 | ||||
-rw-r--r-- | TOOLS/lua/cycle-deinterlace-pullup.lua | 56 | ||||
-rw-r--r-- | TOOLS/lua/nan-test.lua | 37 | ||||
-rw-r--r-- | TOOLS/lua/observe-all.lua | 22 | ||||
-rw-r--r-- | TOOLS/lua/ontop-playback.lua | 19 | ||||
-rw-r--r-- | TOOLS/lua/osd-test.lua | 35 | ||||
-rw-r--r-- | TOOLS/lua/pause-when-minimize.lua | 20 | ||||
-rw-r--r-- | TOOLS/lua/skip-logo.lua | 265 | ||||
-rw-r--r-- | TOOLS/lua/status-line.lua | 92 | ||||
-rw-r--r-- | TOOLS/lua/test-hooks.lua | 32 |
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 |