dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commit d938172418e3d2026b06d48852ea669f701ee085
parent f790377c7c109b5620d00da0296bd6efa9c6528f
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Mon, 24 May 2021 13:34:08 +0200

mpv: remove thumbnailer, performance was terrible

Diffstat:
Mmpv/input.conf | 1-
Mmpv/mpv.conf | 1-
Dmpv/script-opts/mpv_thumbnail_script.conf | 2--
Dmpv/scripts/mpv_thumbnail_script_client_osc.lua | 4117-------------------------------------------------------------------------------
Dmpv/scripts/mpv_thumbnail_script_server-thread-1.lua | 736-------------------------------------------------------------------------------
Dmpv/scripts/mpv_thumbnail_script_server-thread-2.lua | 736-------------------------------------------------------------------------------
6 files changed, 0 insertions(+), 5593 deletions(-)

diff --git a/mpv/input.conf b/mpv/input.conf @@ -191,7 +191,6 @@ WHEEL_DOWN add volume 2 p script-message osc-playlist # show playlist P script-binding osc/visibility # cycle OSC display -ctrl+t script-binding generate-thumbnails c show_text "${chapter-metadata}${chapter}" C show_text "${chapter-list}" alt+c script-message-to crop toggle-crop diff --git a/mpv/mpv.conf b/mpv/mpv.conf @@ -1,4 +1,3 @@ # Start at 50% volume volume=50 osd-font-size=24 -osc=no diff --git a/mpv/script-opts/mpv_thumbnail_script.conf b/mpv/script-opts/mpv_thumbnail_script.conf @@ -1,2 +0,0 @@ -thumbnail_network=yes -disable_keybinds=yes diff --git a/mpv/scripts/mpv_thumbnail_script_client_osc.lua b/mpv/scripts/mpv_thumbnail_script_client_osc.lua @@ -1,4117 +0,0 @@ ---[[ - Copyright (C) 2017 AMM - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. -]]-- ---[[ - mpv_thumbnail_script.lua 0.4.2 - commit b4cf490 (branch master) - https://github.com/TheAMM/mpv_thumbnail_script - Built on 2021-05-01 11:01:10 -]]-- -local assdraw = require 'mp.assdraw' -local msg = require 'mp.msg' -local opt = require 'mp.options' -local utils = require 'mp.utils' - --- Determine platform -- -ON_WINDOWS = (package.config:sub(1,1) ~= '/') - --- Some helper functions needed to parse the options -- -function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end - -function divmod (a, b) - return math.floor(a / b), a % b -end - --- Better modulo -function bmod( i, N ) - return (i % N + N) % N -end - -function join_paths(...) - local sep = ON_WINDOWS and "\\" or "/" - local result = ""; - for i, p in pairs({...}) do - if p ~= "" then - if is_absolute_path(p) then - result = p - else - result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p - end - end - end - return result:gsub("[\\"..sep.."]*$", "") -end - --- /some/path/file.ext -> /some/path, file.ext -function split_path( path ) - local sep = ON_WINDOWS and "\\" or "/" - local first_index, last_index = path:find('^.*' .. sep) - - if last_index == nil then - return "", path - else - local dir = path:sub(0, last_index-1) - local file = path:sub(last_index+1, -1) - - return dir, file - end -end - -function is_absolute_path( path ) - local tmp, is_win = path:gsub("^[A-Z]:\\", "") - local tmp, is_unix = path:gsub("^/", "") - return (is_win > 0) or (is_unix > 0) -end - -function Set(source) - local set = {} - for _, l in ipairs(source) do set[l] = true end - return set -end - ---------------------------- --- More helper functions -- ---------------------------- - --- Removes all keys from a table, without destroying the reference to it -function clear_table(target) - for key, value in pairs(target) do - target[key] = nil - end -end -function shallow_copy(target) - local copy = {} - for k, v in pairs(target) do - copy[k] = v - end - return copy -end - --- Rounds to given decimals. eg. round_dec(3.145, 0) => 3 -function round_dec(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - -function file_exists(name) - local f = io.open(name, "rb") - if f ~= nil then - local ok, err, code = f:read(1) - io.close(f) - return code == nil - else - return false - end -end - -function path_exists(name) - local f = io.open(name, "rb") - if f ~= nil then - io.close(f) - return true - else - return false - end -end - -function create_directories(path) - local cmd - if ON_WINDOWS then - cmd = { args = {"cmd", "/c", "mkdir", path} } - else - cmd = { args = {"mkdir", "-p", path} } - end - utils.subprocess(cmd) -end - --- Find an executable in PATH or CWD with the given name -function find_executable(name) - local delim = ON_WINDOWS and ";" or ":" - - local pwd = os.getenv("PWD") or utils.getcwd() - local path = os.getenv("PATH") - - local env_path = pwd .. delim .. path -- Check CWD first - - local result, filename - for path_dir in env_path:gmatch("[^"..delim.."]+") do - filename = join_paths(path_dir, name) - if file_exists(filename) then - result = filename - break - end - end - - return result -end - -local ExecutableFinder = { path_cache = {} } --- Searches for an executable and caches the result if any -function ExecutableFinder:get_executable_path( name, raw_name ) - name = ON_WINDOWS and not raw_name and (name .. ".exe") or name - - if self.path_cache[name] == nil then - self.path_cache[name] = find_executable(name) or false - end - return self.path_cache[name] -end - --- Format seconds to HH.MM.SS.sss -function format_time(seconds, sep, decimals) - decimals = decimals == nil and 3 or decimals - sep = sep and sep or "." - local s = seconds - local h, s = divmod(s, 60*60) - local m, s = divmod(s, 60) - - local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals) - - return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s) -end - --- Format seconds to 1h 2m 3.4s -function format_time_hms(seconds, sep, decimals, force_full) - decimals = decimals == nil and 1 or decimals - sep = sep ~= nil and sep or " " - - local s = seconds - local h, s = divmod(s, 60*60) - local m, s = divmod(s, 60) - - if force_full or h > 0 then - return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s) - elseif m > 0 then - return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s) - else - return string.format("%." .. tostring(decimals) .. "fs", s) - end -end - --- Writes text on OSD and console -function log_info(txt, timeout) - timeout = timeout or 1.5 - msg.info(txt) - mp.osd_message(txt, timeout) -end - --- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-" -function join_table(source, before, after, sep) - before = before or "" - after = after or "" - sep = sep or ", " - local result = "" - for i, v in pairs(source) do - if not isempty(v) then - local part = before .. v .. after - if i == 1 then - result = part - else - result = result .. sep .. part - end - end - end - return result -end - -function wrap(s, char) - char = char or "'" - return char .. s .. char -end --- Wraps given string into 'string' and escapes any 's in it -function escape_and_wrap(s, char, replacement) - char = char or "'" - replacement = replacement or "\\" .. char - return wrap(string.gsub(s, char, replacement), char) -end --- Escapes single quotes in a string and wraps the input in single quotes -function escape_single_bash(s) - return escape_and_wrap(s, "'", "'\\''") -end - --- Returns (a .. b) if b is not empty or nil -function joined_or_nil(a, b) - return not isempty(b) and (a .. b) or nil -end - --- Put items from one table into another -function extend_table(target, source) - for i, v in pairs(source) do - table.insert(target, v) - end -end - --- Creates a handle and filename for a temporary random file (in current directory) -function create_temporary_file(base, mode, suffix) - local handle, filename - suffix = suffix or "" - while true do - filename = base .. tostring(math.random(1, 5000)) .. suffix - handle = io.open(filename, "r") - if not handle then - handle = io.open(filename, mode) - break - end - io.close(handle) - end - return handle, filename -end - - -function get_processor_count() - local proc_count - - if ON_WINDOWS then - proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS")) - else - local cpuinfo_handle = io.open("/proc/cpuinfo") - if cpuinfo_handle ~= nil then - local cpuinfo_contents = cpuinfo_handle:read("*a") - local _, replace_count = cpuinfo_contents:gsub('processor', '') - proc_count = replace_count - end - end - - if proc_count and proc_count > 0 then - return proc_count - else - return nil - end -end - -function substitute_values(string, values) - local substitutor = function(match) - if match == "%" then - return "%" - else - -- nil is discarded by gsub - return values[match] - end - end - - local substituted = string:gsub('%%(.)', substitutor) - return substituted -end - --- ASS HELPERS -- -function round_rect_top( ass, x0, y0, x1, y1, r ) - local c = 0.551915024494 * r -- circle approximation - ass:move_to(x0 + r, y0) - ass:line_to(x1 - r, y0) -- top line - if r > 0 then - ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner - end - ass:line_to(x1, y1) -- right line - ass:line_to(x0, y1) -- bottom line - ass:line_to(x0, y0 + r) -- left line - if r > 0 then - ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner - end -end - -function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl) - local c = 0.551915024494 - ass:move_to(x0 + rtl, y0) - ass:line_to(x1 - rtr, y0) -- top line - if rtr > 0 then - ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner - end - ass:line_to(x1, y1 - rbr) -- right line - if rbr > 0 then - ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner - end - ass:line_to(x0 + rbl, y1) -- bottom line - if rbl > 0 then - ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner - end - ass:line_to(x0, y0 + rtl) -- left line - if rtl > 0 then - ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner - end -end --- $Revision: 1.5 $ --- $Date: 2014-09-10 16:54:25 $ - --- This module was originally taken from http://cube3d.de/uploads/Main/sha1.txt. - -------------------------------------------------------------------------------- --- SHA-1 secure hash computation, and HMAC-SHA1 signature computation, --- in pure Lua (tested on Lua 5.1) --- License: MIT --- --- Usage: --- local hashAsHex = sha1.hex(message) -- returns a hex string --- local hashAsData = sha1.bin(message) -- returns raw bytes --- --- local hmacAsHex = sha1.hmacHex(key, message) -- hex string --- local hmacAsData = sha1.hmacBin(key, message) -- raw bytes --- --- --- Pass sha1.hex() a string, and it returns a hash as a 40-character hex string. --- For example, the call --- --- local hash = sha1.hex("iNTERFACEWARE") --- --- puts the 40-character string --- --- "e76705ffb88a291a0d2f9710a5471936791b4819" --- --- into the variable 'hash' --- --- Pass sha1.hmacHex() a key and a message, and it returns the signature as a --- 40-byte hex string. --- --- --- The two "bin" versions do the same, but return the 20-byte string of raw --- data that the 40-byte hex strings represent. --- -------------------------------------------------------------------------------- --- --- Description --- Due to the lack of bitwise operations in 5.1, this version uses numbers to --- represents the 32bit words that we combine with binary operations. The basic --- operations of byte based "xor", "or", "and" are all cached in a combination --- table (several 64k large tables are built on startup, which --- consumes some memory and time). The caching can be switched off through --- setting the local cfg_caching variable to false. --- For all binary operations, the 32 bit numbers are split into 8 bit values --- that are combined and then merged again. --- --- Algorithm: http://www.itl.nist.gov/fipspubs/fip180-1.htm --- -------------------------------------------------------------------------------- - -local sha1 = (function() -local sha1 = {} - --- set this to false if you don't want to build several 64k sized tables when --- loading this file (takes a while but grants a boost of factor 13) -local cfg_caching = false --- local storing of global functions (minor speedup) -local floor,modf = math.floor,math.modf -local char,format,rep = string.char,string.format,string.rep - --- merge 4 bytes to an 32 bit word -local function bytes_to_w32 (a,b,c,d) return a*0x1000000+b*0x10000+c*0x100+d end --- split a 32 bit word into four 8 bit numbers -local function w32_to_bytes (i) - return floor(i/0x1000000)%0x100,floor(i/0x10000)%0x100,floor(i/0x100)%0x100,i%0x100 -end - --- shift the bits of a 32 bit word. Don't use negative values for "bits" -local function w32_rot (bits,a) - local b2 = 2^(32-bits) - local a,b = modf(a/b2) - return a+b*b2*(2^(bits)) -end - --- caching function for functions that accept 2 arguments, both of values between --- 0 and 255. The function to be cached is passed, all values are calculated --- during loading and a function is returned that returns the cached values (only) -local function cache2arg (fn) - if not cfg_caching then return fn end - local lut = {} - for i=0,0xffff do - local a,b = floor(i/0x100),i%0x100 - lut[i] = fn(a,b) - end - return function (a,b) - return lut[a*0x100+b] - end -end - --- splits an 8-bit number into 8 bits, returning all 8 bits as booleans -local function byte_to_bits (b) - local b = function (n) - local b = floor(b/n) - return b%2==1 - end - return b(1),b(2),b(4),b(8),b(16),b(32),b(64),b(128) -end - --- builds an 8bit number from 8 booleans -local function bits_to_byte (a,b,c,d,e,f,g,h) - local function n(b,x) return b and x or 0 end - return n(a,1)+n(b,2)+n(c,4)+n(d,8)+n(e,16)+n(f,32)+n(g,64)+n(h,128) -end - --- debug function for visualizing bits in a string -local function bits_to_string (a,b,c,d,e,f,g,h) - local function x(b) return b and "1" or "0" end - return ("%s%s%s%s %s%s%s%s"):format(x(a),x(b),x(c),x(d),x(e),x(f),x(g),x(h)) -end - --- debug function for converting a 8-bit number as bit string -local function byte_to_bit_string (b) - return bits_to_string(byte_to_bits(b)) -end - --- debug function for converting a 32 bit number as bit string -local function w32_to_bit_string(a) - if type(a) == "string" then return a end - local aa,ab,ac,ad = w32_to_bytes(a) - local s = byte_to_bit_string - return ("%s %s %s %s"):format(s(aa):reverse(),s(ab):reverse(),s(ac):reverse(),s(ad):reverse()):reverse() -end - --- bitwise "and" function for 2 8bit number -local band = cache2arg (function(a,b) - local A,B,C,D,E,F,G,H = byte_to_bits(b) - local a,b,c,d,e,f,g,h = byte_to_bits(a) - return bits_to_byte( - A and a, B and b, C and c, D and d, - E and e, F and f, G and g, H and h) - end) - --- bitwise "or" function for 2 8bit numbers -local bor = cache2arg(function(a,b) - local A,B,C,D,E,F,G,H = byte_to_bits(b) - local a,b,c,d,e,f,g,h = byte_to_bits(a) - return bits_to_byte( - A or a, B or b, C or c, D or d, - E or e, F or f, G or g, H or h) - end) - --- bitwise "xor" function for 2 8bit numbers -local bxor = cache2arg(function(a,b) - local A,B,C,D,E,F,G,H = byte_to_bits(b) - local a,b,c,d,e,f,g,h = byte_to_bits(a) - return bits_to_byte( - A ~= a, B ~= b, C ~= c, D ~= d, - E ~= e, F ~= f, G ~= g, H ~= h) - end) - --- bitwise complement for one 8bit number -local function bnot (x) - return 255-(x % 256) -end - --- creates a function to combine to 32bit numbers using an 8bit combination function -local function w32_comb(fn) - return function (a,b) - local aa,ab,ac,ad = w32_to_bytes(a) - local ba,bb,bc,bd = w32_to_bytes(b) - return bytes_to_w32(fn(aa,ba),fn(ab,bb),fn(ac,bc),fn(ad,bd)) - end -end - --- create functions for and, xor and or, all for 2 32bit numbers -local w32_and = w32_comb(band) -local w32_xor = w32_comb(bxor) -local w32_or = w32_comb(bor) - --- xor function that may receive a variable number of arguments -local function w32_xor_n (a,...) - local aa,ab,ac,ad = w32_to_bytes(a) - for i=1,select('#',...) do - local ba,bb,bc,bd = w32_to_bytes(select(i,...)) - aa,ab,ac,ad = bxor(aa,ba),bxor(ab,bb),bxor(ac,bc),bxor(ad,bd) - end - return bytes_to_w32(aa,ab,ac,ad) -end - --- combining 3 32bit numbers through binary "or" operation -local function w32_or3 (a,b,c) - local aa,ab,ac,ad = w32_to_bytes(a) - local ba,bb,bc,bd = w32_to_bytes(b) - local ca,cb,cc,cd = w32_to_bytes(c) - return bytes_to_w32( - bor(aa,bor(ba,ca)), bor(ab,bor(bb,cb)), bor(ac,bor(bc,cc)), bor(ad,bor(bd,cd)) - ) -end - --- binary complement for 32bit numbers -local function w32_not (a) - return 4294967295-(a % 4294967296) -end - --- adding 2 32bit numbers, cutting off the remainder on 33th bit -local function w32_add (a,b) return (a+b) % 4294967296 end - --- adding n 32bit numbers, cutting off the remainder (again) -local function w32_add_n (a,...) - for i=1,select('#',...) do - a = (a+select(i,...)) % 4294967296 - end - return a -end --- converting the number to a hexadecimal string -local function w32_to_hexstring (w) return format("%08x",w) end - --- calculating the SHA1 for some text -function sha1.hex(msg) - local H0,H1,H2,H3,H4 = 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0xC3D2E1F0 - local msg_len_in_bits = #msg * 8 - - local first_append = char(0x80) -- append a '1' bit plus seven '0' bits - - local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length - local current_mod = non_zero_message_bytes % 64 - local second_append = current_mod>0 and rep(char(0), 64 - current_mod) or "" - - -- now to append the length as a 64-bit number. - local B1, R1 = modf(msg_len_in_bits / 0x01000000) - local B2, R2 = modf( 0x01000000 * R1 / 0x00010000) - local B3, R3 = modf( 0x00010000 * R2 / 0x00000100) - local B4 = 0x00000100 * R3 - - local L64 = char( 0) .. char( 0) .. char( 0) .. char( 0) -- high 32 bits - .. char(B1) .. char(B2) .. char(B3) .. char(B4) -- low 32 bits - - msg = msg .. first_append .. second_append .. L64 - - assert(#msg % 64 == 0) - - local chunks = #msg / 64 - - local W = { } - local start, A, B, C, D, E, f, K, TEMP - local chunk = 0 - - while chunk < chunks do - -- - -- break chunk up into W[0] through W[15] - -- - start,chunk = chunk * 64 + 1,chunk + 1 - - for t = 0, 15 do - W[t] = bytes_to_w32(msg:byte(start, start + 3)) - start = start + 4 - end - - -- - -- build W[16] through W[79] - -- - for t = 16, 79 do - -- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16). - W[t] = w32_rot(1, w32_xor_n(W[t-3], W[t-8], W[t-14], W[t-16])) - end - - A,B,C,D,E = H0,H1,H2,H3,H4 - - for t = 0, 79 do - if t <= 19 then - -- (B AND C) OR ((NOT B) AND D) - f = w32_or(w32_and(B, C), w32_and(w32_not(B), D)) - K = 0x5A827999 - elseif t <= 39 then - -- B XOR C XOR D - f = w32_xor_n(B, C, D) - K = 0x6ED9EBA1 - elseif t <= 59 then - -- (B AND C) OR (B AND D) OR (C AND D - f = w32_or3(w32_and(B, C), w32_and(B, D), w32_and(C, D)) - K = 0x8F1BBCDC - else - -- B XOR C XOR D - f = w32_xor_n(B, C, D) - K = 0xCA62C1D6 - end - - -- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt; - A,B,C,D,E = w32_add_n(w32_rot(5, A), f, E, W[t], K), - A, w32_rot(30, B), C, D - end - -- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E. - H0,H1,H2,H3,H4 = w32_add(H0, A),w32_add(H1, B),w32_add(H2, C),w32_add(H3, D),w32_add(H4, E) - end - local f = w32_to_hexstring - return f(H0) .. f(H1) .. f(H2) .. f(H3) .. f(H4) -end - -local function hex_to_binary(hex) - return hex:gsub('..', function(hexval) - return string.char(tonumber(hexval, 16)) - end) -end - -function sha1.bin(msg) - return hex_to_binary(sha1.hex(msg)) -end - -local xor_with_0x5c = {} -local xor_with_0x36 = {} --- building the lookuptables ahead of time (instead of littering the source code --- with precalculated values) -for i=0,0xff do - xor_with_0x5c[char(i)] = char(bxor(i,0x5c)) - xor_with_0x36[char(i)] = char(bxor(i,0x36)) -end - -local blocksize = 64 -- 512 bits - -function sha1.hmacHex(key, text) - assert(type(key) == 'string', "key passed to hmacHex should be a string") - assert(type(text) == 'string', "text passed to hmacHex should be a string") - - if #key > blocksize then - key = sha1.bin(key) - end - - local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), blocksize - #key) - local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), blocksize - #key) - - return sha1.hex(key_xord_with_0x5c .. sha1.bin(key_xord_with_0x36 .. text)) -end - -function sha1.hmacBin(key, text) - return hex_to_binary(sha1.hmacHex(key, text)) -end - -return sha1 -end)() - -local SCRIPT_NAME = "mpv_thumbnail_script" - -local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/" - -local thumbnailer_options = { - -- The thumbnail directory - cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"), - - ------------------------ - -- Generation options -- - ------------------------ - - -- Automatically generate the thumbnails on video load, without a keypress - autogenerate = true, - - -- Only automatically thumbnail videos shorter than this (seconds) - autogenerate_max_duration = 3600, -- 1 hour - - -- SHA1-sum filenames over this length - -- It's nice to know what files the thumbnails are (hence directory names) - -- but long URLs may approach filesystem limits. - hash_filename_length = 128, - - -- Use mpv to generate thumbnail even if ffmpeg is found in PATH - -- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)! - -- mpv is a bit slower, but has better support overall (eg. subtitles in the previews) - prefer_mpv = true, - - -- Explicitly disable subtitles on the mpv sub-calls - mpv_no_sub = false, - -- Add a "--no-config" to the mpv sub-call arguments - mpv_no_config = false, - -- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments - -- Use "" to disable - mpv_profile = "", - -- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log - -- The logs are removed after successful encodes, unless you set mpv_keep_logs below - mpv_logs = true, - -- Keep all mpv logs, even the succesfull ones - mpv_keep_logs = false, - - -- Disable the built-in keybind ("T") to add your own - disable_keybinds = false, - - --------------------- - -- Display options -- - --------------------- - - -- Move the thumbnail up or down - -- For example: - -- topbar/bottombar: 24 - -- rest: 0 - vertical_offset = 24, - - -- Adjust background padding - -- Examples: - -- topbar: 0, 10, 10, 10 - -- bottombar: 10, 0, 10, 10 - -- slimbox/box: 10, 10, 10, 10 - pad_top = 10, - pad_bot = 0, - pad_left = 10, - pad_right = 10, - - -- If true, pad values are screen-pixels. If false, video-pixels. - pad_in_screenspace = true, - -- Calculate pad into the offset - offset_by_pad = true, - - -- Background color in BBGGRR - background_color = "000000", - -- Alpha: 0 - fully opaque, 255 - transparent - background_alpha = 80, - - -- Keep thumbnail on the screen near left or right side - constrain_to_screen = true, - - -- Do not display the thumbnailing progress - hide_progress = false, - - ----------------------- - -- Thumbnail options -- - ----------------------- - - -- The maximum dimensions of the thumbnails (pixels) - thumbnail_width = 200, - thumbnail_height = 200, - - -- The thumbnail count target - -- (This will result in a thumbnail every ~10 seconds for a 25 minute video) - thumbnail_count = 150, - - -- The above target count will be adjusted by the minimum and - -- maximum time difference between thumbnails. - -- The thumbnail_count will be used to calculate a target separation, - -- and min/max_delta will be used to constrict it. - - -- In other words, thumbnails will be: - -- at least min_delta seconds apart (limiting the amount) - -- at most max_delta seconds apart (raising the amount if needed) - min_delta = 5, - -- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours! - max_delta = 90, - - - -- Overrides for remote urls (you generally want less thumbnails!) - -- Thumbnailing network paths will be done with mpv - - -- Allow thumbnailing network paths (naive check for "://") - thumbnail_network = false, - -- Override thumbnail count, min/max delta - remote_thumbnail_count = 60, - remote_min_delta = 15, - remote_max_delta = 120, - - -- Try to grab the raw stream and disable ytdl for the mpv subcalls - -- Much faster than passing the url to ytdl again, but may cause problems with some sites - remote_direct_stream = true, -} - -read_options(thumbnailer_options, SCRIPT_NAME) -local Thumbnailer = { - cache_directory = thumbnailer_options.cache_directory, - - state = { - ready = false, - available = false, - enabled = false, - - thumbnail_template = nil, - - thumbnail_delta = nil, - thumbnail_count = 0, - - thumbnail_size = nil, - - finished_thumbnails = 0, - - -- List of thumbnail states (from 1 to thumbnail_count) - -- ready: 1 - -- in progress: 0 - -- not ready: -1 - thumbnails = {}, - - worker_input_path = nil, - -- Extra options for the workers - worker_extra = {}, - }, - -- Set in register_client - worker_register_timeout = nil, - -- A timer used to wait for more workers in case we have none - worker_wait_timer = nil, - workers = {} -} - -function Thumbnailer:clear_state() - clear_table(self.state) - self.state.ready = false - self.state.available = false - self.state.finished_thumbnails = 0 - self.state.thumbnails = {} - self.state.worker_extra = {} -end - - -function Thumbnailer:on_file_loaded() - self:clear_state() -end - -function Thumbnailer:on_thumb_ready(index) - self.state.thumbnails[index] = 1 - - -- Full recount instead of a naive increment (let's be safe!) - self.state.finished_thumbnails = 0 - for i, v in pairs(self.state.thumbnails) do - if v > 0 then - self.state.finished_thumbnails = self.state.finished_thumbnails + 1 - end - end -end - -function Thumbnailer:on_thumb_progress(index) - self.state.thumbnails[index] = math.max(self.state.thumbnails[index], 0) -end - -function Thumbnailer:on_start_file() - -- Clear state when a new file is being loaded - self:clear_state() -end - -function Thumbnailer:on_video_change(params) - -- Gather a new state when we get proper video-dec-params and our state is empty - if params ~= nil then - if not self.state.ready then - self:update_state() - end - end -end - - -function Thumbnailer:update_state() - msg.debug("Gathering video/thumbnail state") - - self.state.thumbnail_delta = self:get_delta() - self.state.thumbnail_count = self:get_thumbnail_count(self.state.thumbnail_delta) - - -- Prefill individual thumbnail states - for i = 1, self.state.thumbnail_count do - self.state.thumbnails[i] = -1 - end - - self.state.thumbnail_template, self.state.thumbnail_directory = self:get_thumbnail_template() - self.state.thumbnail_size = self:get_thumbnail_size() - - self.state.ready = true - - local file_path = mp.get_property_native("path") - self.state.is_remote = file_path:find("://") ~= nil - - self.state.available = false - - -- Make sure the file has video (and not just albumart) - local track_list = mp.get_property_native("track-list") - local has_video = false - for i, track in pairs(track_list) do - if track.type == "video" and not track.external and not track.albumart then - has_video = true - break - end - end - - if has_video and self.state.thumbnail_delta ~= nil and self.state.thumbnail_size ~= nil and self.state.thumbnail_count > 0 then - self.state.available = true - end - - msg.debug("Thumbnailer.state:", utils.to_string(self.state)) - -end - - -function Thumbnailer:get_thumbnail_template() - local file_path = mp.get_property_native("path") - local is_remote = file_path:find("://") ~= nil - - local filename = mp.get_property_native("filename/no-ext") - local filesize = mp.get_property_native("file-size", 0) - - if is_remote then - filesize = 0 - end - - filename = filename:gsub('[^a-zA-Z0-9_.%-\' ]', '') - -- Hash overly long filenames (most likely URLs) - if #filename > thumbnailer_options.hash_filename_length then - filename = sha1.hex(filename) - end - - local file_key = ("%s-%d"):format(filename, filesize) - - local thumbnail_directory = join_paths(self.cache_directory, file_key) - local file_template = join_paths(thumbnail_directory, "%06d.bgra") - return file_template, thumbnail_directory -end - - -function Thumbnailer:get_thumbnail_size() - local video_dec_params = mp.get_property_native("video-dec-params") - local video_width = video_dec_params.dw - local video_height = video_dec_params.dh - if not (video_width and video_height) then - return nil - end - - local w, h - if video_width > video_height then - w = thumbnailer_options.thumbnail_width - h = math.floor(video_height * (w / video_width)) - else - h = thumbnailer_options.thumbnail_height - w = math.floor(video_width * (h / video_height)) - end - return { w=w, h=h } -end - - -function Thumbnailer:get_delta() - local file_path = mp.get_property_native("path") - local file_duration = mp.get_property_native("duration") - local is_seekable = mp.get_property_native("seekable") - - -- Naive url check - local is_remote = file_path:find("://") ~= nil - - local remote_and_disallowed = is_remote - if is_remote and thumbnailer_options.thumbnail_network then - remote_and_disallowed = false - end - - if remote_and_disallowed or not is_seekable or not file_duration then - -- Not a local path (or remote thumbnails allowed), not seekable or lacks duration - return nil - end - - local thumbnail_count = thumbnailer_options.thumbnail_count - local min_delta = thumbnailer_options.min_delta - local max_delta = thumbnailer_options.max_delta - - if is_remote then - thumbnail_count = thumbnailer_options.remote_thumbnail_count - min_delta = thumbnailer_options.remote_min_delta - max_delta = thumbnailer_options.remote_max_delta - end - - local target_delta = (file_duration / thumbnail_count) - local delta = math.max(min_delta, math.min(max_delta, target_delta)) - - return delta -end - - -function Thumbnailer:get_thumbnail_count(delta) - if delta == nil then - return 0 - end - local file_duration = mp.get_property_native("duration") - - return math.ceil(file_duration / delta) -end - -function Thumbnailer:get_closest(thumbnail_index) - -- Given a 1-based index, find the closest available thumbnail and return it's 1-based index - - -- Check the direct thumbnail index first - if self.state.thumbnails[thumbnail_index] > 0 then - return thumbnail_index - end - - local min_distance = self.state.thumbnail_count + 1 - local closest = nil - - -- Naive, inefficient, lazy. But functional. - for index, value in pairs(self.state.thumbnails) do - local distance = math.abs(index - thumbnail_index) - if distance < min_distance and value > 0 then - min_distance = distance - closest = index - end - end - return closest -end - -function Thumbnailer:get_thumbnail_index(time_position) - -- Returns a 1-based thumbnail index for the given timestamp (between 1 and thumbnail_count, inclusive) - if self.state.thumbnail_delta and (self.state.thumbnail_count and self.state.thumbnail_count > 0) then - return math.min(math.floor(time_position / self.state.thumbnail_delta) + 1, self.state.thumbnail_count) - else - return nil - end -end - -function Thumbnailer:get_thumbnail_path(time_position) - -- Given a timestamp, return: - -- the closest available thumbnail path (if any) - -- the 1-based thumbnail index calculated from the timestamp - -- the 1-based thumbnail index of the closest available (and used) thumbnail - -- OR nil if thumbnails are not available. - - local thumbnail_index = self:get_thumbnail_index(time_position) - if not thumbnail_index then return nil end - - local closest = self:get_closest(thumbnail_index) - - if closest ~= nil then - return self.state.thumbnail_template:format(closest-1), thumbnail_index, closest - else - return nil, thumbnail_index, nil - end -end - -function Thumbnailer:register_client() - self.worker_register_timeout = mp.get_time() + 2 - - mp.register_script_message("mpv_thumbnail_script-ready", function(index, path) - self:on_thumb_ready(tonumber(index), path) - end) - mp.register_script_message("mpv_thumbnail_script-progress", function(index, path) - self:on_thumb_progress(tonumber(index), path) - end) - - mp.register_script_message("mpv_thumbnail_script-worker", function(worker_name) - if not self.workers[worker_name] then - msg.debug("Registered worker", worker_name) - self.workers[worker_name] = true - mp.commandv("script-message-to", worker_name, "mpv_thumbnail_script-slaved") - end - end) - - -- Notify workers to generate thumbnails when video loads/changes - -- This will be executed after the on_video_change (because it's registered after it) - mp.observe_property("video-dec-params", "native", function() - local duration = mp.get_property_native("duration") - local max_duration = thumbnailer_options.autogenerate_max_duration - - if duration ~= nil and self.state.available and thumbnailer_options.autogenerate then - -- Notify if autogenerate is on and video is not too long - if duration < max_duration or max_duration == 0 then - self:start_worker_jobs() - end - end - end) - - local thumb_script_key = not thumbnailer_options.disable_keybinds and "T" or nil - mp.add_key_binding(thumb_script_key, "generate-thumbnails", function() - if self.state.available then - mp.osd_message("Started thumbnailer jobs") - self:start_worker_jobs() - else - mp.osd_message("Thumbnailing unavailabe") - end - end) -end - -function Thumbnailer:_create_thumbnail_job_order() - -- Returns a list of 1-based thumbnail indices in a job order - local used_frames = {} - local work_frames = {} - - -- Pick frames in increasing frequency. - -- This way we can do a quick few passes over the video and then fill in the gaps. - for x = 6, 0, -1 do - local nth = (2^x) - - for thi = 1, self.state.thumbnail_count, nth do - if not used_frames[thi] then - table.insert(work_frames, thi) - used_frames[thi] = true - end - end - end - return work_frames -end - -function Thumbnailer:prepare_source_path() - local file_path = mp.get_property_native("path") - - if self.state.is_remote and thumbnailer_options.remote_direct_stream then - -- Use the direct stream (possibly) provided by ytdl - -- This skips ytdl on the sub-calls, making the thumbnailing faster - -- Works well on YouTube, rest not really tested - file_path = mp.get_property_native("stream-path") - - -- edl:// urls can get LONG. In which case, save the path (URL) - -- to a temporary file and use that instead. - local playlist_filename = join_paths(self.state.thumbnail_directory, "playlist.txt") - - if #file_path > 8000 then - -- Path is too long for a playlist - just pass the original URL to - -- workers and allow ytdl - self.state.worker_extra.enable_ytdl = true - file_path = mp.get_property_native("path") - msg.warn("Falling back to original URL and ytdl due to LONG source path. This will be slow.") - - elseif #file_path > 1024 then - local playlist_file = io.open(playlist_filename, "wb") - if not playlist_file then - msg.error(("Tried to write a playlist to %s but couldn't!"):format(playlist_file)) - return false - end - - playlist_file:write(file_path .. "\n") - playlist_file:close() - - file_path = "--playlist=" .. playlist_filename - msg.warn("Using playlist workaround due to long source path") - end - end - - self.state.worker_input_path = file_path - return true -end - -function Thumbnailer:start_worker_jobs() - -- Create directory for the thumbnails, if needed - local l, err = utils.readdir(self.state.thumbnail_directory) - if err then - msg.debug("Creating thumbnail directory", self.state.thumbnail_directory) - create_directories(self.state.thumbnail_directory) - end - - -- Try to prepare the source path for workers, and bail if unable to do so - if not self:prepare_source_path() then - return - end - - local worker_list = {} - for worker_name in pairs(self.workers) do table.insert(worker_list, worker_name) end - - local worker_count = #worker_list - - -- In case we have a worker timer created already, clear it - -- (For example, if the video-dec-params change in quick succession or the user pressed T, etc) - if self.worker_wait_timer then - self.worker_wait_timer:stop() - end - - if worker_count == 0 then - local now = mp.get_time() - if mp.get_time() > self.worker_register_timeout then - -- Workers have had their time to register but we have none! - local err = "No thumbnail workers found. Make sure you are not missing a script!" - msg.error(err) - mp.osd_message(err, 3) - - else - -- We may be too early. Delay the work start a bit to try again. - msg.warn("No workers found. Waiting a bit more for them.") - -- Wait at least half a second - local wait_time = math.max(self.worker_register_timeout - now, 0.5) - self.worker_wait_timer = mp.add_timeout(wait_time, function() self:start_worker_jobs() end) - end - - else - -- We have at least one worker. This may not be all of them, but they have had - -- their time to register; we've done our best waiting for them. - self.state.enabled = true - - msg.debug( ("Splitting %d thumbnails amongst %d worker(s)"):format(self.state.thumbnail_count, worker_count) ) - - local frame_job_order = self:_create_thumbnail_job_order() - local worker_jobs = {} - for i = 1, worker_count do worker_jobs[worker_list[i]] = {} end - - -- Split frames amongst the workers - for i, thumbnail_index in ipairs(frame_job_order) do - local worker_id = worker_list[ ((i-1) % worker_count) + 1 ] - table.insert(worker_jobs[worker_id], thumbnail_index) - end - - local state_json_string = utils.format_json(self.state) - msg.debug("Giving workers state:", state_json_string) - - for worker_name, worker_frames in pairs(worker_jobs) do - if #worker_frames > 0 then - local frames_json_string = utils.format_json(worker_frames) - msg.debug("Assigning job to", worker_name, frames_json_string) - mp.commandv("script-message-to", worker_name, "mpv_thumbnail_script-job", state_json_string, frames_json_string) - end - end - end -end - -mp.register_event("start-file", function() Thumbnailer:on_start_file() end) -mp.observe_property("video-dec-params", "native", function(name, params) Thumbnailer:on_video_change(params) end) ---[[ -This is mpv's original player/lua/osc.lua patched to display thumbnails - -Sections are denoted with -- mpv_thumbnail_script.lua -- -Current osc.lua version: 69fb2ddbc7b8ccec31348be4b7357ad9a8d48896 -]]-- - -local assdraw = require 'mp.assdraw' -local msg = require 'mp.msg' -local opt = require 'mp.options' -local utils = require 'mp.utils' - --- --- Parameters --- --- default user option values --- do not touch, change them in osc.conf -local user_opts = { - showwindowed = true, -- show OSC when windowed? - showfullscreen = true, -- show OSC when fullscreen? - scalewindowed = 1, -- scaling of the controller when windowed - scalefullscreen = 1, -- scaling of the controller when fullscreen - scaleforcedwindow = 2, -- scaling when rendered on a forced window - vidscale = true, -- scale the controller with the video? - valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom) - halign = 0, -- horizontal alignment, -1 (left) to 1 (right) - barmargin = 0, -- vertical margin of top/bottombar - boxalpha = 80, -- alpha of the background box, - -- 0 (opaque) to 255 (fully transparent) - hidetimeout = 500, -- duration in ms until the OSC hides if no - -- mouse movement. enforced non-negative for the - -- user, but internally negative is "always-on". - fadeduration = 200, -- duration of fade out in ms, 0 = no fade - deadzonesize = 0.5, -- size of deadzone - minmousemove = 0, -- minimum amount of pixels the mouse has to - -- move between ticks to make the OSC show up - iamaprogrammer = false, -- use native mpv values and disable OSC - -- internal track list management (and some - -- functions that depend on it) - layout = "bottombar", - seekbarstyle = "bar", -- bar, diamond or knob - seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle - seekrangestyle = "inverted",-- bar, line, slider, inverted or none - seekrangeseparate = true, -- wether the seekranges overlay on the bar-style seekbar - seekrangealpha = 200, -- transparency of seekranges - seekbarkeyframes = true, -- use keyframes when dragging the seekbar - title = "${media-title}", -- string compatible with property-expansion - -- to be shown as OSC title - tooltipborder = 1, -- border of tooltip in bottom/topbar - timetotal = false, -- display total time instead of remaining time? - timems = false, -- display timecodes with milliseconds? - visibility = "auto", -- only used at init to set visibility_mode(...) - boxmaxchars = 80, -- title crop threshold for box layout - boxvideo = false, -- apply osc_param.video_margins to video - windowcontrols = "auto", -- whether to show window controls - windowcontrols_alignment = "right" -- which side to show window controls on -} - --- read_options may modify hidetimeout, so save the original default value in --- case the user set hidetimeout < 0 and we need the default instead. -local hidetimeout_def = user_opts.hidetimeout --- read options from config and command-line -opt.read_options(user_opts, "osc") -if user_opts.hidetimeout < 0 then - user_opts.hidetimeout = hidetimeout_def - msg.warn("hidetimeout cannot be negative. Using " .. user_opts.hidetimeout) -end - --- validate window control options -if user_opts.windowcontrols ~= "auto" and - user_opts.windowcontrols ~= "yes" and - user_opts.windowcontrols ~= "no" then - msg.warn("windowcontrols cannot be \"" .. - user_opts.windowcontrols .. "\". Ignoring.") - user_opts.windowcontrols = "auto" -end -if user_opts.windowcontrols_alignment ~= "right" and - user_opts.windowcontrols_alignment ~= "left" then - msg.warn("windowcontrols_alignment cannot be \"" .. - user_opts.windowcontrols_alignment .. "\". Ignoring.") - user_opts.windowcontrols_alignment = "right" -end - --- mpv_thumbnail_script.lua -- - --- Patch in msg.trace -if not msg.trace then - msg.trace = function(...) return mp.log("trace", ...) end -end - --- Patch in utils.format_bytes_humanized -if not utils.format_bytes_humanized then - utils.format_bytes_humanized = function(b) - local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"} - local i = 1 - while b >= 1024 do - b = b / 1024 - i = i + 1 - end - return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1)) - end -end - -Thumbnailer:register_client() - -function get_thumbnail_y_offset(thumb_size, msy) - local layout = user_opts.layout - local offset = 0 - - if layout == "bottombar" then - offset = 15 --+ margin - elseif layout == "topbar" then - offset = -(thumb_size.h * msy + 15) - elseif layout == "box" then - offset = 15 - elseif layout == "slimbox" then - offset = 12 - end - - return offset / msy -end - - -local osc_thumb_state = { - visible = false, - overlay_id = 1, - - last_path = nil, - last_x = nil, - last_y = nil, -} - -function hide_thumbnail() - osc_thumb_state.visible = false - osc_thumb_state.last_path = nil - mp.command_native({ "overlay-remove", osc_thumb_state.overlay_id }) -end - -function display_thumbnail(pos, value, ass) - -- If thumbnails are not available, bail - if not (Thumbnailer.state.enabled and Thumbnailer.state.available) then - return - end - - local duration = mp.get_property_number("duration", nil) - if not ((duration == nil) or (value == nil)) then - target_position = duration * (value / 100) - - local msx, msy = get_virt_scale_factor() - local osd_w, osd_h = mp.get_osd_size() - - local thumb_size = Thumbnailer.state.thumbnail_size - local thumb_path, thumb_index, closest_index = Thumbnailer:get_thumbnail_path(target_position) - - local thumbs_ready = Thumbnailer.state.finished_thumbnails - local thumbs_total = Thumbnailer.state.thumbnail_count - local perc = math.floor((thumbs_ready / thumbs_total) * 100) - - local display_progress = thumbs_ready ~= thumbs_total and not thumbnailer_options.hide_progress - - local vertical_offset = thumbnailer_options.vertical_offset - local padding = thumbnailer_options.background_padding - - local pad = { - l = thumbnailer_options.pad_left, r = thumbnailer_options.pad_right, - t = thumbnailer_options.pad_top, b = thumbnailer_options.pad_bot - } - if thumbnailer_options.pad_in_screenspace then - pad.l = pad.l * msx - pad.r = pad.r * msx - pad.t = pad.t * msy - pad.b = pad.b * msy - end - - if thumbnailer_options.offset_by_pad then - vertical_offset = vertical_offset + (user_opts.layout == "topbar" and pad.t or pad.b) - end - - local ass_w = thumb_size.w * msx - local ass_h = thumb_size.h * msy - local y_offset = get_thumbnail_y_offset(thumb_size, 1) - - -- Constrain thumbnail display to window - -- (ie. don't let it go off-screen left/right) - if thumbnailer_options.constrain_to_screen and osd_w > (ass_w + pad.l + pad.r)/msx then - local padded_left = (pad.l + (ass_w / 2)) - local padded_right = (pad.r + (ass_w / 2)) - if pos.x - padded_left < 0 then - pos.x = padded_left - elseif pos.x + padded_right > osd_w*msx then - pos.x = osd_w*msx - padded_right - end - end - - local text_h = 30 * msy - local bg_h = ass_h + (display_progress and text_h or 0) - local bg_left = pos.x - ass_w/2 - local framegraph_h = 10 * msy - - local bg_top = nil - local text_top = nil - local framegraph_top = nil - - if user_opts.layout == "topbar" then - bg_top = pos.y - ( y_offset + thumb_size.h ) + vertical_offset - text_top = bg_top + ass_h + framegraph_h - framegraph_top = bg_top + ass_h - vertical_offset = -vertical_offset - else - bg_top = pos.y - y_offset - bg_h - vertical_offset - text_top = bg_top - framegraph_top = bg_top + 20 * msy - end - - if display_progress then - if user_opts.layout == "topbar" then - pad.b = math.max(0, pad.b - 30) - else - pad.t = math.max(0, pad.t - 30) - end - end - - - - -- Draw background - ass:new_event() - ass:pos(bg_left, bg_top) - ass:append(("{\\bord0\\1c&H%s&\\1a&H%X&}"):format(thumbnailer_options.background_color, thumbnailer_options.background_alpha)) - ass:draw_start() - ass:rect_cw(-pad.l, -pad.t, ass_w+pad.r, bg_h+pad.b) - ass:draw_stop() - - if display_progress then - - ass:new_event() - ass:pos(pos.x, text_top) - ass:an(8) - -- Scale text to correct size - ass:append(("{\\fs20\\bord0\\fscx%f\\fscy%f}"):format(100*msx, 100*msy)) - ass:append(("%d%% - %d/%d"):format(perc, thumbs_ready, thumbs_total)) - - -- Draw the generation progress - local block_w = thumb_size.w * (Thumbnailer.state.thumbnail_delta / duration) * msy - local block_max_x = thumb_size.w * msy - - -- Draw finished thumbnail blocks (white) - ass:new_event() - ass:pos(bg_left, framegraph_top) - ass:append(("{\\bord0\\1c&HFFFFFF&\\1a&H%X&"):format(0)) - ass:draw_start(2) - for i, v in pairs(Thumbnailer.state.thumbnails) do - if i ~= closest_index and v > 0 then - ass:rect_cw((i-1)*block_w, 0, math.min(block_max_x, i*block_w), framegraph_h) - end - end - ass:draw_stop() - - -- Draw in-progress thumbnail blocks (grayish green) - ass:new_event() - ass:pos(bg_left, framegraph_top) - ass:append(("{\\bord0\\1c&H44AA44&\\1a&H%X&"):format(0)) - ass:draw_start(2) - for i, v in pairs(Thumbnailer.state.thumbnails) do - if i ~= closest_index and v == 0 then - ass:rect_cw((i-1)*block_w, 0, math.min(block_max_x, i*block_w), framegraph_h) - end - end - ass:draw_stop() - - if closest_index ~= nil then - ass:new_event() - ass:pos(bg_left, framegraph_top) - ass:append(("{\\bord0\\1c&H4444FF&\\1a&H%X&"):format(0)) - ass:draw_start(2) - ass:rect_cw((closest_index-1)*block_w, 0, math.min(block_max_x, closest_index*block_w), framegraph_h) - ass:draw_stop() - end - end - - if thumb_path then - local overlay_y_offset = get_thumbnail_y_offset(thumb_size, msy) - - local thumb_x = math.floor(pos.x / msx - thumb_size.w/2) - local thumb_y = math.floor(pos.y / msy - thumb_size.h - overlay_y_offset - vertical_offset/msy) - - osc_thumb_state.visible = true - if not (osc_thumb_state.last_path == thumb_path and osc_thumb_state.last_x == thumb_x and osc_thumb_state.last_y == thumb_y) then - local overlay_add_args = { - "overlay-add", osc_thumb_state.overlay_id, - thumb_x, thumb_y, - thumb_path, - 0, - "bgra", - thumb_size.w, thumb_size.h, - 4 * thumb_size.w - } - mp.command_native(overlay_add_args) - - osc_thumb_state.last_path = thumb_path - osc_thumb_state.last_x = thumb_x - osc_thumb_state.last_y = thumb_y - end - end - end -end - --- // mpv_thumbnail_script.lua // -- - -local osc_param = { -- calculated by osc_init() - playresy = 0, -- canvas size Y - playresx = 0, -- canvas size X - display_aspect = 1, - unscaled_y = 0, - areas = {}, - video_margins = { - l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom - }, -} - -local osc_styles = { - bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}", - smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}", - smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}", - smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}", - topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}", - - elementDown = "{\\1c&H999999}", - timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}", - vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}", - box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}", - - topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}", - smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}", - timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}", - timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}", - vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}", - - wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}", - wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}", - wcBar = "{\\1c&H000000}", -} - --- internal states, do not touch -local state = { - showtime, -- time of last invocation (last mouse move) - osc_visible = false, - anistart, -- time when the animation started - anitype, -- current type of animation - animation, -- current animation alpha - mouse_down_counter = 0, -- used for softrepeat - active_element = nil, -- nil = none, 0 = background, 1+ = see elements[] - active_event_source = nil, -- the "button" that issued the current event - rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time - tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds - mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs - initREQ = false, -- is a re-init request pending? - last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement - message_text, - message_timeout, - fullscreen = false, - tick_timer = nil, - tick_last_time = 0, -- when the last tick() was run - cache_state = nil, - idle = false, - enabled = true, - input_enabled = true, - showhide_enabled = false, - dmx_cache = 0, - using_video_margins = false, - border = true, - maximized = false, -} - -local window_control_box_width = 80 -local tick_delay = 0.03 - --- --- Helperfunctions --- - -local margins_opts = { - {"l", "video-margin-ratio-left"}, - {"r", "video-margin-ratio-right"}, - {"t", "video-margin-ratio-top"}, - {"b", "video-margin-ratio-bottom"}, -} - --- scale factor for translating between real and virtual ASS coordinates -function get_virt_scale_factor() - local w, h = mp.get_osd_size() - if w <= 0 or h <= 0 then - return 0, 0 - end - return osc_param.playresx / w, osc_param.playresy / h -end - --- return mouse position in virtual ASS coordinates (playresx/y) -function get_virt_mouse_pos() - local sx, sy = get_virt_scale_factor() - local x, y = mp.get_mouse_pos() - return x * sx, y * sy -end - -function set_virt_mouse_area(x0, y0, x1, y1, name) - local sx, sy = get_virt_scale_factor() - mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name) -end - -function scale_value(x0, x1, y0, y1, val) - local m = (y1 - y0) / (x1 - x0) - local b = y0 - (m * x0) - return (m * val) + b -end - --- returns hitbox spanning coordinates (top left, bottom right corner) --- according to alignment -function get_hitbox_coords(x, y, an, w, h) - - local alignments = { - [1] = function () return x, y-h, x+w, y end, - [2] = function () return x-(w/2), y-h, x+(w/2), y end, - [3] = function () return x-w, y-h, x, y end, - - [4] = function () return x, y-(h/2), x+w, y+(h/2) end, - [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end, - [6] = function () return x-w, y-(h/2), x, y+(h/2) end, - - [7] = function () return x, y, x+w, y+h end, - [8] = function () return x-(w/2), y, x+(w/2), y+h end, - [9] = function () return x-w, y, x, y+h end, - } - - return alignments[an]() -end - -function get_hitbox_coords_geo(geometry) - return get_hitbox_coords(geometry.x, geometry.y, geometry.an, - geometry.w, geometry.h) -end - -function get_element_hitbox(element) - return element.hitbox.x1, element.hitbox.y1, - element.hitbox.x2, element.hitbox.y2 -end - -function mouse_hit(element) - return mouse_hit_coords(get_element_hitbox(element)) -end - -function mouse_hit_coords(bX1, bY1, bX2, bY2) - local mX, mY = get_virt_mouse_pos() - return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2) -end - -function limit_range(min, max, val) - if val > max then - val = max - elseif val < min then - val = min - end - return val -end - --- translate value into element coordinates -function get_slider_ele_pos_for(element, val) - - local ele_pos = scale_value( - element.slider.min.value, element.slider.max.value, - element.slider.min.ele_pos, element.slider.max.ele_pos, - val) - - return limit_range( - element.slider.min.ele_pos, element.slider.max.ele_pos, - ele_pos) -end - --- translates global (mouse) coordinates to value -function get_slider_value_at(element, glob_pos) - - local val = scale_value( - element.slider.min.glob_pos, element.slider.max.glob_pos, - element.slider.min.value, element.slider.max.value, - glob_pos) - - return limit_range( - element.slider.min.value, element.slider.max.value, - val) -end - --- get value at current mouse position -function get_slider_value(element) - return get_slider_value_at(element, get_virt_mouse_pos()) -end - -function countone(val) - if not (user_opts.iamaprogrammer) then - val = val + 1 - end - return val -end - --- align: -1 .. +1 --- frame: size of the containing area --- obj: size of the object that should be positioned inside the area --- margin: min. distance from object to frame (as long as -1 <= align <= +1) -function get_align(align, frame, obj, margin) - return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align) -end - --- multiplies two alpha values, formular can probably be improved -function mult_alpha(alphaA, alphaB) - return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255) -end - -function add_area(name, x1, y1, x2, y2) - -- create area if needed - if (osc_param.areas[name] == nil) then - osc_param.areas[name] = {} - end - table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2}) -end - -function ass_append_alpha(ass, alpha, modifier) - local ar = {} - - for ai, av in pairs(alpha) do - av = mult_alpha(av, modifier) - if state.animation then - av = mult_alpha(av, state.animation) - end - ar[ai] = av - end - - ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}", - ar[1], ar[2], ar[3], ar[4])) -end - -function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2) - if hexagon then - ass:hexagon_cw(x0, y0, x1, y1, r1, r2) - else - ass:round_rect_cw(x0, y0, x1, y1, r1, r2) - end -end - -function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2) - if hexagon then - ass:hexagon_ccw(x0, y0, x1, y1, r1, r2) - else - ass:round_rect_ccw(x0, y0, x1, y1, r1, r2) - end -end - - --- --- Tracklist Management --- - -local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"} - --- updates the OSC internal playlists, should be run each time the track-layout changes -function update_tracklist() - local tracktable = mp.get_property_native("track-list", {}) - - -- by osc_id - tracks_osc = {} - tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {} - -- by mpv_id - tracks_mpv = {} - tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {} - for n = 1, #tracktable do - if not (tracktable[n].type == "unknown") then - local type = tracktable[n].type - local mpv_id = tonumber(tracktable[n].id) - - -- by osc_id - table.insert(tracks_osc[type], tracktable[n]) - - -- by mpv_id - tracks_mpv[type][mpv_id] = tracktable[n] - tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type] - end - end -end - --- return a nice list of tracks of the given type (video, audio, sub) -function get_tracklist(type) - local msg = "Available " .. nicetypes[type] .. " Tracks: " - if #tracks_osc[type] == 0 then - msg = msg .. "none" - else - for n = 1, #tracks_osc[type] do - local track = tracks_osc[type][n] - local lang, title, selected = "unknown", "", "○" - if not(track.lang == nil) then lang = track.lang end - if not(track.title == nil) then title = track.title end - if (track.id == tonumber(mp.get_property(type))) then - selected = "●" - end - msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title - end - end - return msg -end - --- relatively change the track of given <type> by <next> tracks - --(+1 -> next, -1 -> previous) -function set_track(type, next) - local current_track_mpv, current_track_osc - if (mp.get_property(type) == "no") then - current_track_osc = 0 - else - current_track_mpv = tonumber(mp.get_property(type)) - current_track_osc = tracks_mpv[type][current_track_mpv].osc_id - end - local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1) - local new_track_mpv - if new_track_osc == 0 then - new_track_mpv = "no" - else - new_track_mpv = tracks_osc[type][new_track_osc].id - end - - mp.commandv("set", type, new_track_mpv) - - if (new_track_osc == 0) then - show_message(nicetypes[type] .. " Track: none") - else - show_message(nicetypes[type] .. " Track: " - .. new_track_osc .. "/" .. #tracks_osc[type] - .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] " - .. (tracks_osc[type][new_track_osc].title or "")) - end -end - --- get the currently selected track of <type>, OSC-style counted -function get_track(type) - local track = mp.get_property(type) - if track ~= "no" and track ~= nil then - local tr = tracks_mpv[type][tonumber(track)] - if tr then - return tr.osc_id - end - end - return 0 -end - --- WindowControl helpers -function window_controls_enabled() - val = user_opts.windowcontrols - if val == "auto" then - return not state.border - else - return val ~= "no" - end -end - -function window_controls_alignment() - return user_opts.windowcontrols_alignment -end - --- --- Element Management --- - -local elements = {} - -function prepare_elements() - - -- remove elements without layout or invisble - local elements2 = {} - for n, element in pairs(elements) do - if not (element.layout == nil) and (element.visible) then - table.insert(elements2, element) - end - end - elements = elements2 - - function elem_compare (a, b) - return a.layout.layer < b.layout.layer - end - - table.sort(elements, elem_compare) - - - for _,element in pairs(elements) do - - local elem_geo = element.layout.geometry - - -- Calculate the hitbox - local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo) - element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2} - - local style_ass = assdraw.ass_new() - - -- prepare static elements - style_ass:append("{}") -- hack to troll new_event into inserting a \n - style_ass:new_event() - style_ass:pos(elem_geo.x, elem_geo.y) - style_ass:an(elem_geo.an) - style_ass:append(element.layout.style) - - element.style_ass = style_ass - - local static_ass = assdraw.ass_new() - - - if (element.type == "box") then - --draw box - static_ass:draw_start() - ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, - element.layout.box.radius, element.layout.box.hexagon) - static_ass:draw_stop() - - elseif (element.type == "slider") then - --draw static slider parts - - local r1 = 0 - local r2 = 0 - local slider_lo = element.layout.slider - -- offset between element outline and drag-area - local foV = slider_lo.border + slider_lo.gap - - -- calculate positions of min and max points - if (slider_lo.stype ~= "bar") then - r1 = elem_geo.h / 2 - element.slider.min.ele_pos = elem_geo.h / 2 - element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2) - if (slider_lo.stype == "diamond") then - r2 = (elem_geo.h - 2 * slider_lo.border) / 2 - elseif (slider_lo.stype == "knob") then - r2 = r1 - end - else - element.slider.min.ele_pos = - slider_lo.border + slider_lo.gap - element.slider.max.ele_pos = - elem_geo.w - (slider_lo.border + slider_lo.gap) - end - - element.slider.min.glob_pos = - element.hitbox.x1 + element.slider.min.ele_pos - element.slider.max.glob_pos = - element.hitbox.x1 + element.slider.max.ele_pos - - -- -- -- - - static_ass:draw_start() - - -- the box - ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1, slider_lo.stype == "diamond") - - -- the "hole" - ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border, - elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border, - r2, slider_lo.stype == "diamond") - - -- marker nibbles - if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then - local markers = element.slider.markerF() - for _,marker in pairs(markers) do - if (marker > element.slider.min.value) and - (marker < element.slider.max.value) then - - local s = get_slider_ele_pos_for(element, marker) - - if (slider_lo.gap > 1) then -- draw triangles - - local a = slider_lo.gap / 0.5 --0.866 - - --top - if (slider_lo.nibbles_top) then - static_ass:move_to(s - (a/2), slider_lo.border) - static_ass:line_to(s + (a/2), slider_lo.border) - static_ass:line_to(s, foV) - end - - --bottom - if (slider_lo.nibbles_bottom) then - static_ass:move_to(s - (a/2), - elem_geo.h - slider_lo.border) - static_ass:line_to(s, - elem_geo.h - foV) - static_ass:line_to(s + (a/2), - elem_geo.h - slider_lo.border) - end - - else -- draw 2x1px nibbles - - --top - if (slider_lo.nibbles_top) then - static_ass:rect_cw(s - 1, slider_lo.border, - s + 1, slider_lo.border + slider_lo.gap); - end - - --bottom - if (slider_lo.nibbles_bottom) then - static_ass:rect_cw(s - 1, - elem_geo.h -slider_lo.border -slider_lo.gap, - s + 1, elem_geo.h - slider_lo.border); - end - end - end - end - end - end - - element.static_ass = static_ass - - - -- if the element is supposed to be disabled, - -- style it accordingly and kill the eventresponders - if not (element.enabled) then - element.layout.alpha[1] = 136 - element.eventresponder = nil - end - end -end - - --- --- Element Rendering --- - -function render_elements(master_ass) - - for n=1, #elements do - local element = elements[n] - - local style_ass = assdraw.ass_new() - style_ass:merge(element.style_ass) - ass_append_alpha(style_ass, element.layout.alpha, 0) - - if element.eventresponder and (state.active_element == n) then - - -- run render event functions - if not (element.eventresponder.render == nil) then - element.eventresponder.render(element) - end - - if mouse_hit(element) then - -- mouse down styling - if (element.styledown) then - style_ass:append(osc_styles.elementDown) - end - - if (element.softrepeat) and (state.mouse_down_counter >= 15 - and state.mouse_down_counter % 5 == 0) then - - element.eventresponder[state.active_event_source.."_down"](element) - end - state.mouse_down_counter = state.mouse_down_counter + 1 - end - - end - - local elem_ass = assdraw.ass_new() - - elem_ass:merge(style_ass) - - if not (element.type == "button") then - elem_ass:merge(element.static_ass) - end - - if (element.type == "slider") then - - local slider_lo = element.layout.slider - local elem_geo = element.layout.geometry - local s_min = element.slider.min.value - local s_max = element.slider.max.value - - -- draw pos marker - local foH, xp - local pos = element.slider.posF() - local foV = slider_lo.border + slider_lo.gap - local innerH = elem_geo.h - (2 * foV) - local seekRanges = element.slider.seekRangesF() - local seekRangeLineHeight = innerH / 5 - - if slider_lo.stype ~= "bar" then - foH = elem_geo.h / 2 - else - foH = slider_lo.border + slider_lo.gap - end - - if pos then - xp = get_slider_ele_pos_for(element, pos) - - if slider_lo.stype ~= "bar" then - local r = (user_opts.seekbarhandlesize * innerH) / 2 - ass_draw_rr_h_cw(elem_ass, xp - r, foH - r, - xp + r, foH + r, - r, slider_lo.stype == "diamond") - else - local h = 0 - if seekRanges and user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then - h = seekRangeLineHeight - end - elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h) - - if seekRanges and not user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then - -- Punch holes for the seekRanges to be drawn later - for _,range in pairs(seekRanges) do - if range["start"] < pos then - local pstart = get_slider_ele_pos_for(element, range["start"]) - local pend = xp - - if pos > range["end"] then - pend = get_slider_ele_pos_for(element, range["end"]) - end - elem_ass:rect_ccw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV) - end - end - end - end - - if slider_lo.rtype == "slider" then - ass_draw_rr_h_cw(elem_ass, foH - innerH / 6, foH - innerH / 6, - xp, foH + innerH / 6, - innerH / 6, slider_lo.stype == "diamond", 0) - ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15, - elem_geo.w - foH + innerH / 15, foH + innerH / 15, - 0, slider_lo.stype == "diamond", innerH / 15) - for _,range in pairs(seekRanges or {}) do - local pstart = get_slider_ele_pos_for(element, range["start"]) - local pend = get_slider_ele_pos_for(element, range["end"]) - ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21, - pend, foH + innerH / 21, - innerH / 21, slider_lo.stype == "diamond") - end - end - end - - if seekRanges then - if slider_lo.rtype ~= "inverted" then - elem_ass:draw_stop() - elem_ass:merge(element.style_ass) - ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha) - elem_ass:merge(element.static_ass) - end - - for _,range in pairs(seekRanges) do - local pstart = get_slider_ele_pos_for(element, range["start"]) - local pend = get_slider_ele_pos_for(element, range["end"]) - - if slider_lo.rtype == "slider" then - ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21, - pend, foH + innerH / 21, - innerH / 21, slider_lo.stype == "diamond") - elseif slider_lo.rtype == "line" then - if slider_lo.stype == "bar" then - elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV) - else - ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8, foH - innerH / 8, - pend + innerH / 8, foH + innerH / 8, - innerH / 8, slider_lo.stype == "diamond") - end - elseif slider_lo.rtype == "bar" then - if slider_lo.stype ~= "bar" then - ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV, - pend + innerH / 2, foV + innerH, - innerH / 2, slider_lo.stype == "diamond") - elseif range["end"] >= (pos or 0) then - elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV) - else - elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV) - end - elseif slider_lo.rtype == "inverted" then - if slider_lo.stype ~= "bar" then - ass_draw_rr_h_ccw(elem_ass, pstart, (elem_geo.h / 2) - 1, pend, - (elem_geo.h / 2) + 1, - 1, slider_lo.stype == "diamond") - else - elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1, pend, (elem_geo.h / 2) + 1) - end - end - end - end - - elem_ass:draw_stop() - - -- add tooltip - if not (element.slider.tooltipF == nil) then - - if mouse_hit(element) then - local sliderpos = get_slider_value(element) - local tooltiplabel = element.slider.tooltipF(sliderpos) - - local an = slider_lo.tooltip_an - - local ty - - if (an == 2) then - ty = element.hitbox.y1 - slider_lo.border - else - ty = element.hitbox.y1 + elem_geo.h/2 - end - - local tx = get_virt_mouse_pos() - if (slider_lo.adjust_tooltip) then - if (an == 2) then - if (sliderpos < (s_min + 3)) then - an = an - 1 - elseif (sliderpos > (s_max - 3)) then - an = an + 1 - end - elseif (sliderpos > (s_max-s_min)/2) then - an = an + 1 - tx = tx - 5 - else - an = an - 1 - tx = tx + 10 - end - end - - -- tooltip label - elem_ass:new_event() - elem_ass:pos(tx, ty) - elem_ass:an(an) - elem_ass:append(slider_lo.tooltip_style) - ass_append_alpha(elem_ass, slider_lo.alpha, 0) - elem_ass:append(tooltiplabel) - - -- mpv_thumbnail_script.lua -- - display_thumbnail({x=get_virt_mouse_pos(), y=ty, a=an}, sliderpos, elem_ass) - -- // mpv_thumbnail_script.lua // -- - - end - end - - elseif (element.type == "button") then - - local buttontext - if type(element.content) == "function" then - buttontext = element.content() -- function objects - elseif not (element.content == nil) then - buttontext = element.content -- text objects - end - - local maxchars = element.layout.button.maxchars - if not (maxchars == nil) and (#buttontext > maxchars) then - local max_ratio = 1.25 -- up to 25% more chars while shrinking - local limit = math.max(0, math.floor(maxchars * max_ratio) - 3) - if (#buttontext > limit) then - while (#buttontext > limit) do - buttontext = buttontext:gsub(".[\128-\191]*$", "") - end - buttontext = buttontext .. "..." - end - local _, nchars2 = buttontext:gsub(".[\128-\191]*", "") - local stretch = (maxchars/#buttontext)*100 - buttontext = string.format("{\\fscx%f}", - (maxchars/#buttontext)*100) .. buttontext - end - - elem_ass:append(buttontext) - end - - master_ass:merge(elem_ass) - end -end - --- --- Message display --- - --- pos is 1 based -function limited_list(prop, pos) - local proplist = mp.get_property_native(prop, {}) - local count = #proplist - if count == 0 then - return count, proplist - end - - local fs = tonumber(mp.get_property('options/osd-font-size')) - local max = math.ceil(osc_param.unscaled_y*0.75 / fs) - if max % 2 == 0 then - max = max - 1 - end - local delta = math.ceil(max / 2) - 1 - local begi = math.max(math.min(pos - delta, count - max + 1), 1) - local endi = math.min(begi + max - 1, count) - - local reslist = {} - for i=begi, endi do - local item = proplist[i] - item.current = (i == pos) and true or nil - table.insert(reslist, item) - end - return count, reslist -end - -function get_playlist() - local pos = mp.get_property_number('playlist-pos', 0) + 1 - local count, limlist = limited_list('playlist', pos) - if count == 0 then - return 'Empty playlist.' - end - - local message = string.format('Playlist [%d/%d]:\n', pos, count) - for i, v in ipairs(limlist) do - local title = v.title - local _, filename = utils.split_path(v.filename) - if title == nil then - title = filename - end - message = string.format('%s %s %s\n', message, - (v.current and '●' or '○'), title) - end - return message -end - -function get_chapterlist() - local pos = mp.get_property_number('chapter', 0) + 1 - local count, limlist = limited_list('chapter-list', pos) - if count == 0 then - return 'No chapters.' - end - - local message = string.format('Chapters [%d/%d]:\n', pos, count) - for i, v in ipairs(limlist) do - local time = mp.format_time(v.time) - local title = v.title - if title == nil then - title = string.format('Chapter %02d', i) - end - message = string.format('%s[%s] %s %s\n', message, time, - (v.current and '●' or '○'), title) - end - return message -end - -function show_message(text, duration) - - --print("text: "..text.." duration: " .. duration) - if duration == nil then - duration = tonumber(mp.get_property("options/osd-duration")) / 1000 - elseif not type(duration) == "number" then - print("duration: " .. duration) - end - - -- cut the text short, otherwise the following functions - -- may slow down massively on huge input - text = string.sub(text, 0, 4000) - - -- replace actual linebreaks with ASS linebreaks - text = string.gsub(text, "\n", "\\N") - - state.message_text = text - state.message_timeout = mp.get_time() + duration -end - -function render_message(ass) - if not(state.message_timeout == nil) and not(state.message_text == nil) - and state.message_timeout > mp.get_time() then - local _, lines = string.gsub(state.message_text, "\\N", "") - - local fontsize = tonumber(mp.get_property("options/osd-font-size")) - local outline = tonumber(mp.get_property("options/osd-border-size")) - local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize) - local counterscale = osc_param.playresy / osc_param.unscaled_y - - fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1) - outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1) - - local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}" - - - ass:new_event() - ass:append(style .. state.message_text) - else - state.message_text = nil - state.message_timeout = nil - end -end - --- --- Initialisation and Layout --- - -function new_element(name, type) - elements[name] = {} - elements[name].type = type - - -- add default stuff - elements[name].eventresponder = {} - elements[name].visible = true - elements[name].enabled = true - elements[name].softrepeat = false - elements[name].styledown = (type == "button") - elements[name].state = {} - - if (type == "slider") then - elements[name].slider = {min = {value = 0}, max = {value = 100}} - end - - - return elements[name] -end - -function add_layout(name) - if not (elements[name] == nil) then - -- new layout - elements[name].layout = {} - - -- set layout defaults - elements[name].layout.layer = 50 - elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255} - - if (elements[name].type == "button") then - elements[name].layout.button = { - maxchars = nil, - } - elseif (elements[name].type == "slider") then - -- slider defaults - elements[name].layout.slider = { - border = 1, - gap = 1, - nibbles_top = true, - nibbles_bottom = true, - stype = "slider", - adjust_tooltip = true, - tooltip_style = "", - tooltip_an = 2, - alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255}, - } - elseif (elements[name].type == "box") then - elements[name].layout.box = {radius = 0} - end - - return elements[name].layout - else - msg.error("Can't add_layout to element \""..name.."\", doesn't exist.") - end -end - --- Window Controls -function window_controls(topbar) - local wc_geo = { - x = 0, - y = 30 + user_opts.barmargin, - an = 1, - w = osc_param.playresx, - h = 30, - } - - local alignment = window_controls_alignment() - local controlbox_w = window_control_box_width - local titlebox_w = wc_geo.w - controlbox_w - - -- Default alignment is "right" - local controlbox_left = wc_geo.w - controlbox_w - local titlebox_left = wc_geo.x + 5 - - if alignment == "left" then - controlbox_left = wc_geo.x - titlebox_left = wc_geo.x + controlbox_w + 5 - end - - add_area("window-controls", - get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an, - controlbox_w, wc_geo.h)) - - local lo - - -- Background Bar - new_element("wcbar", "box") - lo = add_layout("wcbar") - lo.geometry = wc_geo - lo.layer = 10 - lo.style = osc_styles.wcBar - lo.alpha[1] = user_opts.boxalpha - - local button_y = wc_geo.y - (wc_geo.h / 2) - local first_geo = - {x = controlbox_left + 5, y = button_y, an = 4, w = 25, h = 25} - local second_geo = - {x = controlbox_left + 30, y = button_y, an = 4, w = 25, h = 25} - local third_geo = - {x = controlbox_left + 55, y = button_y, an = 4, w = 25, h = 25} - - -- Window control buttons use symbols in the custom mpv osd font - -- because the official unicode codepoints are sufficiently - -- exotic that a system might lack an installed font with them, - -- and libass will complain that they are not present in the - -- default font, even if another font with them is available. - - -- Close: 🗙 - ne = new_element("close", "button") - ne.content = "\238\132\149" - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("quit") end - lo = add_layout("close") - lo.geometry = alignment == "left" and first_geo or third_geo - lo.style = osc_styles.wcButtons - - -- Minimize: 🗕 - ne = new_element("minimize", "button") - ne.content = "\238\132\146" - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "window-minimized") end - lo = add_layout("minimize") - lo.geometry = alignment == "left" and second_geo or first_geo - lo.style = osc_styles.wcButtons - - -- Maximize: 🗖 /🗗 - ne = new_element("maximize", "button") - if state.maximized then - ne.content = "\238\132\148" - else - ne.content = "\238\132\147" - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "window-maximized") end - lo = add_layout("maximize") - lo.geometry = alignment == "left" and third_geo or second_geo - lo.style = osc_styles.wcButtons - - -- deadzone below window controls - local sh_area_y0, sh_area_y1 - sh_area_y0 = user_opts.barmargin - sh_area_y1 = (wc_geo.y + (wc_geo.h / 2)) + - get_align(1 - (2 * user_opts.deadzonesize), - osc_param.playresy - (wc_geo.y + (wc_geo.h / 2)), 0, 0) - add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1) - - if topbar then - -- The title is already there as part of the top bar - return - else - -- Apply boxvideo margins to the control bar - osc_param.video_margins.t = wc_geo.h / osc_param.playresy - end - - -- Window Title - ne = new_element("wctitle", "button") - ne.content = function () - local title = mp.command_native({"expand-text", user_opts.title}) - -- escape ASS, and strip newlines and trailing slashes - title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{") - return not (title == "") and title or "mpv" - end - lo = add_layout("wctitle") - lo.geometry = - { x = titlebox_left, y = wc_geo.y - 3, an = 1, w = titlebox_w, h = wc_geo.h } - lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", - osc_styles.wcTitle, - titlebox_left, wc_geo.y - wc_geo.h, titlebox_w, wc_geo.y + wc_geo.h) -end - --- --- Layouts --- - -local layouts = {} - --- Classic box layout -layouts["box"] = function () - - local osc_geo = { - w = 550, -- width - h = 138, -- height - r = 10, -- corner-radius - p = 15, -- padding - } - - -- make sure the OSC actually fits into the video - if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then - osc_param.playresy = (osc_geo.w+(2*osc_geo.p))/osc_param.display_aspect - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - end - - -- position of the controller according to video aspect and valignment - local posX = math.floor(get_align(user_opts.halign, osc_param.playresx, - osc_geo.w, 0)) - local posY = math.floor(get_align(user_opts.valign, osc_param.playresy, - osc_geo.h, 0)) - - -- position offset for contents aligned at the borders of the box - local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2 - local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2 - - osc_param.areas = {} -- delete areas - - -- area for active mouse input - add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h)) - - -- area for show/hide - local sh_area_y0, sh_area_y1 - if user_opts.valign > 0 then - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - posY - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - else - -- deadzone below OSC - sh_area_y0 = 0 - sh_area_y1 = (posY + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0) - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - -- fetch values - local osc_w, osc_h, osc_r, osc_p = - osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p - - local lo - - -- - -- Background box - -- - - new_element("bgbox", "box") - lo = add_layout("bgbox") - - lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h} - lo.layer = 10 - lo.style = osc_styles.box - lo.alpha[1] = user_opts.boxalpha - lo.alpha[3] = user_opts.boxalpha - lo.box.radius = osc_r - - -- - -- Title row - -- - - local titlerowY = posY - pos_offsetY - 10 - - lo = add_layout("title") - lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12} - lo.style = osc_styles.vidtitle - lo.button.maxchars = user_opts.boxmaxchars - - lo = add_layout("pl_prev") - lo.geometry = - {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12} - lo.style = osc_styles.topButtons - - lo = add_layout("pl_next") - lo.geometry = - {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12} - lo.style = osc_styles.topButtons - - -- - -- Big buttons - -- - - local bigbtnrowY = posY - pos_offsetY + 35 - local bigbtndist = 60 - - lo = add_layout("playpause") - lo.geometry = - {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("skipback") - lo.geometry = - {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("skipfrwd") - lo.geometry = - {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("ch_prev") - lo.geometry = - {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("ch_next") - lo.geometry = - {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bigButtons - - lo = add_layout("cy_audio") - lo.geometry = - {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18} - lo.style = osc_styles.smallButtonsL - - lo = add_layout("cy_sub") - lo.geometry = - {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18} - lo.style = osc_styles.smallButtonsL - - lo = add_layout("tog_fs") - lo.geometry = - {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25} - lo.style = osc_styles.smallButtonsR - - lo = add_layout("volume") - lo.geometry = - {x = posX+pos_offsetX - (25 * 2) - osc_geo.p, - y = bigbtnrowY, an = 4, w = 25, h = 25} - lo.style = osc_styles.smallButtonsR - - -- - -- Seekbar - -- - - lo = add_layout("seekbar") - lo.geometry = - {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15} - lo.style = osc_styles.timecodes - lo.slider.tooltip_style = osc_styles.vidtitle - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - - -- - -- Timecodes + Cache - -- - - local bottomrowY = posY + pos_offsetY - 5 - - lo = add_layout("tc_left") - lo.geometry = - {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18} - lo.style = osc_styles.timecodes - - lo = add_layout("tc_right") - lo.geometry = - {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18} - lo.style = osc_styles.timecodes - - lo = add_layout("cache") - lo.geometry = - {x = posX, y = bottomrowY, an = 5, w = 110, h = 18} - lo.style = osc_styles.timecodes - -end - --- slim box layout -layouts["slimbox"] = function () - - local osc_geo = { - w = 660, -- width - h = 70, -- height - r = 10, -- corner-radius - } - - -- make sure the OSC actually fits into the video - if (osc_param.playresx < (osc_geo.w)) then - osc_param.playresy = (osc_geo.w)/osc_param.display_aspect - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - end - - -- position of the controller according to video aspect and valignment - local posX = math.floor(get_align(user_opts.halign, osc_param.playresx, - osc_geo.w, 0)) - local posY = math.floor(get_align(user_opts.valign, osc_param.playresy, - osc_geo.h, 0)) - - osc_param.areas = {} -- delete areas - - -- area for active mouse input - add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h)) - - -- area for show/hide - local sh_area_y0, sh_area_y1 - if user_opts.valign > 0 then - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - posY - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - else - -- deadzone below OSC - sh_area_y0 = 0 - sh_area_y1 = (posY + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0) - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - local lo - - local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100 - - -- styles - local styles = { - box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}", - timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}", - tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}", - } - - - new_element("bgbox", "box") - lo = add_layout("bgbox") - - lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h} - lo.layer = 10 - lo.style = osc_styles.box - lo.alpha[1] = user_opts.boxalpha - lo.alpha[3] = 0 - if not (user_opts["seekbarstyle"] == "bar") then - lo.box.radius = osc_geo.r - lo.box.hexagon = user_opts["seekbarstyle"] == "diamond" - end - - - lo = add_layout("seekbar") - lo.geometry = - {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h} - lo.style = osc_styles.timecodes - lo.slider.border = 0 - lo.slider.gap = 1.5 - lo.slider.tooltip_style = styles.tooltip - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - lo.slider.adjust_tooltip = false - - -- - -- Timecodes - -- - - lo = add_layout("tc_left") - lo.geometry = - {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1, - an = 7, w = tc_w, h = ele_h} - lo.style = styles.timecodes - lo.alpha[3] = user_opts.boxalpha - - lo = add_layout("tc_right") - lo.geometry = - {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1, - an = 9, w = tc_w, h = ele_h} - lo.style = styles.timecodes - lo.alpha[3] = user_opts.boxalpha - - -- Cache - - lo = add_layout("cache") - lo.geometry = - {x = posX, y = posY + 1, - an = 8, w = tc_w, h = ele_h} - lo.style = styles.timecodes - lo.alpha[3] = user_opts.boxalpha - - -end - -function bar_layout(direction) - local osc_geo = { - x = -2, - y, - an = (direction < 0) and 7 or 1, - w, - h = 56, - } - - local padX = 9 - local padY = 3 - local buttonW = 27 - local tcW = (state.tc_ms) and 170 or 110 - local tsW = 90 - local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2 - - -- Special topbar handling when window controls are present - local padwc_l - local padwc_r - if direction < 0 or not window_controls_enabled() then - padwc_l = 0 - padwc_r = 0 - elseif window_controls_alignment() == "left" then - padwc_l = window_control_box_width - padwc_r = 0 - else - padwc_l = 0 - padwc_r = window_control_box_width - end - - if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then - osc_param.playresy = minW / osc_param.display_aspect - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - end - - osc_geo.y = direction * (54 + user_opts.barmargin) - osc_geo.w = osc_param.playresx + 4 - if direction < 0 then - osc_geo.y = osc_geo.y + osc_param.playresy - end - - local line1 = osc_geo.y - direction * (9 + padY) - local line2 = osc_geo.y - direction * (36 + padY) - - osc_param.areas = {} - - add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an, - osc_geo.w, osc_geo.h)) - - local sh_area_y0, sh_area_y1 - if direction > 0 then - -- deadzone below OSC - sh_area_y0 = user_opts.barmargin - sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (osc_geo.y + (osc_geo.h / 2)), 0, 0) - else - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - osc_geo.y - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - user_opts.barmargin - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - local lo, geo - - -- Background bar - new_element("bgbox", "box") - lo = add_layout("bgbox") - - lo.geometry = osc_geo - lo.layer = 10 - lo.style = osc_styles.box - lo.alpha[1] = user_opts.boxalpha - - - -- Playlist prev/next - geo = { x = osc_geo.x + padX, y = line1, - an = 4, w = 18, h = 18 - padY } - lo = add_layout("pl_prev") - lo.geometry = geo - lo.style = osc_styles.topButtonsBar - - geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("pl_next") - lo.geometry = geo - lo.style = osc_styles.topButtonsBar - - local t_l = geo.x + geo.w + padX - - -- Cache - geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y, - an = 6, w = 150, h = geo.h } - lo = add_layout("cache") - lo.geometry = geo - lo.style = osc_styles.vidtitleBar - - local t_r = geo.x - geo.w - padX*2 - - -- Title - geo = { x = t_l, y = geo.y, an = 4, - w = t_r - t_l, h = geo.h } - lo = add_layout("title") - lo.geometry = geo - lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", - osc_styles.vidtitleBar, - geo.x, geo.y-geo.h, geo.w, geo.y+geo.h) - - - -- Playback control buttons - geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4, - w = buttonW, h = 36 - padY*2} - lo = add_layout("playpause") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("ch_prev") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("ch_next") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - -- Left timecode - geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6, - w = tcW, h = geo.h } - lo = add_layout("tc_left") - lo.geometry = geo - lo.style = osc_styles.timecodesBar - - local sb_l = geo.x + padX - - -- Fullscreen button - geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4, - w = buttonW, h = geo.h } - lo = add_layout("tog_fs") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - -- Volume - geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("volume") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - -- Track selection buttons - geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h } - lo = add_layout("cy_sub") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h } - lo = add_layout("cy_audio") - lo.geometry = geo - lo.style = osc_styles.smallButtonsBar - - - -- Right timecode - geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an, - w = tcW, h = geo.h } - lo = add_layout("tc_right") - lo.geometry = geo - lo.style = osc_styles.timecodesBar - - local sb_r = geo.x - padX - - - -- Seekbar - geo = { x = sb_l, y = geo.y, an = geo.an, - w = math.max(0, sb_r - sb_l), h = geo.h } - new_element("bgbar1", "box") - lo = add_layout("bgbar1") - - lo.geometry = geo - lo.layer = 15 - lo.style = osc_styles.timecodesBar - lo.alpha[1] = - math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8) - if not (user_opts["seekbarstyle"] == "bar") then - lo.box.radius = geo.h / 2 - lo.box.hexagon = user_opts["seekbarstyle"] == "diamond" - end - - lo = add_layout("seekbar") - lo.geometry = geo - lo.style = osc_styles.timecodesBar - lo.slider.border = 0 - lo.slider.gap = 2 - lo.slider.tooltip_style = osc_styles.timePosBar - lo.slider.tooltip_an = 5 - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - - if direction < 0 then - osc_param.video_margins.b = osc_geo.h / osc_param.playresy - else - osc_param.video_margins.t = osc_geo.h / osc_param.playresy - end -end - -layouts["bottombar"] = function() - bar_layout(-1) -end - -layouts["topbar"] = function() - bar_layout(1) -end - --- Validate string type user options -function validate_user_opts() - if layouts[user_opts.layout] == nil then - msg.warn("Invalid setting \""..user_opts.layout.."\" for layout") - user_opts.layout = "bottombar" - end - - if user_opts.seekbarstyle ~= "bar" and - user_opts.seekbarstyle ~= "diamond" and - user_opts.seekbarstyle ~= "knob" then - msg.warn("Invalid setting \"" .. user_opts.seekbarstyle - .. "\" for seekbarstyle") - user_opts.seekbarstyle = "bar" - end - - if user_opts.seekrangestyle ~= "bar" and - user_opts.seekrangestyle ~= "line" and - user_opts.seekrangestyle ~= "slider" and - user_opts.seekrangestyle ~= "inverted" and - user_opts.seekrangestyle ~= "none" then - msg.warn("Invalid setting \"" .. user_opts.seekrangestyle - .. "\" for seekrangestyle") - user_opts.seekrangestyle = "inverted" - end - - if user_opts.seekrangestyle == "slider" and - user_opts.seekbarstyle == "bar" then - msg.warn("Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported") - user_opts.seekrangestyle = "inverted" - end -end - - --- OSC INIT -function osc_init() - msg.debug("osc_init") - - -- set canvas resolution according to display aspect and scaling setting - local baseResY = 720 - local display_w, display_h, display_aspect = mp.get_osd_size() - local scale = 1 - - if (mp.get_property("video") == "no") then -- dummy/forced window - scale = user_opts.scaleforcedwindow - elseif state.fullscreen then - scale = user_opts.scalefullscreen - else - scale = user_opts.scalewindowed - end - - if user_opts.vidscale then - osc_param.unscaled_y = baseResY - else - osc_param.unscaled_y = display_h - end - osc_param.playresy = osc_param.unscaled_y / scale - if (display_aspect > 0) then - osc_param.display_aspect = display_aspect - end - osc_param.playresx = osc_param.playresy * osc_param.display_aspect - - -- stop seeking with the slider to prevent skipping files - state.active_element = nil - - - - - elements = {} - - -- some often needed stuff - local pl_count = mp.get_property_number("playlist-count", 0) - local have_pl = (pl_count > 1) - local pl_pos = mp.get_property_number("playlist-pos", 0) + 1 - local have_ch = (mp.get_property_number("chapters", 0) > 0) - local loop = mp.get_property("loop-playlist", "no") - - local ne - - -- title - ne = new_element("title", "button") - - ne.content = function () - local title = mp.command_native({"expand-text", user_opts.title}) - -- escape ASS, and strip newlines and trailing slashes - title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{") - return not (title == "") and title or "mpv" - end - - ne.eventresponder["mbtn_left_up"] = function () - local title = mp.get_property_osd("media-title") - if (have_pl) then - title = string.format("[%d/%d] %s", countone(pl_pos - 1), - pl_count, title) - end - show_message(title) - end - - ne.eventresponder["mbtn_right_up"] = - function () show_message(mp.get_property_osd("filename")) end - - -- playlist buttons - - -- prev - ne = new_element("pl_prev", "button") - - ne.content = "\238\132\144" - ne.enabled = (pl_pos > 1) or (loop ~= "no") - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("playlist-prev", "weak") - show_message(get_playlist(), 3) - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_playlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_playlist(), 3) end - - --next - ne = new_element("pl_next", "button") - - ne.content = "\238\132\129" - ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no") - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("playlist-next", "weak") - show_message(get_playlist(), 3) - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_playlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_playlist(), 3) end - - - -- big buttons - - --playpause - ne = new_element("playpause", "button") - - ne.content = function () - if mp.get_property("pause") == "yes" then - return ("\238\132\129") - else - return ("\238\128\130") - end - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "pause") end - - --skipback - ne = new_element("skipback", "button") - - ne.softrepeat = true - ne.content = "\238\128\132" - ne.eventresponder["mbtn_left_down"] = - function () mp.commandv("seek", -5, "relative", "keyframes") end - ne.eventresponder["shift+mbtn_left_down"] = - function () mp.commandv("frame-back-step") end - ne.eventresponder["mbtn_right_down"] = - function () mp.commandv("seek", -30, "relative", "keyframes") end - - --skipfrwd - ne = new_element("skipfrwd", "button") - - ne.softrepeat = true - ne.content = "\238\128\133" - ne.eventresponder["mbtn_left_down"] = - function () mp.commandv("seek", 10, "relative", "keyframes") end - ne.eventresponder["shift+mbtn_left_down"] = - function () mp.commandv("frame-step") end - ne.eventresponder["mbtn_right_down"] = - function () mp.commandv("seek", 60, "relative", "keyframes") end - - --ch_prev - ne = new_element("ch_prev", "button") - - ne.enabled = have_ch - ne.content = "\238\132\132" - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("add", "chapter", -1) - show_message(get_chapterlist(), 3) - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_chapterlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_chapterlist(), 3) end - - --ch_next - ne = new_element("ch_next", "button") - - ne.enabled = have_ch - ne.content = "\238\132\133" - ne.eventresponder["mbtn_left_up"] = - function () - mp.commandv("add", "chapter", 1) - show_message(get_chapterlist(), 3) - end - ne.eventresponder["shift+mbtn_left_up"] = - function () show_message(get_chapterlist(), 3) end - ne.eventresponder["mbtn_right_up"] = - function () show_message(get_chapterlist(), 3) end - - -- - update_tracklist() - - --cy_audio - ne = new_element("cy_audio", "button") - - ne.enabled = (#tracks_osc.audio > 0) - ne.content = function () - local aid = "–" - if not (get_track("audio") == 0) then - aid = get_track("audio") - end - return ("\238\132\134" .. osc_styles.smallButtonsLlabel - .. " " .. aid .. "/" .. #tracks_osc.audio) - end - ne.eventresponder["mbtn_left_up"] = - function () set_track("audio", 1) end - ne.eventresponder["mbtn_right_up"] = - function () set_track("audio", -1) end - ne.eventresponder["shift+mbtn_left_down"] = - function () show_message(get_tracklist("audio"), 2) end - - --cy_sub - ne = new_element("cy_sub", "button") - - ne.enabled = (#tracks_osc.sub > 0) - ne.content = function () - local sid = "–" - if not (get_track("sub") == 0) then - sid = get_track("sub") - end - return ("\238\132\135" .. osc_styles.smallButtonsLlabel - .. " " .. sid .. "/" .. #tracks_osc.sub) - end - ne.eventresponder["mbtn_left_up"] = - function () set_track("sub", 1) end - ne.eventresponder["mbtn_right_up"] = - function () set_track("sub", -1) end - ne.eventresponder["shift+mbtn_left_down"] = - function () show_message(get_tracklist("sub"), 2) end - - --tog_fs - ne = new_element("tog_fs", "button") - ne.content = function () - if (state.fullscreen) then - return ("\238\132\137") - else - return ("\238\132\136") - end - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "fullscreen") end - - --seekbar - ne = new_element("seekbar", "slider") - - ne.enabled = not (mp.get_property("percent-pos") == nil) - ne.slider.markerF = function () - local duration = mp.get_property_number("duration", nil) - if not (duration == nil) then - local chapters = mp.get_property_native("chapter-list", {}) - local markers = {} - for n = 1, #chapters do - markers[n] = (chapters[n].time / duration * 100) - end - return markers - else - return {} - end - end - ne.slider.posF = - function () return mp.get_property_number("percent-pos", nil) end - ne.slider.tooltipF = function (pos) - local duration = mp.get_property_number("duration", nil) - if not ((duration == nil) or (pos == nil)) then - possec = duration * (pos / 100) - return mp.format_time(possec) - else - return "" - end - end - ne.slider.seekRangesF = function() - if user_opts.seekrangestyle == "none" then - return nil - end - local cache_state = state.cache_state - if not cache_state then - return nil - end - local duration = mp.get_property_number("duration", nil) - if (duration == nil) or duration <= 0 then - return nil - end - local ranges = cache_state["seekable-ranges"] - if #ranges == 0 then - return nil - end - local nranges = {} - for _, range in pairs(ranges) do - nranges[#nranges + 1] = { - ["start"] = 100 * range["start"] / duration, - ["end"] = 100 * range["end"] / duration, - } - end - return nranges - end - ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged - function (element) - -- mouse move events may pile up during seeking and may still get - -- sent when the user is done seeking, so we need to throw away - -- identical seeks - local seekto = get_slider_value(element) - if (element.state.lastseek == nil) or - (not (element.state.lastseek == seekto)) then - mp.commandv("seek", seekto, "absolute-percent", - user_opts.seekbarkeyframes and "keyframes" or "exact") - element.state.lastseek = seekto - end - - end - ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks - function (element) mp.commandv("seek", get_slider_value(element), - "absolute-percent", "exact") end - ne.eventresponder["reset"] = - function (element) element.state.lastseek = nil end - - - -- tc_left (current pos) - ne = new_element("tc_left", "button") - - ne.content = function () - if (state.tc_ms) then - return (mp.get_property_osd("playback-time/full")) - else - return (mp.get_property_osd("playback-time")) - end - end - ne.eventresponder["mbtn_left_up"] = function () - state.tc_ms = not state.tc_ms - request_init() - end - - -- tc_right (total/remaining time) - ne = new_element("tc_right", "button") - - ne.visible = (mp.get_property_number("duration", 0) > 0) - ne.content = function () - if (state.rightTC_trem) then - if state.tc_ms then - return ("-"..mp.get_property_osd("playtime-remaining/full")) - else - return ("-"..mp.get_property_osd("playtime-remaining")) - end - else - if state.tc_ms then - return (mp.get_property_osd("duration/full")) - else - return (mp.get_property_osd("duration")) - end - end - end - ne.eventresponder["mbtn_left_up"] = - function () state.rightTC_trem = not state.rightTC_trem end - - -- cache - ne = new_element("cache", "button") - - ne.content = function () - local cache_state = state.cache_state - if not (cache_state and cache_state["seekable-ranges"] and - #cache_state["seekable-ranges"] > 0) then - -- probably not a network stream - return "" - end - local dmx_cache = mp.get_property_number("demuxer-cache-duration") - if dmx_cache and (dmx_cache > state.dmx_cache * 1.1 or - dmx_cache < state.dmx_cache * 0.9) then - state.dmx_cache = dmx_cache - else - dmx_cache = state.dmx_cache - end - local min = math.floor(dmx_cache / 60) - local sec = dmx_cache % 60 - return "Cache: " .. (min > 0 and - string.format("%sm%02.0fs", min, sec) or - string.format("%3.0fs", dmx_cache)) - end - - -- volume - ne = new_element("volume", "button") - - ne.content = function() - local volume = mp.get_property_number("volume", 0) - local mute = mp.get_property_native("mute") - local volicon = {"\238\132\139", "\238\132\140", - "\238\132\141", "\238\132\142"} - if volume == 0 or mute then - return "\238\132\138" - else - return volicon[math.min(4,math.ceil(volume / (100/3)))] - end - end - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("cycle", "mute") end - - ne.eventresponder["wheel_up_press"] = - function () mp.commandv("osd-auto", "add", "volume", 5) end - ne.eventresponder["wheel_down_press"] = - function () mp.commandv("osd-auto", "add", "volume", -5) end - - - -- load layout - layouts[user_opts.layout]() - - -- load window controls - if window_controls_enabled() then - window_controls(user_opts.layout == "topbar") - end - - --do something with the elements - prepare_elements() - - if user_opts.boxvideo then - -- check whether any margin option has a non-default value - local margins_used = false - - for _, opt in ipairs(margins_opts) do - if mp.get_property_number(opt[2], 0.0) ~= 0.0 then - margins_used = true - end - end - - if not margins_used then - local margins = osc_param.video_margins - for _, opt in ipairs(margins_opts) do - local v = margins[opt[1]] - if v ~= 0 then - mp.set_property_number(opt[2], v) - state.using_video_margins = true - end - end - end - else - reset_margins() - end - - update_margins() -end - -function reset_margins() - if state.using_video_margins then - for _, opt in ipairs(margins_opts) do - mp.set_property_number(opt[2], 0.0) - end - state.using_video_margins = false - end -end - -function update_margins() - local margins = osc_param.video_margins - - -- Don't report margins if it's visible only temporarily. At least for - -- console.lua this makes no sense. - if (not state.osc_visible) or (user_opts.hidetimeout >= 0) then - margins = {l = 0, r = 0, t = 0, b = 0} - end - - utils.shared_script_property_set("osc-margins", - string.format("%f,%f,%f,%f", margins.l, margins.r, margins.t, margins.b)) -end - -function shutdown() - reset_margins() - utils.shared_script_property_set("osc-margins", nil) -end - --- --- Other important stuff --- - - -function show_osc() - -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding - if not state.enabled then return end - - msg.trace("show_osc") - --remember last time of invocation (mouse move) - state.showtime = mp.get_time() - - osc_visible(true) - - if (user_opts.fadeduration > 0) then - state.anitype = nil - end -end - -function hide_osc() - msg.trace("hide_osc") - if not state.enabled then - -- typically hide happens at render() from tick(), but now tick() is - -- no-op and won't render again to remove the osc, so do that manually. - state.osc_visible = false - render_wipe() - elseif (user_opts.fadeduration > 0) then - if not(state.osc_visible == false) then - state.anitype = "out" - request_tick() - end - else - osc_visible(false) - end -end - -function osc_visible(visible) - if state.osc_visible ~= visible then - state.osc_visible = visible - update_margins() - end - request_tick() -end - -function pause_state(name, enabled) - state.paused = enabled - request_tick() -end - -function cache_state(name, st) - state.cache_state = st - request_tick() -end - --- Request that tick() is called (which typically re-renders the OSC). --- The tick is then either executed immediately, or rate-limited if it was --- called a small time ago. -function request_tick() - if state.tick_timer == nil then - state.tick_timer = mp.add_timeout(0, tick) - end - - if not state.tick_timer:is_enabled() then - local now = mp.get_time() - local timeout = tick_delay - (now - state.tick_last_time) - if timeout < 0 then - timeout = 0 - end - state.tick_timer.timeout = timeout - state.tick_timer:resume() - end -end - -function mouse_leave() - if user_opts.hidetimeout >= 0 then - hide_osc() - end - -- reset mouse position - state.last_mouseX, state.last_mouseY = nil, nil -end - -function request_init() - state.initREQ = true -end - -function render_wipe() - msg.trace("render_wipe()") - mp.set_osd_ass(0, 0, "{}") -end - -function render() - msg.trace("rendering") - local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size() - local mouseX, mouseY = get_virt_mouse_pos() - local now = mp.get_time() - - -- check if display changed, if so request reinit - if not (state.mp_screen_sizeX == current_screen_sizeX - and state.mp_screen_sizeY == current_screen_sizeY) then - - request_init() - - state.mp_screen_sizeX = current_screen_sizeX - state.mp_screen_sizeY = current_screen_sizeY - end - - -- init management - if state.initREQ then - osc_init() - state.initREQ = false - - -- store initial mouse position - if (state.last_mouseX == nil or state.last_mouseY == nil) - and not (mouseX == nil or mouseY == nil) then - - state.last_mouseX, state.last_mouseY = mouseX, mouseY - end - end - - - -- fade animation - if not(state.anitype == nil) then - - if (state.anistart == nil) then - state.anistart = now - end - - if (now < state.anistart + (user_opts.fadeduration/1000)) then - - if (state.anitype == "in") then --fade in - osc_visible(true) - state.animation = scale_value(state.anistart, - (state.anistart + (user_opts.fadeduration/1000)), - 255, 0, now) - elseif (state.anitype == "out") then --fade out - state.animation = scale_value(state.anistart, - (state.anistart + (user_opts.fadeduration/1000)), - 0, 255, now) - end - - else - if (state.anitype == "out") then - osc_visible(false) - end - state.anistart = nil - state.animation = nil - state.anitype = nil - end - else - state.anistart = nil - state.animation = nil - state.anitype = nil - end - - --mouse show/hide area - for k,cords in pairs(osc_param.areas["showhide"]) do - set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide") - end - if osc_param.areas["showhide_wc"] then - for k,cords in pairs(osc_param.areas["showhide_wc"]) do - set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc") - end - else - set_virt_mouse_area(0, 0, 0, 0, "showhide_wc") - end - do_enable_keybindings() - - --mouse input area - local mouse_over_osc = false - - for _,cords in ipairs(osc_param.areas["input"]) do - if state.osc_visible then -- activate only when OSC is actually visible - set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input") - end - if state.osc_visible ~= state.input_enabled then - if state.osc_visible then - mp.enable_key_bindings("input") - else - mp.disable_key_bindings("input") - end - state.input_enabled = state.osc_visible - end - - if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then - mouse_over_osc = true - end - end - - if osc_param.areas["window-controls"] then - for _,cords in ipairs(osc_param.areas["window-controls"]) do - if state.osc_visible then -- activate only when OSC is actually visible - set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls") - mp.enable_key_bindings("window-controls") - else - mp.disable_key_bindings("window-controls") - end - - if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then - mouse_over_osc = true - end - end - end - - -- autohide - if not (state.showtime == nil) and (user_opts.hidetimeout >= 0) - and (state.showtime + (user_opts.hidetimeout/1000) < now) - and (state.active_element == nil) and not (mouse_over_osc) then - - hide_osc() - end - - - -- actual rendering - local ass = assdraw.ass_new() - - -- Messages - render_message(ass) - - -- mpv_thumbnail_script.lua -- - local thumb_was_visible = osc_thumb_state.visible - osc_thumb_state.visible = false - -- // mpv_thumbnail_script.lua // -- - - -- actual OSC - if state.osc_visible then - render_elements(ass) - end - - -- mpv_thumbnail_script.lua -- - if not osc_thumb_state.visible and thumb_was_visible then - hide_thumbnail() - end - -- // mpv_thumbnail_script.lua // -- - - -- submit - mp.set_osd_ass(osc_param.playresy * osc_param.display_aspect, - osc_param.playresy, ass.text) - - - - -end - --- --- Eventhandling --- - -local function element_has_action(element, action) - return element and element.eventresponder and - element.eventresponder[action] -end - -function process_event(source, what) - local action = string.format("%s%s", source, - what and ("_" .. what) or "") - - if what == "down" or what == "press" then - - for n = 1, #elements do - - if mouse_hit(elements[n]) and - elements[n].eventresponder and - (elements[n].eventresponder[source .. "_up"] or - elements[n].eventresponder[action]) then - - if what == "down" then - state.active_element = n - state.active_event_source = source - end - -- fire the down or press event if the element has one - if element_has_action(elements[n], action) then - elements[n].eventresponder[action](elements[n]) - end - - end - end - - elseif what == "up" then - - if elements[state.active_element] then - local n = state.active_element - - if n == 0 then - --click on background (does not work) - elseif element_has_action(elements[n], action) and - mouse_hit(elements[n]) then - - elements[n].eventresponder[action](elements[n]) - end - - --reset active element - if element_has_action(elements[n], "reset") then - elements[n].eventresponder["reset"](elements[n]) - end - - end - state.active_element = nil - state.mouse_down_counter = 0 - - elseif source == "mouse_move" then - - local mouseX, mouseY = get_virt_mouse_pos() - if (user_opts.minmousemove == 0) or - (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and - ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove) - or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove) - ) - ) then - show_osc() - end - state.last_mouseX, state.last_mouseY = mouseX, mouseY - - local n = state.active_element - if element_has_action(elements[n], action) then - elements[n].eventresponder[action](elements[n]) - end - request_tick() - end -end - --- called by mpv on every frame -function tick() - if (not state.enabled) then return end - - if (state.idle) then - - -- render idle message - msg.trace("idle message") - local icon_x, icon_y = 320 - 26, 140 - - local ass = assdraw.ass_new() - ass:new_event() - ass:pos(icon_x, icon_y) - ass:append("{\\rDefault\\an7\\c&H430142&\\1a&H00&\\bord0\\shad0\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}") - ass:new_event() - ass:pos(icon_x, icon_y) - ass:append("{\\rDefault\\an7\\c&HDDDBDD&\\1a&H00&\\bord0\\shad0\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}") - ass:new_event() - ass:pos(icon_x, icon_y) - ass:append("{\\rDefault\\an7\\c&H691F69&\\1a&H00&\\bord0\\shad0\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}") - ass:new_event() - ass:pos(icon_x, icon_y) - ass:append("{\\rDefault\\an7\\c&H682167&\\1a&H00&\\bord0\\shad0\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42 m 925 42 m 977 200 b 1324 200 1605 482 1605 828 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200{\\p0}") - ass:new_event() - ass:pos(icon_x, icon_y) - ass:append("{\\rDefault\\an7\\c&H753074&\\1a&H00&\\bord0\\shad0\\p6}m 977 198 b 630 198 348 480 348 828 348 1176 630 1458 977 1458 1325 1458 1607 1176 1607 828 1607 480 1325 198 977 198 m 977 198 m 977 202 b 1323 202 1604 483 1604 828 1604 1174 1323 1454 977 1454 632 1454 351 1174 351 828 351 483 632 202 977 202{\\p0}") - ass:new_event() - ass:pos(icon_x, icon_y) - ass:append("{\\rDefault\\an7\\c&HE5E5E5&\\1a&H00&\\bord0\\shad0\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 m 895 10 m 925 42 b 1388 42 1763 418 1763 880 1763 1343 1388 1718 925 1718 463 1718 87 1343 87 880 87 418 463 42 925 42{\\p0}") - ass:new_event() - ass:pos(320, icon_y+65) - ass:an(8) - ass:append("Drop files or URLs to play here.") - mp.set_osd_ass(640, 360, ass.text) - - if state.showhide_enabled then - mp.disable_key_bindings("showhide") - mp.disable_key_bindings("showhide_wc") - state.showhide_enabled = false - end - - - elseif (state.fullscreen and user_opts.showfullscreen) - or (not state.fullscreen and user_opts.showwindowed) then - - -- render the OSC - render() - else - -- Flush OSD - mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "") - end - - state.tick_last_time = mp.get_time() - - if state.anitype ~= nil then - request_tick() - end -end - -function do_enable_keybindings() - if state.enabled then - if not state.showhide_enabled then - mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor") - mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor") - end - state.showhide_enabled = true - end -end - -function enable_osc(enable) - state.enabled = enable - if enable then - do_enable_keybindings() - else - hide_osc() -- acts immediately when state.enabled == false - if state.showhide_enabled then - mp.disable_key_bindings("showhide") - mp.disable_key_bindings("showhide_wc") - end - state.showhide_enabled = false - end -end - --- mpv_thumbnail_script.lua -- - -local builtin_osc_enabled = mp.get_property_native('osc') -if builtin_osc_enabled then - local err = "You must disable the built-in OSC with osc=no in your configuration!" - mp.osd_message(err, 5) - msg.error(err) - - -- This may break, but since we can, let's try to just disable the builtin OSC. - mp.set_property_native('osc', false) -end - --- // mpv_thumbnail_script.lua // -- - -validate_user_opts() - -mp.register_event("shutdown", shutdown) -mp.register_event("start-file", request_init) -mp.register_event("tracks-changed", request_init) -mp.observe_property("playlist", nil, request_init) - -mp.register_script_message("osc-message", show_message) -mp.register_script_message("osc-chapterlist", function(dur) - show_message(get_chapterlist(), dur) -end) -mp.register_script_message("osc-playlist", function(dur) - show_message(get_playlist(), dur) -end) -mp.register_script_message("osc-tracklist", function(dur) - local msg = {} - for k,v in pairs(nicetypes) do - table.insert(msg, get_tracklist(k)) - end - show_message(table.concat(msg, '\n\n'), dur) -end) - -mp.observe_property("fullscreen", "bool", - function(name, val) - state.fullscreen = val - request_init() - end -) -mp.observe_property("border", "bool", - function(name, val) - state.border = val - request_init() - end -) -mp.observe_property("window-maximized", "bool", - function(name, val) - state.maximized = val - request_init() - end -) -mp.observe_property("idle-active", "bool", - function(name, val) - state.idle = val - request_tick() - end -) -mp.observe_property("pause", "bool", pause_state) -mp.observe_property("demuxer-cache-state", "native", cache_state) -mp.observe_property("vo-configured", "bool", function(name, val) - request_tick() -end) -mp.observe_property("playback-time", "number", function(name, val) - request_tick() -end) - --- mouse show/hide bindings -mp.set_key_bindings({ - {"mouse_move", function(e) process_event("mouse_move", nil) end}, - {"mouse_leave", mouse_leave}, -}, "showhide", "force") -mp.set_key_bindings({ - {"mouse_move", function(e) process_event("mouse_move", nil) end}, - {"mouse_leave", mouse_leave}, -}, "showhide_wc", "force") -do_enable_keybindings() - ---mouse input bindings -mp.set_key_bindings({ - {"mbtn_left", function(e) process_event("mbtn_left", "up") end, - function(e) process_event("mbtn_left", "down") end}, - {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end, - function(e) process_event("shift+mbtn_left", "down") end}, - {"mbtn_right", function(e) process_event("mbtn_right", "up") end, - function(e) process_event("mbtn_right", "down") end}, - -- alias to shift_mbtn_left for single-handed mouse use - {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end, - function(e) process_event("shift+mbtn_left", "down") end}, - {"wheel_up", function(e) process_event("wheel_up", "press") end}, - {"wheel_down", function(e) process_event("wheel_down", "press") end}, - {"mbtn_left_dbl", "ignore"}, - {"shift+mbtn_left_dbl", "ignore"}, - {"mbtn_right_dbl", "ignore"}, -}, "input", "force") -mp.enable_key_bindings("input") - -mp.set_key_bindings({ - {"mbtn_left", function(e) process_event("mbtn_left", "up") end, - function(e) process_event("mbtn_left", "down") end}, -}, "window-controls", "force") -mp.enable_key_bindings("window-controls") - -user_opts.hidetimeout_orig = user_opts.hidetimeout - -function always_on(val) - if val then - user_opts.hidetimeout = -1 -- disable autohide - if state.enabled then show_osc() end - else - user_opts.hidetimeout = user_opts.hidetimeout_orig - if state.enabled then hide_osc() end - end -end - --- mode can be auto/always/never/cycle --- the modes only affect internal variables and not stored on its own. -function visibility_mode(mode, no_osd) - if mode == "cycle" then - if not state.enabled then - mode = "auto" - elseif user_opts.hidetimeout >= 0 then - mode = "always" - else - mode = "never" - end - end - - if mode == "auto" then - always_on(false) - enable_osc(true) - elseif mode == "always" then - enable_osc(true) - always_on(true) - elseif mode == "never" then - enable_osc(false) - else - msg.warn("Ignoring unknown visibility mode '" .. mode .. "'") - return - end - - if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then - mp.osd_message("OSC visibility: " .. mode) - end - - update_margins() -end - -visibility_mode(user_opts.visibility, true) -mp.register_script_message("osc-visibility", visibility_mode) -mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end) - -set_virt_mouse_area(0, 0, 0, 0, "input") -set_virt_mouse_area(0, 0, 0, 0, "window-controls") diff --git a/mpv/scripts/mpv_thumbnail_script_server-thread-1.lua b/mpv/scripts/mpv_thumbnail_script_server-thread-1.lua @@ -1,736 +0,0 @@ ---[[ - Copyright (C) 2017 AMM - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. -]]-- ---[[ - mpv_thumbnail_script.lua 0.4.2 - commit b4cf490 (branch master) - https://github.com/TheAMM/mpv_thumbnail_script - Built on 2021-05-01 11:01:10 -]]-- -local assdraw = require 'mp.assdraw' -local msg = require 'mp.msg' -local opt = require 'mp.options' -local utils = require 'mp.utils' - --- Determine platform -- -ON_WINDOWS = (package.config:sub(1,1) ~= '/') - --- Some helper functions needed to parse the options -- -function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end - -function divmod (a, b) - return math.floor(a / b), a % b -end - --- Better modulo -function bmod( i, N ) - return (i % N + N) % N -end - -function join_paths(...) - local sep = ON_WINDOWS and "\\" or "/" - local result = ""; - for i, p in pairs({...}) do - if p ~= "" then - if is_absolute_path(p) then - result = p - else - result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p - end - end - end - return result:gsub("[\\"..sep.."]*$", "") -end - --- /some/path/file.ext -> /some/path, file.ext -function split_path( path ) - local sep = ON_WINDOWS and "\\" or "/" - local first_index, last_index = path:find('^.*' .. sep) - - if last_index == nil then - return "", path - else - local dir = path:sub(0, last_index-1) - local file = path:sub(last_index+1, -1) - - return dir, file - end -end - -function is_absolute_path( path ) - local tmp, is_win = path:gsub("^[A-Z]:\\", "") - local tmp, is_unix = path:gsub("^/", "") - return (is_win > 0) or (is_unix > 0) -end - -function Set(source) - local set = {} - for _, l in ipairs(source) do set[l] = true end - return set -end - ---------------------------- --- More helper functions -- ---------------------------- - --- Removes all keys from a table, without destroying the reference to it -function clear_table(target) - for key, value in pairs(target) do - target[key] = nil - end -end -function shallow_copy(target) - local copy = {} - for k, v in pairs(target) do - copy[k] = v - end - return copy -end - --- Rounds to given decimals. eg. round_dec(3.145, 0) => 3 -function round_dec(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - -function file_exists(name) - local f = io.open(name, "rb") - if f ~= nil then - local ok, err, code = f:read(1) - io.close(f) - return code == nil - else - return false - end -end - -function path_exists(name) - local f = io.open(name, "rb") - if f ~= nil then - io.close(f) - return true - else - return false - end -end - -function create_directories(path) - local cmd - if ON_WINDOWS then - cmd = { args = {"cmd", "/c", "mkdir", path} } - else - cmd = { args = {"mkdir", "-p", path} } - end - utils.subprocess(cmd) -end - --- Find an executable in PATH or CWD with the given name -function find_executable(name) - local delim = ON_WINDOWS and ";" or ":" - - local pwd = os.getenv("PWD") or utils.getcwd() - local path = os.getenv("PATH") - - local env_path = pwd .. delim .. path -- Check CWD first - - local result, filename - for path_dir in env_path:gmatch("[^"..delim.."]+") do - filename = join_paths(path_dir, name) - if file_exists(filename) then - result = filename - break - end - end - - return result -end - -local ExecutableFinder = { path_cache = {} } --- Searches for an executable and caches the result if any -function ExecutableFinder:get_executable_path( name, raw_name ) - name = ON_WINDOWS and not raw_name and (name .. ".exe") or name - - if self.path_cache[name] == nil then - self.path_cache[name] = find_executable(name) or false - end - return self.path_cache[name] -end - --- Format seconds to HH.MM.SS.sss -function format_time(seconds, sep, decimals) - decimals = decimals == nil and 3 or decimals - sep = sep and sep or "." - local s = seconds - local h, s = divmod(s, 60*60) - local m, s = divmod(s, 60) - - local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals) - - return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s) -end - --- Format seconds to 1h 2m 3.4s -function format_time_hms(seconds, sep, decimals, force_full) - decimals = decimals == nil and 1 or decimals - sep = sep ~= nil and sep or " " - - local s = seconds - local h, s = divmod(s, 60*60) - local m, s = divmod(s, 60) - - if force_full or h > 0 then - return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s) - elseif m > 0 then - return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s) - else - return string.format("%." .. tostring(decimals) .. "fs", s) - end -end - --- Writes text on OSD and console -function log_info(txt, timeout) - timeout = timeout or 1.5 - msg.info(txt) - mp.osd_message(txt, timeout) -end - --- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-" -function join_table(source, before, after, sep) - before = before or "" - after = after or "" - sep = sep or ", " - local result = "" - for i, v in pairs(source) do - if not isempty(v) then - local part = before .. v .. after - if i == 1 then - result = part - else - result = result .. sep .. part - end - end - end - return result -end - -function wrap(s, char) - char = char or "'" - return char .. s .. char -end --- Wraps given string into 'string' and escapes any 's in it -function escape_and_wrap(s, char, replacement) - char = char or "'" - replacement = replacement or "\\" .. char - return wrap(string.gsub(s, char, replacement), char) -end --- Escapes single quotes in a string and wraps the input in single quotes -function escape_single_bash(s) - return escape_and_wrap(s, "'", "'\\''") -end - --- Returns (a .. b) if b is not empty or nil -function joined_or_nil(a, b) - return not isempty(b) and (a .. b) or nil -end - --- Put items from one table into another -function extend_table(target, source) - for i, v in pairs(source) do - table.insert(target, v) - end -end - --- Creates a handle and filename for a temporary random file (in current directory) -function create_temporary_file(base, mode, suffix) - local handle, filename - suffix = suffix or "" - while true do - filename = base .. tostring(math.random(1, 5000)) .. suffix - handle = io.open(filename, "r") - if not handle then - handle = io.open(filename, mode) - break - end - io.close(handle) - end - return handle, filename -end - - -function get_processor_count() - local proc_count - - if ON_WINDOWS then - proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS")) - else - local cpuinfo_handle = io.open("/proc/cpuinfo") - if cpuinfo_handle ~= nil then - local cpuinfo_contents = cpuinfo_handle:read("*a") - local _, replace_count = cpuinfo_contents:gsub('processor', '') - proc_count = replace_count - end - end - - if proc_count and proc_count > 0 then - return proc_count - else - return nil - end -end - -function substitute_values(string, values) - local substitutor = function(match) - if match == "%" then - return "%" - else - -- nil is discarded by gsub - return values[match] - end - end - - local substituted = string:gsub('%%(.)', substitutor) - return substituted -end - --- ASS HELPERS -- -function round_rect_top( ass, x0, y0, x1, y1, r ) - local c = 0.551915024494 * r -- circle approximation - ass:move_to(x0 + r, y0) - ass:line_to(x1 - r, y0) -- top line - if r > 0 then - ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner - end - ass:line_to(x1, y1) -- right line - ass:line_to(x0, y1) -- bottom line - ass:line_to(x0, y0 + r) -- left line - if r > 0 then - ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner - end -end - -function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl) - local c = 0.551915024494 - ass:move_to(x0 + rtl, y0) - ass:line_to(x1 - rtr, y0) -- top line - if rtr > 0 then - ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner - end - ass:line_to(x1, y1 - rbr) -- right line - if rbr > 0 then - ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner - end - ass:line_to(x0 + rbl, y1) -- bottom line - if rbl > 0 then - ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner - end - ass:line_to(x0, y0 + rtl) -- left line - if rtl > 0 then - ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner - end -end -local SCRIPT_NAME = "mpv_thumbnail_script" - -local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/" - -local thumbnailer_options = { - -- The thumbnail directory - cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"), - - ------------------------ - -- Generation options -- - ------------------------ - - -- Automatically generate the thumbnails on video load, without a keypress - autogenerate = true, - - -- Only automatically thumbnail videos shorter than this (seconds) - autogenerate_max_duration = 3600, -- 1 hour - - -- SHA1-sum filenames over this length - -- It's nice to know what files the thumbnails are (hence directory names) - -- but long URLs may approach filesystem limits. - hash_filename_length = 128, - - -- Use mpv to generate thumbnail even if ffmpeg is found in PATH - -- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)! - -- mpv is a bit slower, but has better support overall (eg. subtitles in the previews) - prefer_mpv = true, - - -- Explicitly disable subtitles on the mpv sub-calls - mpv_no_sub = false, - -- Add a "--no-config" to the mpv sub-call arguments - mpv_no_config = false, - -- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments - -- Use "" to disable - mpv_profile = "", - -- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log - -- The logs are removed after successful encodes, unless you set mpv_keep_logs below - mpv_logs = true, - -- Keep all mpv logs, even the succesfull ones - mpv_keep_logs = false, - - -- Disable the built-in keybind ("T") to add your own - disable_keybinds = false, - - --------------------- - -- Display options -- - --------------------- - - -- Move the thumbnail up or down - -- For example: - -- topbar/bottombar: 24 - -- rest: 0 - vertical_offset = 24, - - -- Adjust background padding - -- Examples: - -- topbar: 0, 10, 10, 10 - -- bottombar: 10, 0, 10, 10 - -- slimbox/box: 10, 10, 10, 10 - pad_top = 10, - pad_bot = 0, - pad_left = 10, - pad_right = 10, - - -- If true, pad values are screen-pixels. If false, video-pixels. - pad_in_screenspace = true, - -- Calculate pad into the offset - offset_by_pad = true, - - -- Background color in BBGGRR - background_color = "000000", - -- Alpha: 0 - fully opaque, 255 - transparent - background_alpha = 80, - - -- Keep thumbnail on the screen near left or right side - constrain_to_screen = true, - - -- Do not display the thumbnailing progress - hide_progress = false, - - ----------------------- - -- Thumbnail options -- - ----------------------- - - -- The maximum dimensions of the thumbnails (pixels) - thumbnail_width = 200, - thumbnail_height = 200, - - -- The thumbnail count target - -- (This will result in a thumbnail every ~10 seconds for a 25 minute video) - thumbnail_count = 150, - - -- The above target count will be adjusted by the minimum and - -- maximum time difference between thumbnails. - -- The thumbnail_count will be used to calculate a target separation, - -- and min/max_delta will be used to constrict it. - - -- In other words, thumbnails will be: - -- at least min_delta seconds apart (limiting the amount) - -- at most max_delta seconds apart (raising the amount if needed) - min_delta = 5, - -- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours! - max_delta = 90, - - - -- Overrides for remote urls (you generally want less thumbnails!) - -- Thumbnailing network paths will be done with mpv - - -- Allow thumbnailing network paths (naive check for "://") - thumbnail_network = false, - -- Override thumbnail count, min/max delta - remote_thumbnail_count = 60, - remote_min_delta = 15, - remote_max_delta = 120, - - -- Try to grab the raw stream and disable ytdl for the mpv subcalls - -- Much faster than passing the url to ytdl again, but may cause problems with some sites - remote_direct_stream = true, -} - -read_options(thumbnailer_options, SCRIPT_NAME) -function skip_nil(tbl) - local n = {} - for k, v in pairs(tbl) do - table.insert(n, v) - end - return n -end - -function create_thumbnail_mpv(file_path, timestamp, size, output_path, options) - options = options or {} - - local ytdl_disabled = not options.enable_ytdl and (mp.get_property_native("ytdl") == false - or thumbnailer_options.remote_direct_stream) - - local header_fields_arg = nil - local header_fields = mp.get_property_native("http-header-fields") - if #header_fields > 0 then - -- We can't escape the headers, mpv won't parse "--http-header-fields='Name: value'" properly - header_fields_arg = "--http-header-fields=" .. table.concat(header_fields, ",") - end - - local profile_arg = nil - if thumbnailer_options.mpv_profile ~= "" then - profile_arg = "--profile=" .. thumbnailer_options.mpv_profile - end - - local log_arg = "--log-file=" .. output_path .. ".log" - - local mpv_command = skip_nil({ - "mpv", - -- Hide console output - "--msg-level=all=no", - - -- Disable ytdl - (ytdl_disabled and "--no-ytdl" or nil), - -- Pass HTTP headers from current instance - header_fields_arg, - -- Pass User-Agent and Referer - should do no harm even with ytdl active - "--user-agent=" .. mp.get_property_native("user-agent"), - "--referrer=" .. mp.get_property_native("referrer"), - -- Disable hardware decoding - "--hwdec=no", - - -- Insert --no-config, --profile=... and --log-file if enabled - (thumbnailer_options.mpv_no_config and "--no-config" or nil), - profile_arg, - (thumbnailer_options.mpv_logs and log_arg or nil), - - file_path, - - "--start=" .. tostring(timestamp), - "--frames=1", - "--hr-seek=yes", - "--no-audio", - -- Optionally disable subtitles - (thumbnailer_options.mpv_no_sub and "--no-sub" or nil), - - ("--vf=scale=%d:%d"):format(size.w, size.h), - "--vf-add=format=bgra", - "--of=rawvideo", - "--ovc=rawvideo", - "--o=" .. output_path - }) - return utils.subprocess({args=mpv_command}) -end - - -function create_thumbnail_ffmpeg(file_path, timestamp, size, output_path) - local ffmpeg_command = { - "ffmpeg", - "-loglevel", "quiet", - "-noaccurate_seek", - "-ss", format_time(timestamp, ":"), - "-i", file_path, - - "-frames:v", "1", - "-an", - - "-vf", ("scale=%d:%d"):format(size.w, size.h), - "-c:v", "rawvideo", - "-pix_fmt", "bgra", - "-f", "rawvideo", - - "-y", output_path - } - return utils.subprocess({args=ffmpeg_command}) -end - - -function check_output(ret, output_path, is_mpv) - local log_path = output_path .. ".log" - local success = true - - if ret.killed_by_us then - return nil - else - if ret.error or ret.status ~= 0 then - msg.error("Thumbnailing command failed!") - msg.error("mpv process error:", ret.error) - msg.error("Process stdout:", ret.stdout) - if is_mpv then - msg.error("Debug log:", log_path) - end - - success = false - end - - if not file_exists(output_path) then - msg.error("Output file missing!", output_path) - success = false - end - end - - if is_mpv and not thumbnailer_options.mpv_keep_logs then - -- Remove successful debug logs - if success and file_exists(log_path) then - os.remove(log_path) - end - end - - return success -end - - -function do_worker_job(state_json_string, frames_json_string) - msg.debug("Handling given job") - local thumb_state, err = utils.parse_json(state_json_string) - if err then - msg.error("Failed to parse state JSON") - return - end - - local thumbnail_indexes, err = utils.parse_json(frames_json_string) - if err then - msg.error("Failed to parse thumbnail frame indexes") - return - end - - local thumbnail_func = create_thumbnail_mpv - if not thumbnailer_options.prefer_mpv then - if ExecutableFinder:get_executable_path("ffmpeg") then - thumbnail_func = create_thumbnail_ffmpeg - else - msg.warn("Could not find ffmpeg in PATH! Falling back on mpv.") - end - end - - local file_duration = mp.get_property_native("duration") - local file_path = thumb_state.worker_input_path - - if thumb_state.is_remote then - if (thumbnail_func == create_thumbnail_ffmpeg) then - msg.warn("Thumbnailing remote path, falling back on mpv.") - end - thumbnail_func = create_thumbnail_mpv - end - - local generate_thumbnail_for_index = function(thumbnail_index) - -- Given a 1-based thumbnail index, generate a thumbnail for it based on the thumbnailer state - local thumb_idx = thumbnail_index - 1 - msg.debug("Starting work on thumbnail", thumb_idx) - - local thumbnail_path = thumb_state.thumbnail_template:format(thumb_idx) - -- Grab the "middle" of the thumbnail duration instead of the very start, and leave some margin in the end - local timestamp = math.min(file_duration - 0.25, (thumb_idx + 0.5) * thumb_state.thumbnail_delta) - - mp.commandv("script-message", "mpv_thumbnail_script-progress", tostring(thumbnail_index)) - - -- The expected size (raw BGRA image) - local thumbnail_raw_size = (thumb_state.thumbnail_size.w * thumb_state.thumbnail_size.h * 4) - - local need_thumbnail_generation = false - - -- Check if the thumbnail already exists and is the correct size - local thumbnail_file = io.open(thumbnail_path, "rb") - if thumbnail_file == nil then - need_thumbnail_generation = true - else - local existing_thumbnail_filesize = thumbnail_file:seek("end") - if existing_thumbnail_filesize ~= thumbnail_raw_size then - -- Size doesn't match, so (re)generate - msg.warn("Thumbnail", thumb_idx, "did not match expected size, regenerating") - need_thumbnail_generation = true - end - thumbnail_file:close() - end - - if need_thumbnail_generation then - local ret = thumbnail_func(file_path, timestamp, thumb_state.thumbnail_size, thumbnail_path, thumb_state.worker_extra) - local success = check_output(ret, thumbnail_path, thumbnail_func == create_thumbnail_mpv) - - if success == nil then - -- Killed by us, changing files, ignore - msg.debug("Changing files, subprocess killed") - return true - elseif not success then - -- Real failure - mp.osd_message("Thumbnailing failed, check console for details", 3.5) - return true - end - else - msg.debug("Thumbnail", thumb_idx, "already done!") - end - - -- Verify thumbnail size - -- Sometimes ffmpeg will output an empty file when seeking to a "bad" section (usually the end) - thumbnail_file = io.open(thumbnail_path, "rb") - - -- Bail if we can't read the file (it should really exist by now, we checked this in check_output!) - if thumbnail_file == nil then - msg.error("Thumbnail suddenly disappeared!") - return true - end - - -- Check the size of the generated file - local thumbnail_file_size = thumbnail_file:seek("end") - thumbnail_file:close() - - -- Check if the file is big enough - local missing_bytes = math.max(0, thumbnail_raw_size - thumbnail_file_size) - if missing_bytes > 0 then - msg.warn(("Thumbnail missing %d bytes (expected %d, had %d), padding %s"):format( - missing_bytes, thumbnail_raw_size, thumbnail_file_size, thumbnail_path - )) - -- Pad the file if it's missing content (eg. ffmpeg seek to file end) - thumbnail_file = io.open(thumbnail_path, "ab") - thumbnail_file:write(string.rep(string.char(0), missing_bytes)) - thumbnail_file:close() - end - - msg.debug("Finished work on thumbnail", thumb_idx) - mp.commandv("script-message", "mpv_thumbnail_script-ready", tostring(thumbnail_index), thumbnail_path) - end - - msg.debug(("Generating %d thumbnails @ %dx%d for %q"):format( - #thumbnail_indexes, - thumb_state.thumbnail_size.w, - thumb_state.thumbnail_size.h, - file_path)) - - for i, thumbnail_index in ipairs(thumbnail_indexes) do - local bail = generate_thumbnail_for_index(thumbnail_index) - if bail then return end - end - -end - --- Set up listeners and keybinds - --- Job listener -mp.register_script_message("mpv_thumbnail_script-job", do_worker_job) - - --- Register this worker with the master script -local register_timer = nil -local register_timeout = mp.get_time() + 1.5 - -local register_function = function() - if mp.get_time() > register_timeout and register_timer then - msg.error("Thumbnail worker registering timed out") - register_timer:stop() - else - msg.debug("Announcing self to master...") - mp.commandv("script-message", "mpv_thumbnail_script-worker", mp.get_script_name()) - end -end - -register_timer = mp.add_periodic_timer(0.1, register_function) - -mp.register_script_message("mpv_thumbnail_script-slaved", function() - msg.debug("Successfully registered with master") - register_timer:stop() -end) diff --git a/mpv/scripts/mpv_thumbnail_script_server-thread-2.lua b/mpv/scripts/mpv_thumbnail_script_server-thread-2.lua @@ -1,736 +0,0 @@ ---[[ - Copyright (C) 2017 AMM - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. -]]-- ---[[ - mpv_thumbnail_script.lua 0.4.2 - commit b4cf490 (branch master) - https://github.com/TheAMM/mpv_thumbnail_script - Built on 2021-05-01 11:01:10 -]]-- -local assdraw = require 'mp.assdraw' -local msg = require 'mp.msg' -local opt = require 'mp.options' -local utils = require 'mp.utils' - --- Determine platform -- -ON_WINDOWS = (package.config:sub(1,1) ~= '/') - --- Some helper functions needed to parse the options -- -function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end - -function divmod (a, b) - return math.floor(a / b), a % b -end - --- Better modulo -function bmod( i, N ) - return (i % N + N) % N -end - -function join_paths(...) - local sep = ON_WINDOWS and "\\" or "/" - local result = ""; - for i, p in pairs({...}) do - if p ~= "" then - if is_absolute_path(p) then - result = p - else - result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p - end - end - end - return result:gsub("[\\"..sep.."]*$", "") -end - --- /some/path/file.ext -> /some/path, file.ext -function split_path( path ) - local sep = ON_WINDOWS and "\\" or "/" - local first_index, last_index = path:find('^.*' .. sep) - - if last_index == nil then - return "", path - else - local dir = path:sub(0, last_index-1) - local file = path:sub(last_index+1, -1) - - return dir, file - end -end - -function is_absolute_path( path ) - local tmp, is_win = path:gsub("^[A-Z]:\\", "") - local tmp, is_unix = path:gsub("^/", "") - return (is_win > 0) or (is_unix > 0) -end - -function Set(source) - local set = {} - for _, l in ipairs(source) do set[l] = true end - return set -end - ---------------------------- --- More helper functions -- ---------------------------- - --- Removes all keys from a table, without destroying the reference to it -function clear_table(target) - for key, value in pairs(target) do - target[key] = nil - end -end -function shallow_copy(target) - local copy = {} - for k, v in pairs(target) do - copy[k] = v - end - return copy -end - --- Rounds to given decimals. eg. round_dec(3.145, 0) => 3 -function round_dec(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult -end - -function file_exists(name) - local f = io.open(name, "rb") - if f ~= nil then - local ok, err, code = f:read(1) - io.close(f) - return code == nil - else - return false - end -end - -function path_exists(name) - local f = io.open(name, "rb") - if f ~= nil then - io.close(f) - return true - else - return false - end -end - -function create_directories(path) - local cmd - if ON_WINDOWS then - cmd = { args = {"cmd", "/c", "mkdir", path} } - else - cmd = { args = {"mkdir", "-p", path} } - end - utils.subprocess(cmd) -end - --- Find an executable in PATH or CWD with the given name -function find_executable(name) - local delim = ON_WINDOWS and ";" or ":" - - local pwd = os.getenv("PWD") or utils.getcwd() - local path = os.getenv("PATH") - - local env_path = pwd .. delim .. path -- Check CWD first - - local result, filename - for path_dir in env_path:gmatch("[^"..delim.."]+") do - filename = join_paths(path_dir, name) - if file_exists(filename) then - result = filename - break - end - end - - return result -end - -local ExecutableFinder = { path_cache = {} } --- Searches for an executable and caches the result if any -function ExecutableFinder:get_executable_path( name, raw_name ) - name = ON_WINDOWS and not raw_name and (name .. ".exe") or name - - if self.path_cache[name] == nil then - self.path_cache[name] = find_executable(name) or false - end - return self.path_cache[name] -end - --- Format seconds to HH.MM.SS.sss -function format_time(seconds, sep, decimals) - decimals = decimals == nil and 3 or decimals - sep = sep and sep or "." - local s = seconds - local h, s = divmod(s, 60*60) - local m, s = divmod(s, 60) - - local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals) - - return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s) -end - --- Format seconds to 1h 2m 3.4s -function format_time_hms(seconds, sep, decimals, force_full) - decimals = decimals == nil and 1 or decimals - sep = sep ~= nil and sep or " " - - local s = seconds - local h, s = divmod(s, 60*60) - local m, s = divmod(s, 60) - - if force_full or h > 0 then - return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s) - elseif m > 0 then - return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s) - else - return string.format("%." .. tostring(decimals) .. "fs", s) - end -end - --- Writes text on OSD and console -function log_info(txt, timeout) - timeout = timeout or 1.5 - msg.info(txt) - mp.osd_message(txt, timeout) -end - --- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-" -function join_table(source, before, after, sep) - before = before or "" - after = after or "" - sep = sep or ", " - local result = "" - for i, v in pairs(source) do - if not isempty(v) then - local part = before .. v .. after - if i == 1 then - result = part - else - result = result .. sep .. part - end - end - end - return result -end - -function wrap(s, char) - char = char or "'" - return char .. s .. char -end --- Wraps given string into 'string' and escapes any 's in it -function escape_and_wrap(s, char, replacement) - char = char or "'" - replacement = replacement or "\\" .. char - return wrap(string.gsub(s, char, replacement), char) -end --- Escapes single quotes in a string and wraps the input in single quotes -function escape_single_bash(s) - return escape_and_wrap(s, "'", "'\\''") -end - --- Returns (a .. b) if b is not empty or nil -function joined_or_nil(a, b) - return not isempty(b) and (a .. b) or nil -end - --- Put items from one table into another -function extend_table(target, source) - for i, v in pairs(source) do - table.insert(target, v) - end -end - --- Creates a handle and filename for a temporary random file (in current directory) -function create_temporary_file(base, mode, suffix) - local handle, filename - suffix = suffix or "" - while true do - filename = base .. tostring(math.random(1, 5000)) .. suffix - handle = io.open(filename, "r") - if not handle then - handle = io.open(filename, mode) - break - end - io.close(handle) - end - return handle, filename -end - - -function get_processor_count() - local proc_count - - if ON_WINDOWS then - proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS")) - else - local cpuinfo_handle = io.open("/proc/cpuinfo") - if cpuinfo_handle ~= nil then - local cpuinfo_contents = cpuinfo_handle:read("*a") - local _, replace_count = cpuinfo_contents:gsub('processor', '') - proc_count = replace_count - end - end - - if proc_count and proc_count > 0 then - return proc_count - else - return nil - end -end - -function substitute_values(string, values) - local substitutor = function(match) - if match == "%" then - return "%" - else - -- nil is discarded by gsub - return values[match] - end - end - - local substituted = string:gsub('%%(.)', substitutor) - return substituted -end - --- ASS HELPERS -- -function round_rect_top( ass, x0, y0, x1, y1, r ) - local c = 0.551915024494 * r -- circle approximation - ass:move_to(x0 + r, y0) - ass:line_to(x1 - r, y0) -- top line - if r > 0 then - ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner - end - ass:line_to(x1, y1) -- right line - ass:line_to(x0, y1) -- bottom line - ass:line_to(x0, y0 + r) -- left line - if r > 0 then - ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner - end -end - -function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl) - local c = 0.551915024494 - ass:move_to(x0 + rtl, y0) - ass:line_to(x1 - rtr, y0) -- top line - if rtr > 0 then - ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner - end - ass:line_to(x1, y1 - rbr) -- right line - if rbr > 0 then - ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner - end - ass:line_to(x0 + rbl, y1) -- bottom line - if rbl > 0 then - ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner - end - ass:line_to(x0, y0 + rtl) -- left line - if rtl > 0 then - ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner - end -end -local SCRIPT_NAME = "mpv_thumbnail_script" - -local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/" - -local thumbnailer_options = { - -- The thumbnail directory - cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"), - - ------------------------ - -- Generation options -- - ------------------------ - - -- Automatically generate the thumbnails on video load, without a keypress - autogenerate = true, - - -- Only automatically thumbnail videos shorter than this (seconds) - autogenerate_max_duration = 3600, -- 1 hour - - -- SHA1-sum filenames over this length - -- It's nice to know what files the thumbnails are (hence directory names) - -- but long URLs may approach filesystem limits. - hash_filename_length = 128, - - -- Use mpv to generate thumbnail even if ffmpeg is found in PATH - -- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)! - -- mpv is a bit slower, but has better support overall (eg. subtitles in the previews) - prefer_mpv = true, - - -- Explicitly disable subtitles on the mpv sub-calls - mpv_no_sub = false, - -- Add a "--no-config" to the mpv sub-call arguments - mpv_no_config = false, - -- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments - -- Use "" to disable - mpv_profile = "", - -- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log - -- The logs are removed after successful encodes, unless you set mpv_keep_logs below - mpv_logs = true, - -- Keep all mpv logs, even the succesfull ones - mpv_keep_logs = false, - - -- Disable the built-in keybind ("T") to add your own - disable_keybinds = false, - - --------------------- - -- Display options -- - --------------------- - - -- Move the thumbnail up or down - -- For example: - -- topbar/bottombar: 24 - -- rest: 0 - vertical_offset = 24, - - -- Adjust background padding - -- Examples: - -- topbar: 0, 10, 10, 10 - -- bottombar: 10, 0, 10, 10 - -- slimbox/box: 10, 10, 10, 10 - pad_top = 10, - pad_bot = 0, - pad_left = 10, - pad_right = 10, - - -- If true, pad values are screen-pixels. If false, video-pixels. - pad_in_screenspace = true, - -- Calculate pad into the offset - offset_by_pad = true, - - -- Background color in BBGGRR - background_color = "000000", - -- Alpha: 0 - fully opaque, 255 - transparent - background_alpha = 80, - - -- Keep thumbnail on the screen near left or right side - constrain_to_screen = true, - - -- Do not display the thumbnailing progress - hide_progress = false, - - ----------------------- - -- Thumbnail options -- - ----------------------- - - -- The maximum dimensions of the thumbnails (pixels) - thumbnail_width = 200, - thumbnail_height = 200, - - -- The thumbnail count target - -- (This will result in a thumbnail every ~10 seconds for a 25 minute video) - thumbnail_count = 150, - - -- The above target count will be adjusted by the minimum and - -- maximum time difference between thumbnails. - -- The thumbnail_count will be used to calculate a target separation, - -- and min/max_delta will be used to constrict it. - - -- In other words, thumbnails will be: - -- at least min_delta seconds apart (limiting the amount) - -- at most max_delta seconds apart (raising the amount if needed) - min_delta = 5, - -- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours! - max_delta = 90, - - - -- Overrides for remote urls (you generally want less thumbnails!) - -- Thumbnailing network paths will be done with mpv - - -- Allow thumbnailing network paths (naive check for "://") - thumbnail_network = false, - -- Override thumbnail count, min/max delta - remote_thumbnail_count = 60, - remote_min_delta = 15, - remote_max_delta = 120, - - -- Try to grab the raw stream and disable ytdl for the mpv subcalls - -- Much faster than passing the url to ytdl again, but may cause problems with some sites - remote_direct_stream = true, -} - -read_options(thumbnailer_options, SCRIPT_NAME) -function skip_nil(tbl) - local n = {} - for k, v in pairs(tbl) do - table.insert(n, v) - end - return n -end - -function create_thumbnail_mpv(file_path, timestamp, size, output_path, options) - options = options or {} - - local ytdl_disabled = not options.enable_ytdl and (mp.get_property_native("ytdl") == false - or thumbnailer_options.remote_direct_stream) - - local header_fields_arg = nil - local header_fields = mp.get_property_native("http-header-fields") - if #header_fields > 0 then - -- We can't escape the headers, mpv won't parse "--http-header-fields='Name: value'" properly - header_fields_arg = "--http-header-fields=" .. table.concat(header_fields, ",") - end - - local profile_arg = nil - if thumbnailer_options.mpv_profile ~= "" then - profile_arg = "--profile=" .. thumbnailer_options.mpv_profile - end - - local log_arg = "--log-file=" .. output_path .. ".log" - - local mpv_command = skip_nil({ - "mpv", - -- Hide console output - "--msg-level=all=no", - - -- Disable ytdl - (ytdl_disabled and "--no-ytdl" or nil), - -- Pass HTTP headers from current instance - header_fields_arg, - -- Pass User-Agent and Referer - should do no harm even with ytdl active - "--user-agent=" .. mp.get_property_native("user-agent"), - "--referrer=" .. mp.get_property_native("referrer"), - -- Disable hardware decoding - "--hwdec=no", - - -- Insert --no-config, --profile=... and --log-file if enabled - (thumbnailer_options.mpv_no_config and "--no-config" or nil), - profile_arg, - (thumbnailer_options.mpv_logs and log_arg or nil), - - file_path, - - "--start=" .. tostring(timestamp), - "--frames=1", - "--hr-seek=yes", - "--no-audio", - -- Optionally disable subtitles - (thumbnailer_options.mpv_no_sub and "--no-sub" or nil), - - ("--vf=scale=%d:%d"):format(size.w, size.h), - "--vf-add=format=bgra", - "--of=rawvideo", - "--ovc=rawvideo", - "--o=" .. output_path - }) - return utils.subprocess({args=mpv_command}) -end - - -function create_thumbnail_ffmpeg(file_path, timestamp, size, output_path) - local ffmpeg_command = { - "ffmpeg", - "-loglevel", "quiet", - "-noaccurate_seek", - "-ss", format_time(timestamp, ":"), - "-i", file_path, - - "-frames:v", "1", - "-an", - - "-vf", ("scale=%d:%d"):format(size.w, size.h), - "-c:v", "rawvideo", - "-pix_fmt", "bgra", - "-f", "rawvideo", - - "-y", output_path - } - return utils.subprocess({args=ffmpeg_command}) -end - - -function check_output(ret, output_path, is_mpv) - local log_path = output_path .. ".log" - local success = true - - if ret.killed_by_us then - return nil - else - if ret.error or ret.status ~= 0 then - msg.error("Thumbnailing command failed!") - msg.error("mpv process error:", ret.error) - msg.error("Process stdout:", ret.stdout) - if is_mpv then - msg.error("Debug log:", log_path) - end - - success = false - end - - if not file_exists(output_path) then - msg.error("Output file missing!", output_path) - success = false - end - end - - if is_mpv and not thumbnailer_options.mpv_keep_logs then - -- Remove successful debug logs - if success and file_exists(log_path) then - os.remove(log_path) - end - end - - return success -end - - -function do_worker_job(state_json_string, frames_json_string) - msg.debug("Handling given job") - local thumb_state, err = utils.parse_json(state_json_string) - if err then - msg.error("Failed to parse state JSON") - return - end - - local thumbnail_indexes, err = utils.parse_json(frames_json_string) - if err then - msg.error("Failed to parse thumbnail frame indexes") - return - end - - local thumbnail_func = create_thumbnail_mpv - if not thumbnailer_options.prefer_mpv then - if ExecutableFinder:get_executable_path("ffmpeg") then - thumbnail_func = create_thumbnail_ffmpeg - else - msg.warn("Could not find ffmpeg in PATH! Falling back on mpv.") - end - end - - local file_duration = mp.get_property_native("duration") - local file_path = thumb_state.worker_input_path - - if thumb_state.is_remote then - if (thumbnail_func == create_thumbnail_ffmpeg) then - msg.warn("Thumbnailing remote path, falling back on mpv.") - end - thumbnail_func = create_thumbnail_mpv - end - - local generate_thumbnail_for_index = function(thumbnail_index) - -- Given a 1-based thumbnail index, generate a thumbnail for it based on the thumbnailer state - local thumb_idx = thumbnail_index - 1 - msg.debug("Starting work on thumbnail", thumb_idx) - - local thumbnail_path = thumb_state.thumbnail_template:format(thumb_idx) - -- Grab the "middle" of the thumbnail duration instead of the very start, and leave some margin in the end - local timestamp = math.min(file_duration - 0.25, (thumb_idx + 0.5) * thumb_state.thumbnail_delta) - - mp.commandv("script-message", "mpv_thumbnail_script-progress", tostring(thumbnail_index)) - - -- The expected size (raw BGRA image) - local thumbnail_raw_size = (thumb_state.thumbnail_size.w * thumb_state.thumbnail_size.h * 4) - - local need_thumbnail_generation = false - - -- Check if the thumbnail already exists and is the correct size - local thumbnail_file = io.open(thumbnail_path, "rb") - if thumbnail_file == nil then - need_thumbnail_generation = true - else - local existing_thumbnail_filesize = thumbnail_file:seek("end") - if existing_thumbnail_filesize ~= thumbnail_raw_size then - -- Size doesn't match, so (re)generate - msg.warn("Thumbnail", thumb_idx, "did not match expected size, regenerating") - need_thumbnail_generation = true - end - thumbnail_file:close() - end - - if need_thumbnail_generation then - local ret = thumbnail_func(file_path, timestamp, thumb_state.thumbnail_size, thumbnail_path, thumb_state.worker_extra) - local success = check_output(ret, thumbnail_path, thumbnail_func == create_thumbnail_mpv) - - if success == nil then - -- Killed by us, changing files, ignore - msg.debug("Changing files, subprocess killed") - return true - elseif not success then - -- Real failure - mp.osd_message("Thumbnailing failed, check console for details", 3.5) - return true - end - else - msg.debug("Thumbnail", thumb_idx, "already done!") - end - - -- Verify thumbnail size - -- Sometimes ffmpeg will output an empty file when seeking to a "bad" section (usually the end) - thumbnail_file = io.open(thumbnail_path, "rb") - - -- Bail if we can't read the file (it should really exist by now, we checked this in check_output!) - if thumbnail_file == nil then - msg.error("Thumbnail suddenly disappeared!") - return true - end - - -- Check the size of the generated file - local thumbnail_file_size = thumbnail_file:seek("end") - thumbnail_file:close() - - -- Check if the file is big enough - local missing_bytes = math.max(0, thumbnail_raw_size - thumbnail_file_size) - if missing_bytes > 0 then - msg.warn(("Thumbnail missing %d bytes (expected %d, had %d), padding %s"):format( - missing_bytes, thumbnail_raw_size, thumbnail_file_size, thumbnail_path - )) - -- Pad the file if it's missing content (eg. ffmpeg seek to file end) - thumbnail_file = io.open(thumbnail_path, "ab") - thumbnail_file:write(string.rep(string.char(0), missing_bytes)) - thumbnail_file:close() - end - - msg.debug("Finished work on thumbnail", thumb_idx) - mp.commandv("script-message", "mpv_thumbnail_script-ready", tostring(thumbnail_index), thumbnail_path) - end - - msg.debug(("Generating %d thumbnails @ %dx%d for %q"):format( - #thumbnail_indexes, - thumb_state.thumbnail_size.w, - thumb_state.thumbnail_size.h, - file_path)) - - for i, thumbnail_index in ipairs(thumbnail_indexes) do - local bail = generate_thumbnail_for_index(thumbnail_index) - if bail then return end - end - -end - --- Set up listeners and keybinds - --- Job listener -mp.register_script_message("mpv_thumbnail_script-job", do_worker_job) - - --- Register this worker with the master script -local register_timer = nil -local register_timeout = mp.get_time() + 1.5 - -local register_function = function() - if mp.get_time() > register_timeout and register_timer then - msg.error("Thumbnail worker registering timed out") - register_timer:stop() - else - msg.debug("Announcing self to master...") - mp.commandv("script-message", "mpv_thumbnail_script-worker", mp.get_script_name()) - end -end - -register_timer = mp.add_periodic_timer(0.1, register_function) - -mp.register_script_message("mpv_thumbnail_script-slaved", function() - msg.debug("Successfully registered with master") - register_timer:stop() -end)