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:
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)