diff options
Diffstat (limited to 'TOOLS/lua/skip-logo.lua')
-rw-r--r-- | TOOLS/lua/skip-logo.lua | 265 |
1 files changed, 265 insertions, 0 deletions
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 |