dotfiles

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

mpv_thumbnail_script_client_osc.lua (135803B)


      1 --[[
      2     Copyright (C) 2017 AMM
      3 
      4     This program is free software: you can redistribute it and/or modify
      5     it under the terms of the GNU General Public License as published by
      6     the Free Software Foundation, either version 3 of the License, or
      7     (at your option) any later version.
      8 
      9     This program is distributed in the hope that it will be useful,
     10     but WITHOUT ANY WARRANTY; without even the implied warranty of
     11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12     GNU General Public License for more details.
     13 
     14     You should have received a copy of the GNU General Public License
     15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 ]]--
     17 --[[
     18     mpv_thumbnail_script.lua 0.4.2 - commit b4cf490 (branch master)
     19     https://github.com/TheAMM/mpv_thumbnail_script
     20     Built on 2021-05-01 11:01:10
     21 ]]--
     22 local assdraw = require 'mp.assdraw'
     23 local msg = require 'mp.msg'
     24 local opt = require 'mp.options'
     25 local utils = require 'mp.utils'
     26 
     27 -- Determine platform --
     28 ON_WINDOWS = (package.config:sub(1,1) ~= '/')
     29 
     30 -- Some helper functions needed to parse the options --
     31 function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end
     32 
     33 function divmod (a, b)
     34   return math.floor(a / b), a % b
     35 end
     36 
     37 -- Better modulo
     38 function bmod( i, N )
     39   return (i % N + N) % N
     40 end
     41 
     42 function join_paths(...)
     43   local sep = ON_WINDOWS and "\\" or "/"
     44   local result = "";
     45   for i, p in pairs({...}) do
     46     if p ~= "" then
     47       if is_absolute_path(p) then
     48         result = p
     49       else
     50         result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p
     51       end
     52     end
     53   end
     54   return result:gsub("[\\"..sep.."]*$", "")
     55 end
     56 
     57 -- /some/path/file.ext -> /some/path, file.ext
     58 function split_path( path )
     59   local sep = ON_WINDOWS and "\\" or "/"
     60   local first_index, last_index = path:find('^.*' .. sep)
     61 
     62   if last_index == nil then
     63     return "", path
     64   else
     65     local dir = path:sub(0, last_index-1)
     66     local file = path:sub(last_index+1, -1)
     67 
     68     return dir, file
     69   end
     70 end
     71 
     72 function is_absolute_path( path )
     73   local tmp, is_win  = path:gsub("^[A-Z]:\\", "")
     74   local tmp, is_unix = path:gsub("^/", "")
     75   return (is_win > 0) or (is_unix > 0)
     76 end
     77 
     78 function Set(source)
     79   local set = {}
     80   for _, l in ipairs(source) do set[l] = true end
     81   return set
     82 end
     83 
     84 ---------------------------
     85 -- More helper functions --
     86 ---------------------------
     87 
     88 -- Removes all keys from a table, without destroying the reference to it
     89 function clear_table(target)
     90   for key, value in pairs(target) do
     91     target[key] = nil
     92   end
     93 end
     94 function shallow_copy(target)
     95   local copy = {}
     96   for k, v in pairs(target) do
     97     copy[k] = v
     98   end
     99   return copy
    100 end
    101 
    102 -- Rounds to given decimals. eg. round_dec(3.145, 0) => 3
    103 function round_dec(num, idp)
    104   local mult = 10^(idp or 0)
    105   return math.floor(num * mult + 0.5) / mult
    106 end
    107 
    108 function file_exists(name)
    109   local f = io.open(name, "rb")
    110   if f ~= nil then
    111     local ok, err, code = f:read(1)
    112     io.close(f)
    113     return code == nil
    114   else
    115     return false
    116   end
    117 end
    118 
    119 function path_exists(name)
    120   local f = io.open(name, "rb")
    121   if f ~= nil then
    122     io.close(f)
    123     return true
    124   else
    125     return false
    126   end
    127 end
    128 
    129 function create_directories(path)
    130   local cmd
    131   if ON_WINDOWS then
    132     cmd = { args = {"cmd", "/c", "mkdir", path} }
    133   else
    134     cmd = { args = {"mkdir", "-p", path} }
    135   end
    136   utils.subprocess(cmd)
    137 end
    138 
    139 -- Find an executable in PATH or CWD with the given name
    140 function find_executable(name)
    141   local delim = ON_WINDOWS and ";" or ":"
    142 
    143   local pwd = os.getenv("PWD") or utils.getcwd()
    144   local path = os.getenv("PATH")
    145 
    146   local env_path = pwd .. delim .. path -- Check CWD first
    147 
    148   local result, filename
    149   for path_dir in env_path:gmatch("[^"..delim.."]+") do
    150     filename = join_paths(path_dir, name)
    151     if file_exists(filename) then
    152       result = filename
    153       break
    154     end
    155   end
    156 
    157   return result
    158 end
    159 
    160 local ExecutableFinder = { path_cache = {} }
    161 -- Searches for an executable and caches the result if any
    162 function ExecutableFinder:get_executable_path( name, raw_name )
    163   name = ON_WINDOWS and not raw_name and (name .. ".exe") or name
    164 
    165   if self.path_cache[name] == nil then
    166     self.path_cache[name] = find_executable(name) or false
    167   end
    168   return self.path_cache[name]
    169 end
    170 
    171 -- Format seconds to HH.MM.SS.sss
    172 function format_time(seconds, sep, decimals)
    173   decimals = decimals == nil and 3 or decimals
    174   sep = sep and sep or "."
    175   local s = seconds
    176   local h, s = divmod(s, 60*60)
    177   local m, s = divmod(s, 60)
    178 
    179   local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals)
    180 
    181   return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s)
    182 end
    183 
    184 -- Format seconds to 1h 2m 3.4s
    185 function format_time_hms(seconds, sep, decimals, force_full)
    186   decimals = decimals == nil and 1 or decimals
    187   sep = sep ~= nil and sep or " "
    188 
    189   local s = seconds
    190   local h, s = divmod(s, 60*60)
    191   local m, s = divmod(s, 60)
    192 
    193   if force_full or h > 0 then
    194     return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s)
    195   elseif m > 0 then
    196     return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s)
    197   else
    198     return string.format("%." .. tostring(decimals) .. "fs", s)
    199   end
    200 end
    201 
    202 -- Writes text on OSD and console
    203 function log_info(txt, timeout)
    204   timeout = timeout or 1.5
    205   msg.info(txt)
    206   mp.osd_message(txt, timeout)
    207 end
    208 
    209 -- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-"
    210 function join_table(source, before, after, sep)
    211   before = before or ""
    212   after = after or ""
    213   sep = sep or ", "
    214   local result = ""
    215   for i, v in pairs(source) do
    216     if not isempty(v) then
    217       local part = before .. v .. after
    218       if i == 1 then
    219         result = part
    220       else
    221         result = result .. sep .. part
    222       end
    223     end
    224   end
    225   return result
    226 end
    227 
    228 function wrap(s, char)
    229   char = char or "'"
    230   return char .. s .. char
    231 end
    232 -- Wraps given string into 'string' and escapes any 's in it
    233 function escape_and_wrap(s, char, replacement)
    234   char = char or "'"
    235   replacement = replacement or "\\" .. char
    236   return wrap(string.gsub(s, char, replacement), char)
    237 end
    238 -- Escapes single quotes in a string and wraps the input in single quotes
    239 function escape_single_bash(s)
    240   return escape_and_wrap(s, "'", "'\\''")
    241 end
    242 
    243 -- Returns (a .. b) if b is not empty or nil
    244 function joined_or_nil(a, b)
    245   return not isempty(b) and (a .. b) or nil
    246 end
    247 
    248 -- Put items from one table into another
    249 function extend_table(target, source)
    250   for i, v in pairs(source) do
    251     table.insert(target, v)
    252   end
    253 end
    254 
    255 -- Creates a handle and filename for a temporary random file (in current directory)
    256 function create_temporary_file(base, mode, suffix)
    257   local handle, filename
    258   suffix = suffix or ""
    259   while true do
    260     filename = base .. tostring(math.random(1, 5000)) .. suffix
    261     handle = io.open(filename, "r")
    262     if not handle then
    263       handle = io.open(filename, mode)
    264       break
    265     end
    266     io.close(handle)
    267   end
    268   return handle, filename
    269 end
    270 
    271 
    272 function get_processor_count()
    273   local proc_count
    274 
    275   if ON_WINDOWS then
    276     proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS"))
    277   else
    278     local cpuinfo_handle = io.open("/proc/cpuinfo")
    279     if cpuinfo_handle ~= nil then
    280       local cpuinfo_contents = cpuinfo_handle:read("*a")
    281       local _, replace_count = cpuinfo_contents:gsub('processor', '')
    282       proc_count = replace_count
    283     end
    284   end
    285 
    286   if proc_count and proc_count > 0 then
    287       return proc_count
    288   else
    289     return nil
    290   end
    291 end
    292 
    293 function substitute_values(string, values)
    294   local substitutor = function(match)
    295     if match == "%" then
    296        return "%"
    297     else
    298       -- nil is discarded by gsub
    299       return values[match]
    300     end
    301   end
    302 
    303   local substituted = string:gsub('%%(.)', substitutor)
    304   return substituted
    305 end
    306 
    307 -- ASS HELPERS --
    308 function round_rect_top( ass, x0, y0, x1, y1, r )
    309   local c = 0.551915024494 * r -- circle approximation
    310   ass:move_to(x0 + r, y0)
    311   ass:line_to(x1 - r, y0) -- top line
    312   if r > 0 then
    313       ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner
    314   end
    315   ass:line_to(x1, y1) -- right line
    316   ass:line_to(x0, y1) -- bottom line
    317   ass:line_to(x0, y0 + r) -- left line
    318   if r > 0 then
    319       ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner
    320   end
    321 end
    322 
    323 function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl)
    324     local c = 0.551915024494
    325     ass:move_to(x0 + rtl, y0)
    326     ass:line_to(x1 - rtr, y0) -- top line
    327     if rtr > 0 then
    328         ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner
    329     end
    330     ass:line_to(x1, y1 - rbr) -- right line
    331     if rbr > 0 then
    332         ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner
    333     end
    334     ass:line_to(x0 + rbl, y1) -- bottom line
    335     if rbl > 0 then
    336         ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner
    337     end
    338     ass:line_to(x0, y0 + rtl) -- left line
    339     if rtl > 0 then
    340         ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner
    341     end
    342 end
    343 -- $Revision: 1.5 $
    344 -- $Date: 2014-09-10 16:54:25 $
    345 
    346 -- This module was originally taken from http://cube3d.de/uploads/Main/sha1.txt.
    347 
    348 -------------------------------------------------------------------------------
    349 -- SHA-1 secure hash computation, and HMAC-SHA1 signature computation,
    350 -- in pure Lua (tested on Lua 5.1)
    351 -- License: MIT
    352 --
    353 -- Usage:
    354 -- local hashAsHex = sha1.hex(message) -- returns a hex string
    355 -- local hashAsData = sha1.bin(message) -- returns raw bytes
    356 --
    357 -- local hmacAsHex = sha1.hmacHex(key, message) -- hex string
    358 -- local hmacAsData = sha1.hmacBin(key, message) -- raw bytes
    359 --
    360 --
    361 -- Pass sha1.hex() a string, and it returns a hash as a 40-character hex string.
    362 -- For example, the call
    363 --
    364 -- local hash = sha1.hex("iNTERFACEWARE")
    365 --
    366 -- puts the 40-character string
    367 --
    368 -- "e76705ffb88a291a0d2f9710a5471936791b4819"
    369 --
    370 -- into the variable 'hash'
    371 --
    372 -- Pass sha1.hmacHex() a key and a message, and it returns the signature as a
    373 -- 40-byte hex string.
    374 --
    375 --
    376 -- The two "bin" versions do the same, but return the 20-byte string of raw
    377 -- data that the 40-byte hex strings represent.
    378 --
    379 -------------------------------------------------------------------------------
    380 --
    381 -- Description
    382 -- Due to the lack of bitwise operations in 5.1, this version uses numbers to
    383 -- represents the 32bit words that we combine with binary operations. The basic
    384 -- operations of byte based "xor", "or", "and" are all cached in a combination
    385 -- table (several 64k large tables are built on startup, which
    386 -- consumes some memory and time). The caching can be switched off through
    387 -- setting the local cfg_caching variable to false.
    388 -- For all binary operations, the 32 bit numbers are split into 8 bit values
    389 -- that are combined and then merged again.
    390 --
    391 -- Algorithm: http://www.itl.nist.gov/fipspubs/fip180-1.htm
    392 --
    393 -------------------------------------------------------------------------------
    394 
    395 local sha1 = (function()
    396 local sha1 = {}
    397 
    398 -- set this to false if you don't want to build several 64k sized tables when
    399 -- loading this file (takes a while but grants a boost of factor 13)
    400 local cfg_caching = false
    401 -- local storing of global functions (minor speedup)
    402 local floor,modf = math.floor,math.modf
    403 local char,format,rep = string.char,string.format,string.rep
    404 
    405 -- merge 4 bytes to an 32 bit word
    406 local function bytes_to_w32 (a,b,c,d) return a*0x1000000+b*0x10000+c*0x100+d end
    407 -- split a 32 bit word into four 8 bit numbers
    408 local function w32_to_bytes (i)
    409    return floor(i/0x1000000)%0x100,floor(i/0x10000)%0x100,floor(i/0x100)%0x100,i%0x100
    410 end
    411 
    412 -- shift the bits of a 32 bit word. Don't use negative values for "bits"
    413 local function w32_rot (bits,a)
    414    local b2 = 2^(32-bits)
    415    local a,b = modf(a/b2)
    416    return a+b*b2*(2^(bits))
    417 end
    418 
    419 -- caching function for functions that accept 2 arguments, both of values between
    420 -- 0 and 255. The function to be cached is passed, all values are calculated
    421 -- during loading and a function is returned that returns the cached values (only)
    422 local function cache2arg (fn)
    423    if not cfg_caching then return fn end
    424    local lut = {}
    425    for i=0,0xffff do
    426       local a,b = floor(i/0x100),i%0x100
    427       lut[i] = fn(a,b)
    428    end
    429    return function (a,b)
    430       return lut[a*0x100+b]
    431    end
    432 end
    433 
    434 -- splits an 8-bit number into 8 bits, returning all 8 bits as booleans
    435 local function byte_to_bits (b)
    436    local b = function (n)
    437       local b = floor(b/n)
    438       return b%2==1
    439    end
    440    return b(1),b(2),b(4),b(8),b(16),b(32),b(64),b(128)
    441 end
    442 
    443 -- builds an 8bit number from 8 booleans
    444 local function bits_to_byte (a,b,c,d,e,f,g,h)
    445    local function n(b,x) return b and x or 0 end
    446    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)
    447 end
    448 
    449 -- debug function for visualizing bits in a string
    450 local function bits_to_string (a,b,c,d,e,f,g,h)
    451    local function x(b) return b and "1" or "0" end
    452    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))
    453 end
    454 
    455 -- debug function for converting a 8-bit number as bit string
    456 local function byte_to_bit_string (b)
    457    return bits_to_string(byte_to_bits(b))
    458 end
    459 
    460 -- debug function for converting a 32 bit number as bit string
    461 local function w32_to_bit_string(a)
    462    if type(a) == "string" then return a end
    463    local aa,ab,ac,ad = w32_to_bytes(a)
    464    local s = byte_to_bit_string
    465    return ("%s %s %s %s"):format(s(aa):reverse(),s(ab):reverse(),s(ac):reverse(),s(ad):reverse()):reverse()
    466 end
    467 
    468 -- bitwise "and" function for 2 8bit number
    469 local band = cache2arg (function(a,b)
    470       local A,B,C,D,E,F,G,H = byte_to_bits(b)
    471       local a,b,c,d,e,f,g,h = byte_to_bits(a)
    472       return bits_to_byte(
    473          A and a, B and b, C and c, D and d,
    474          E and e, F and f, G and g, H and h)
    475    end)
    476 
    477 -- bitwise "or" function for 2 8bit numbers
    478 local bor = cache2arg(function(a,b)
    479       local A,B,C,D,E,F,G,H = byte_to_bits(b)
    480       local a,b,c,d,e,f,g,h = byte_to_bits(a)
    481       return bits_to_byte(
    482          A or a, B or b, C or c, D or d,
    483          E or e, F or f, G or g, H or h)
    484    end)
    485 
    486 -- bitwise "xor" function for 2 8bit numbers
    487 local bxor = cache2arg(function(a,b)
    488       local A,B,C,D,E,F,G,H = byte_to_bits(b)
    489       local a,b,c,d,e,f,g,h = byte_to_bits(a)
    490       return bits_to_byte(
    491          A ~= a, B ~= b, C ~= c, D ~= d,
    492          E ~= e, F ~= f, G ~= g, H ~= h)
    493    end)
    494 
    495 -- bitwise complement for one 8bit number
    496 local function bnot (x)
    497    return 255-(x % 256)
    498 end
    499 
    500 -- creates a function to combine to 32bit numbers using an 8bit combination function
    501 local function w32_comb(fn)
    502    return function (a,b)
    503       local aa,ab,ac,ad = w32_to_bytes(a)
    504       local ba,bb,bc,bd = w32_to_bytes(b)
    505       return bytes_to_w32(fn(aa,ba),fn(ab,bb),fn(ac,bc),fn(ad,bd))
    506    end
    507 end
    508 
    509 -- create functions for and, xor and or, all for 2 32bit numbers
    510 local w32_and = w32_comb(band)
    511 local w32_xor = w32_comb(bxor)
    512 local w32_or = w32_comb(bor)
    513 
    514 -- xor function that may receive a variable number of arguments
    515 local function w32_xor_n (a,...)
    516    local aa,ab,ac,ad = w32_to_bytes(a)
    517    for i=1,select('#',...) do
    518       local ba,bb,bc,bd = w32_to_bytes(select(i,...))
    519       aa,ab,ac,ad = bxor(aa,ba),bxor(ab,bb),bxor(ac,bc),bxor(ad,bd)
    520    end
    521    return bytes_to_w32(aa,ab,ac,ad)
    522 end
    523 
    524 -- combining 3 32bit numbers through binary "or" operation
    525 local function w32_or3 (a,b,c)
    526    local aa,ab,ac,ad = w32_to_bytes(a)
    527    local ba,bb,bc,bd = w32_to_bytes(b)
    528    local ca,cb,cc,cd = w32_to_bytes(c)
    529    return bytes_to_w32(
    530       bor(aa,bor(ba,ca)), bor(ab,bor(bb,cb)), bor(ac,bor(bc,cc)), bor(ad,bor(bd,cd))
    531    )
    532 end
    533 
    534 -- binary complement for 32bit numbers
    535 local function w32_not (a)
    536    return 4294967295-(a % 4294967296)
    537 end
    538 
    539 -- adding 2 32bit numbers, cutting off the remainder on 33th bit
    540 local function w32_add (a,b) return (a+b) % 4294967296 end
    541 
    542 -- adding n 32bit numbers, cutting off the remainder (again)
    543 local function w32_add_n (a,...)
    544    for i=1,select('#',...) do
    545       a = (a+select(i,...)) % 4294967296
    546    end
    547    return a
    548 end
    549 -- converting the number to a hexadecimal string
    550 local function w32_to_hexstring (w) return format("%08x",w) end
    551 
    552 -- calculating the SHA1 for some text
    553 function sha1.hex(msg)
    554    local H0,H1,H2,H3,H4 = 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0xC3D2E1F0
    555    local msg_len_in_bits = #msg * 8
    556 
    557    local first_append = char(0x80) -- append a '1' bit plus seven '0' bits
    558 
    559    local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length
    560    local current_mod = non_zero_message_bytes % 64
    561    local second_append = current_mod>0 and rep(char(0), 64 - current_mod) or ""
    562 
    563    -- now to append the length as a 64-bit number.
    564    local B1, R1 = modf(msg_len_in_bits / 0x01000000)
    565    local B2, R2 = modf( 0x01000000 * R1 / 0x00010000)
    566    local B3, R3 = modf( 0x00010000 * R2 / 0x00000100)
    567    local B4 = 0x00000100 * R3
    568 
    569    local L64 = char( 0) .. char( 0) .. char( 0) .. char( 0) -- high 32 bits
    570    .. char(B1) .. char(B2) .. char(B3) .. char(B4) -- low 32 bits
    571 
    572    msg = msg .. first_append .. second_append .. L64
    573 
    574    assert(#msg % 64 == 0)
    575 
    576    local chunks = #msg / 64
    577 
    578    local W = { }
    579    local start, A, B, C, D, E, f, K, TEMP
    580    local chunk = 0
    581 
    582    while chunk < chunks do
    583       --
    584       -- break chunk up into W[0] through W[15]
    585       --
    586       start,chunk = chunk * 64 + 1,chunk + 1
    587 
    588       for t = 0, 15 do
    589          W[t] = bytes_to_w32(msg:byte(start, start + 3))
    590          start = start + 4
    591       end
    592 
    593       --
    594       -- build W[16] through W[79]
    595       --
    596       for t = 16, 79 do
    597          -- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16).
    598          W[t] = w32_rot(1, w32_xor_n(W[t-3], W[t-8], W[t-14], W[t-16]))
    599       end
    600 
    601       A,B,C,D,E = H0,H1,H2,H3,H4
    602 
    603       for t = 0, 79 do
    604          if t <= 19 then
    605             -- (B AND C) OR ((NOT B) AND D)
    606             f = w32_or(w32_and(B, C), w32_and(w32_not(B), D))
    607             K = 0x5A827999
    608          elseif t <= 39 then
    609             -- B XOR C XOR D
    610             f = w32_xor_n(B, C, D)
    611             K = 0x6ED9EBA1
    612          elseif t <= 59 then
    613             -- (B AND C) OR (B AND D) OR (C AND D
    614             f = w32_or3(w32_and(B, C), w32_and(B, D), w32_and(C, D))
    615             K = 0x8F1BBCDC
    616          else
    617             -- B XOR C XOR D
    618             f = w32_xor_n(B, C, D)
    619             K = 0xCA62C1D6
    620          end
    621 
    622          -- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt;
    623          A,B,C,D,E = w32_add_n(w32_rot(5, A), f, E, W[t], K),
    624          A, w32_rot(30, B), C, D
    625       end
    626       -- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E.
    627       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)
    628    end
    629    local f = w32_to_hexstring
    630    return f(H0) .. f(H1) .. f(H2) .. f(H3) .. f(H4)
    631 end
    632 
    633 local function hex_to_binary(hex)
    634    return hex:gsub('..', function(hexval)
    635          return string.char(tonumber(hexval, 16))
    636       end)
    637 end
    638 
    639 function sha1.bin(msg)
    640    return hex_to_binary(sha1.hex(msg))
    641 end
    642 
    643 local xor_with_0x5c = {}
    644 local xor_with_0x36 = {}
    645 -- building the lookuptables ahead of time (instead of littering the source code
    646 -- with precalculated values)
    647 for i=0,0xff do
    648    xor_with_0x5c[char(i)] = char(bxor(i,0x5c))
    649    xor_with_0x36[char(i)] = char(bxor(i,0x36))
    650 end
    651 
    652 local blocksize = 64 -- 512 bits
    653 
    654 function sha1.hmacHex(key, text)
    655    assert(type(key) == 'string', "key passed to hmacHex should be a string")
    656    assert(type(text) == 'string', "text passed to hmacHex should be a string")
    657 
    658    if #key > blocksize then
    659       key = sha1.bin(key)
    660    end
    661 
    662    local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), blocksize - #key)
    663    local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), blocksize - #key)
    664 
    665    return sha1.hex(key_xord_with_0x5c .. sha1.bin(key_xord_with_0x36 .. text))
    666 end
    667 
    668 function sha1.hmacBin(key, text)
    669    return hex_to_binary(sha1.hmacHex(key, text))
    670 end
    671 
    672 return sha1
    673 end)()
    674 
    675 local SCRIPT_NAME = "mpv_thumbnail_script"
    676 
    677 local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/"
    678 
    679 local thumbnailer_options = {
    680     -- The thumbnail directory
    681     cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"),
    682 
    683     ------------------------
    684     -- Generation options --
    685     ------------------------
    686 
    687     -- Automatically generate the thumbnails on video load, without a keypress
    688     autogenerate = true,
    689 
    690     -- Only automatically thumbnail videos shorter than this (seconds)
    691     autogenerate_max_duration = 3600, -- 1 hour
    692 
    693     -- SHA1-sum filenames over this length
    694     -- It's nice to know what files the thumbnails are (hence directory names)
    695     -- but long URLs may approach filesystem limits.
    696     hash_filename_length = 128,
    697 
    698     -- Use mpv to generate thumbnail even if ffmpeg is found in PATH
    699     -- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)!
    700     -- mpv is a bit slower, but has better support overall (eg. subtitles in the previews)
    701     prefer_mpv = true,
    702 
    703     -- Explicitly disable subtitles on the mpv sub-calls
    704     mpv_no_sub = false,
    705     -- Add a "--no-config" to the mpv sub-call arguments
    706     mpv_no_config = false,
    707     -- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments
    708     -- Use "" to disable
    709     mpv_profile = "",
    710     -- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log
    711     -- The logs are removed after successful encodes, unless you set mpv_keep_logs below
    712     mpv_logs = true,
    713     -- Keep all mpv logs, even the succesfull ones
    714     mpv_keep_logs = false,
    715 
    716     -- Disable the built-in keybind ("T") to add your own
    717     disable_keybinds = false,
    718 
    719     ---------------------
    720     -- Display options --
    721     ---------------------
    722 
    723     -- Move the thumbnail up or down
    724     -- For example:
    725     --   topbar/bottombar: 24
    726     --   rest: 0
    727     vertical_offset = 24,
    728 
    729     -- Adjust background padding
    730     -- Examples:
    731     --   topbar:       0, 10, 10, 10
    732     --   bottombar:   10,  0, 10, 10
    733     --   slimbox/box: 10, 10, 10, 10
    734     pad_top   = 10,
    735     pad_bot   =  0,
    736     pad_left  = 10,
    737     pad_right = 10,
    738 
    739     -- If true, pad values are screen-pixels. If false, video-pixels.
    740     pad_in_screenspace = true,
    741     -- Calculate pad into the offset
    742     offset_by_pad = true,
    743 
    744     -- Background color in BBGGRR
    745     background_color = "000000",
    746     -- Alpha: 0 - fully opaque, 255 - transparent
    747     background_alpha = 80,
    748 
    749     -- Keep thumbnail on the screen near left or right side
    750     constrain_to_screen = true,
    751 
    752     -- Do not display the thumbnailing progress
    753     hide_progress = false,
    754 
    755     -----------------------
    756     -- Thumbnail options --
    757     -----------------------
    758 
    759     -- The maximum dimensions of the thumbnails (pixels)
    760     thumbnail_width = 200,
    761     thumbnail_height = 200,
    762 
    763     -- The thumbnail count target
    764     -- (This will result in a thumbnail every ~10 seconds for a 25 minute video)
    765     thumbnail_count = 150,
    766 
    767     -- The above target count will be adjusted by the minimum and
    768     -- maximum time difference between thumbnails.
    769     -- The thumbnail_count will be used to calculate a target separation,
    770     -- and min/max_delta will be used to constrict it.
    771 
    772     -- In other words, thumbnails will be:
    773     --   at least min_delta seconds apart (limiting the amount)
    774     --   at most max_delta seconds apart (raising the amount if needed)
    775     min_delta = 5,
    776     -- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours!
    777     max_delta = 90,
    778 
    779 
    780     -- Overrides for remote urls (you generally want less thumbnails!)
    781     -- Thumbnailing network paths will be done with mpv
    782 
    783     -- Allow thumbnailing network paths (naive check for "://")
    784     thumbnail_network = false,
    785     -- Override thumbnail count, min/max delta
    786     remote_thumbnail_count = 60,
    787     remote_min_delta = 15,
    788     remote_max_delta = 120,
    789 
    790     -- Try to grab the raw stream and disable ytdl for the mpv subcalls
    791     -- Much faster than passing the url to ytdl again, but may cause problems with some sites
    792     remote_direct_stream = true,
    793 }
    794 
    795 read_options(thumbnailer_options, SCRIPT_NAME)
    796 local Thumbnailer = {
    797     cache_directory = thumbnailer_options.cache_directory,
    798 
    799     state = {
    800         ready = false,
    801         available = false,
    802         enabled = false,
    803 
    804         thumbnail_template = nil,
    805 
    806         thumbnail_delta = nil,
    807         thumbnail_count = 0,
    808 
    809         thumbnail_size = nil,
    810 
    811         finished_thumbnails = 0,
    812 
    813         -- List of thumbnail states (from 1 to thumbnail_count)
    814         -- ready: 1
    815         -- in progress: 0
    816         -- not ready: -1
    817         thumbnails = {},
    818 
    819         worker_input_path = nil,
    820         -- Extra options for the workers
    821         worker_extra = {},
    822     },
    823     -- Set in register_client
    824     worker_register_timeout = nil,
    825     -- A timer used to wait for more workers in case we have none
    826     worker_wait_timer = nil,
    827     workers = {}
    828 }
    829 
    830 function Thumbnailer:clear_state()
    831     clear_table(self.state)
    832     self.state.ready = false
    833     self.state.available = false
    834     self.state.finished_thumbnails = 0
    835     self.state.thumbnails = {}
    836     self.state.worker_extra = {}
    837 end
    838 
    839 
    840 function Thumbnailer:on_file_loaded()
    841     self:clear_state()
    842 end
    843 
    844 function Thumbnailer:on_thumb_ready(index)
    845     self.state.thumbnails[index] = 1
    846 
    847     -- Full recount instead of a naive increment (let's be safe!)
    848     self.state.finished_thumbnails = 0
    849     for i, v in pairs(self.state.thumbnails) do
    850         if v > 0 then
    851             self.state.finished_thumbnails = self.state.finished_thumbnails + 1
    852         end
    853     end
    854 end
    855 
    856 function Thumbnailer:on_thumb_progress(index)
    857     self.state.thumbnails[index] = math.max(self.state.thumbnails[index], 0)
    858 end
    859 
    860 function Thumbnailer:on_start_file()
    861     -- Clear state when a new file is being loaded
    862     self:clear_state()
    863 end
    864 
    865 function Thumbnailer:on_video_change(params)
    866     -- Gather a new state when we get proper video-dec-params and our state is empty
    867     if params ~= nil then
    868         if not self.state.ready then
    869             self:update_state()
    870         end
    871     end
    872 end
    873 
    874 
    875 function Thumbnailer:update_state()
    876     msg.debug("Gathering video/thumbnail state")
    877 
    878     self.state.thumbnail_delta = self:get_delta()
    879     self.state.thumbnail_count = self:get_thumbnail_count(self.state.thumbnail_delta)
    880 
    881     -- Prefill individual thumbnail states
    882     for i = 1, self.state.thumbnail_count do
    883         self.state.thumbnails[i] = -1
    884     end
    885 
    886     self.state.thumbnail_template, self.state.thumbnail_directory = self:get_thumbnail_template()
    887     self.state.thumbnail_size = self:get_thumbnail_size()
    888 
    889     self.state.ready = true
    890 
    891     local file_path = mp.get_property_native("path")
    892     self.state.is_remote = file_path:find("://") ~= nil
    893 
    894     self.state.available = false
    895 
    896     -- Make sure the file has video (and not just albumart)
    897     local track_list = mp.get_property_native("track-list")
    898     local has_video = false
    899     for i, track in pairs(track_list) do
    900         if track.type == "video" and not track.external and not track.albumart then
    901             has_video = true
    902             break
    903         end
    904     end
    905 
    906     if has_video and self.state.thumbnail_delta ~= nil and self.state.thumbnail_size ~= nil and self.state.thumbnail_count > 0 then
    907         self.state.available = true
    908     end
    909 
    910     msg.debug("Thumbnailer.state:", utils.to_string(self.state))
    911 
    912 end
    913 
    914 
    915 function Thumbnailer:get_thumbnail_template()
    916     local file_path = mp.get_property_native("path")
    917     local is_remote = file_path:find("://") ~= nil
    918 
    919     local filename = mp.get_property_native("filename/no-ext")
    920     local filesize = mp.get_property_native("file-size", 0)
    921 
    922     if is_remote then
    923         filesize = 0
    924     end
    925 
    926     filename = filename:gsub('[^a-zA-Z0-9_.%-\' ]', '')
    927     -- Hash overly long filenames (most likely URLs)
    928     if #filename > thumbnailer_options.hash_filename_length then
    929         filename = sha1.hex(filename)
    930     end
    931 
    932     local file_key = ("%s-%d"):format(filename, filesize)
    933 
    934     local thumbnail_directory = join_paths(self.cache_directory, file_key)
    935     local file_template = join_paths(thumbnail_directory, "%06d.bgra")
    936     return file_template, thumbnail_directory
    937 end
    938 
    939 
    940 function Thumbnailer:get_thumbnail_size()
    941     local video_dec_params = mp.get_property_native("video-dec-params")
    942     local video_width = video_dec_params.dw
    943     local video_height = video_dec_params.dh
    944     if not (video_width and video_height) then
    945         return nil
    946     end
    947 
    948     local w, h
    949     if video_width > video_height then
    950         w = thumbnailer_options.thumbnail_width
    951         h = math.floor(video_height * (w / video_width))
    952     else
    953         h = thumbnailer_options.thumbnail_height
    954         w = math.floor(video_width * (h / video_height))
    955     end
    956     return { w=w, h=h }
    957 end
    958 
    959 
    960 function Thumbnailer:get_delta()
    961     local file_path = mp.get_property_native("path")
    962     local file_duration = mp.get_property_native("duration")
    963     local is_seekable = mp.get_property_native("seekable")
    964 
    965     -- Naive url check
    966     local is_remote = file_path:find("://") ~= nil
    967 
    968     local remote_and_disallowed = is_remote
    969     if is_remote and thumbnailer_options.thumbnail_network then
    970         remote_and_disallowed = false
    971     end
    972 
    973     if remote_and_disallowed or not is_seekable or not file_duration then
    974         -- Not a local path (or remote thumbnails allowed), not seekable or lacks duration
    975         return nil
    976     end
    977 
    978     local thumbnail_count = thumbnailer_options.thumbnail_count
    979     local min_delta = thumbnailer_options.min_delta
    980     local max_delta = thumbnailer_options.max_delta
    981 
    982     if is_remote then
    983         thumbnail_count = thumbnailer_options.remote_thumbnail_count
    984         min_delta = thumbnailer_options.remote_min_delta
    985         max_delta = thumbnailer_options.remote_max_delta
    986     end
    987 
    988     local target_delta = (file_duration / thumbnail_count)
    989     local delta = math.max(min_delta, math.min(max_delta, target_delta))
    990 
    991     return delta
    992 end
    993 
    994 
    995 function Thumbnailer:get_thumbnail_count(delta)
    996     if delta == nil then
    997         return 0
    998     end
    999     local file_duration = mp.get_property_native("duration")
   1000 
   1001     return math.ceil(file_duration / delta)
   1002 end
   1003 
   1004 function Thumbnailer:get_closest(thumbnail_index)
   1005     -- Given a 1-based index, find the closest available thumbnail and return it's 1-based index
   1006 
   1007     -- Check the direct thumbnail index first
   1008     if self.state.thumbnails[thumbnail_index] > 0 then
   1009         return thumbnail_index
   1010     end
   1011 
   1012     local min_distance = self.state.thumbnail_count + 1
   1013     local closest = nil
   1014 
   1015     -- Naive, inefficient, lazy. But functional.
   1016     for index, value in pairs(self.state.thumbnails) do
   1017         local distance = math.abs(index - thumbnail_index)
   1018         if distance < min_distance and value > 0 then
   1019             min_distance = distance
   1020             closest = index
   1021         end
   1022     end
   1023     return closest
   1024 end
   1025 
   1026 function Thumbnailer:get_thumbnail_index(time_position)
   1027     -- Returns a 1-based thumbnail index for the given timestamp (between 1 and thumbnail_count, inclusive)
   1028     if self.state.thumbnail_delta and (self.state.thumbnail_count and self.state.thumbnail_count > 0) then
   1029         return math.min(math.floor(time_position / self.state.thumbnail_delta) + 1, self.state.thumbnail_count)
   1030     else
   1031         return nil
   1032     end
   1033 end
   1034 
   1035 function Thumbnailer:get_thumbnail_path(time_position)
   1036     -- Given a timestamp, return:
   1037     --   the closest available thumbnail path (if any)
   1038     --   the 1-based thumbnail index calculated from the timestamp
   1039     --   the 1-based thumbnail index of the closest available (and used) thumbnail
   1040     -- OR nil if thumbnails are not available.
   1041 
   1042     local thumbnail_index = self:get_thumbnail_index(time_position)
   1043     if not thumbnail_index then return nil end
   1044 
   1045     local closest = self:get_closest(thumbnail_index)
   1046 
   1047     if closest ~= nil then
   1048         return self.state.thumbnail_template:format(closest-1), thumbnail_index, closest
   1049     else
   1050         return nil, thumbnail_index, nil
   1051     end
   1052 end
   1053 
   1054 function Thumbnailer:register_client()
   1055     self.worker_register_timeout = mp.get_time() + 2
   1056 
   1057     mp.register_script_message("mpv_thumbnail_script-ready", function(index, path)
   1058         self:on_thumb_ready(tonumber(index), path)
   1059     end)
   1060     mp.register_script_message("mpv_thumbnail_script-progress", function(index, path)
   1061         self:on_thumb_progress(tonumber(index), path)
   1062     end)
   1063 
   1064     mp.register_script_message("mpv_thumbnail_script-worker", function(worker_name)
   1065         if not self.workers[worker_name] then
   1066             msg.debug("Registered worker", worker_name)
   1067             self.workers[worker_name] = true
   1068             mp.commandv("script-message-to", worker_name, "mpv_thumbnail_script-slaved")
   1069         end
   1070     end)
   1071 
   1072     -- Notify workers to generate thumbnails when video loads/changes
   1073     -- This will be executed after the on_video_change (because it's registered after it)
   1074     mp.observe_property("video-dec-params", "native", function()
   1075         local duration = mp.get_property_native("duration")
   1076         local max_duration = thumbnailer_options.autogenerate_max_duration
   1077 
   1078         if duration ~= nil and self.state.available and thumbnailer_options.autogenerate then
   1079             -- Notify if autogenerate is on and video is not too long
   1080             if duration < max_duration or max_duration == 0 then
   1081                 self:start_worker_jobs()
   1082             end
   1083         end
   1084     end)
   1085 
   1086     local thumb_script_key = not thumbnailer_options.disable_keybinds and "T" or nil
   1087     mp.add_key_binding(thumb_script_key, "generate-thumbnails", function()
   1088         if self.state.available then
   1089             mp.osd_message("Started thumbnailer jobs")
   1090             self:start_worker_jobs()
   1091         else
   1092             mp.osd_message("Thumbnailing unavailabe")
   1093         end
   1094     end)
   1095 end
   1096 
   1097 function Thumbnailer:_create_thumbnail_job_order()
   1098     -- Returns a list of 1-based thumbnail indices in a job order
   1099     local used_frames = {}
   1100     local work_frames = {}
   1101 
   1102     -- Pick frames in increasing frequency.
   1103     -- This way we can do a quick few passes over the video and then fill in the gaps.
   1104     for x = 6, 0, -1 do
   1105         local nth = (2^x)
   1106 
   1107         for thi = 1, self.state.thumbnail_count, nth do
   1108             if not used_frames[thi] then
   1109                 table.insert(work_frames, thi)
   1110                 used_frames[thi] = true
   1111             end
   1112         end
   1113     end
   1114     return work_frames
   1115 end
   1116 
   1117 function Thumbnailer:prepare_source_path()
   1118     local file_path = mp.get_property_native("path")
   1119 
   1120     if self.state.is_remote and thumbnailer_options.remote_direct_stream then
   1121         -- Use the direct stream (possibly) provided by ytdl
   1122         -- This skips ytdl on the sub-calls, making the thumbnailing faster
   1123         -- Works well on YouTube, rest not really tested
   1124         file_path = mp.get_property_native("stream-path")
   1125 
   1126         -- edl:// urls can get LONG. In which case, save the path (URL)
   1127         -- to a temporary file and use that instead.
   1128         local playlist_filename = join_paths(self.state.thumbnail_directory, "playlist.txt")
   1129 
   1130         if #file_path > 8000 then
   1131             -- Path is too long for a playlist - just pass the original URL to
   1132             -- workers and allow ytdl
   1133             self.state.worker_extra.enable_ytdl = true
   1134             file_path = mp.get_property_native("path")
   1135             msg.warn("Falling back to original URL and ytdl due to LONG source path. This will be slow.")
   1136 
   1137         elseif #file_path > 1024 then
   1138             local playlist_file = io.open(playlist_filename, "wb")
   1139             if not playlist_file then
   1140                 msg.error(("Tried to write a playlist to %s but couldn't!"):format(playlist_file))
   1141                 return false
   1142             end
   1143 
   1144             playlist_file:write(file_path .. "\n")
   1145             playlist_file:close()
   1146 
   1147             file_path = "--playlist=" .. playlist_filename
   1148             msg.warn("Using playlist workaround due to long source path")
   1149         end
   1150     end
   1151 
   1152     self.state.worker_input_path = file_path
   1153     return true
   1154 end
   1155 
   1156 function Thumbnailer:start_worker_jobs()
   1157     -- Create directory for the thumbnails, if needed
   1158     local l, err = utils.readdir(self.state.thumbnail_directory)
   1159     if err then
   1160         msg.debug("Creating thumbnail directory", self.state.thumbnail_directory)
   1161         create_directories(self.state.thumbnail_directory)
   1162     end
   1163 
   1164     -- Try to prepare the source path for workers, and bail if unable to do so
   1165     if not self:prepare_source_path() then
   1166         return
   1167     end
   1168 
   1169     local worker_list = {}
   1170     for worker_name in pairs(self.workers) do table.insert(worker_list, worker_name) end
   1171 
   1172     local worker_count = #worker_list
   1173 
   1174     -- In case we have a worker timer created already, clear it
   1175     -- (For example, if the video-dec-params change in quick succession or the user pressed T, etc)
   1176     if self.worker_wait_timer then
   1177         self.worker_wait_timer:stop()
   1178     end
   1179 
   1180     if worker_count == 0 then
   1181         local now = mp.get_time()
   1182         if mp.get_time() > self.worker_register_timeout then
   1183             -- Workers have had their time to register but we have none!
   1184             local err = "No thumbnail workers found. Make sure you are not missing a script!"
   1185             msg.error(err)
   1186             mp.osd_message(err, 3)
   1187 
   1188         else
   1189             -- We may be too early. Delay the work start a bit to try again.
   1190             msg.warn("No workers found. Waiting a bit more for them.")
   1191             -- Wait at least half a second
   1192             local wait_time = math.max(self.worker_register_timeout - now, 0.5)
   1193             self.worker_wait_timer = mp.add_timeout(wait_time, function() self:start_worker_jobs() end)
   1194         end
   1195 
   1196     else
   1197         -- We have at least one worker. This may not be all of them, but they have had
   1198         -- their time to register; we've done our best waiting for them.
   1199         self.state.enabled = true
   1200 
   1201         msg.debug( ("Splitting %d thumbnails amongst %d worker(s)"):format(self.state.thumbnail_count, worker_count) )
   1202 
   1203         local frame_job_order = self:_create_thumbnail_job_order()
   1204         local worker_jobs = {}
   1205         for i = 1, worker_count do worker_jobs[worker_list[i]] = {} end
   1206 
   1207         -- Split frames amongst the workers
   1208         for i, thumbnail_index in ipairs(frame_job_order) do
   1209             local worker_id = worker_list[ ((i-1) % worker_count) + 1 ]
   1210             table.insert(worker_jobs[worker_id], thumbnail_index)
   1211         end
   1212 
   1213         local state_json_string = utils.format_json(self.state)
   1214         msg.debug("Giving workers state:", state_json_string)
   1215 
   1216         for worker_name, worker_frames in pairs(worker_jobs) do
   1217             if #worker_frames > 0 then
   1218                 local frames_json_string = utils.format_json(worker_frames)
   1219                 msg.debug("Assigning job to", worker_name, frames_json_string)
   1220                 mp.commandv("script-message-to", worker_name, "mpv_thumbnail_script-job", state_json_string, frames_json_string)
   1221             end
   1222         end
   1223     end
   1224 end
   1225 
   1226 mp.register_event("start-file", function() Thumbnailer:on_start_file() end)
   1227 mp.observe_property("video-dec-params", "native", function(name, params) Thumbnailer:on_video_change(params) end)
   1228 --[[
   1229 This is mpv's original player/lua/osc.lua patched to display thumbnails
   1230 
   1231 Sections are denoted with -- mpv_thumbnail_script.lua --
   1232 Current osc.lua version: 69fb2ddbc7b8ccec31348be4b7357ad9a8d48896
   1233 ]]--
   1234 
   1235 local assdraw = require 'mp.assdraw'
   1236 local msg = require 'mp.msg'
   1237 local opt = require 'mp.options'
   1238 local utils = require 'mp.utils'
   1239 
   1240 --
   1241 -- Parameters
   1242 --
   1243 -- default user option values
   1244 -- do not touch, change them in osc.conf
   1245 local user_opts = {
   1246     showwindowed = true,        -- show OSC when windowed?
   1247     showfullscreen = true,      -- show OSC when fullscreen?
   1248     scalewindowed = 1,          -- scaling of the controller when windowed
   1249     scalefullscreen = 1,        -- scaling of the controller when fullscreen
   1250     scaleforcedwindow = 2,      -- scaling when rendered on a forced window
   1251     vidscale = true,            -- scale the controller with the video?
   1252     valign = 0.8,               -- vertical alignment, -1 (top) to 1 (bottom)
   1253     halign = 0,                 -- horizontal alignment, -1 (left) to 1 (right)
   1254     barmargin = 0,              -- vertical margin of top/bottombar
   1255     boxalpha = 80,              -- alpha of the background box,
   1256                                 -- 0 (opaque) to 255 (fully transparent)
   1257     hidetimeout = 500,          -- duration in ms until the OSC hides if no
   1258                                 -- mouse movement. enforced non-negative for the
   1259                                 -- user, but internally negative is "always-on".
   1260     fadeduration = 200,         -- duration of fade out in ms, 0 = no fade
   1261     deadzonesize = 0.5,         -- size of deadzone
   1262     minmousemove = 0,           -- minimum amount of pixels the mouse has to
   1263                                 -- move between ticks to make the OSC show up
   1264     iamaprogrammer = false,     -- use native mpv values and disable OSC
   1265                                 -- internal track list management (and some
   1266                                 -- functions that depend on it)
   1267     layout = "bottombar",
   1268     seekbarstyle = "bar",       -- bar, diamond or knob
   1269     seekbarhandlesize = 0.6,    -- size ratio of the diamond and knob handle
   1270     seekrangestyle = "inverted",-- bar, line, slider, inverted or none
   1271     seekrangeseparate = true,   -- wether the seekranges overlay on the bar-style seekbar
   1272     seekrangealpha = 200,       -- transparency of seekranges
   1273     seekbarkeyframes = true,    -- use keyframes when dragging the seekbar
   1274     title = "${media-title}",   -- string compatible with property-expansion
   1275                                 -- to be shown as OSC title
   1276     tooltipborder = 1,          -- border of tooltip in bottom/topbar
   1277     timetotal = false,          -- display total time instead of remaining time?
   1278     timems = false,             -- display timecodes with milliseconds?
   1279     visibility = "auto",        -- only used at init to set visibility_mode(...)
   1280     boxmaxchars = 80,           -- title crop threshold for box layout
   1281     boxvideo = false,           -- apply osc_param.video_margins to video
   1282     windowcontrols = "auto",    -- whether to show window controls
   1283     windowcontrols_alignment = "right" -- which side to show window controls on
   1284 }
   1285 
   1286 -- read_options may modify hidetimeout, so save the original default value in
   1287 -- case the user set hidetimeout < 0 and we need the default instead.
   1288 local hidetimeout_def = user_opts.hidetimeout
   1289 -- read options from config and command-line
   1290 opt.read_options(user_opts, "osc")
   1291 if user_opts.hidetimeout < 0 then
   1292     user_opts.hidetimeout = hidetimeout_def
   1293     msg.warn("hidetimeout cannot be negative. Using " .. user_opts.hidetimeout)
   1294 end
   1295 
   1296 -- validate window control options
   1297 if user_opts.windowcontrols ~= "auto" and
   1298    user_opts.windowcontrols ~= "yes" and
   1299    user_opts.windowcontrols ~= "no" then
   1300     msg.warn("windowcontrols cannot be \"" ..
   1301              user_opts.windowcontrols .. "\". Ignoring.")
   1302     user_opts.windowcontrols = "auto"
   1303 end
   1304 if user_opts.windowcontrols_alignment ~= "right" and
   1305    user_opts.windowcontrols_alignment ~= "left" then
   1306     msg.warn("windowcontrols_alignment cannot be \"" ..
   1307              user_opts.windowcontrols_alignment .. "\". Ignoring.")
   1308     user_opts.windowcontrols_alignment = "right"
   1309 end
   1310 
   1311 -- mpv_thumbnail_script.lua --
   1312 
   1313 -- Patch in msg.trace
   1314 if not msg.trace then
   1315     msg.trace = function(...) return mp.log("trace", ...) end
   1316 end
   1317 
   1318 -- Patch in utils.format_bytes_humanized
   1319 if not utils.format_bytes_humanized then
   1320     utils.format_bytes_humanized = function(b)
   1321         local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"}
   1322         local i = 1
   1323         while b >= 1024 do
   1324             b = b / 1024
   1325             i = i + 1
   1326         end
   1327         return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1))
   1328     end
   1329 end
   1330 
   1331 Thumbnailer:register_client()
   1332 
   1333 function get_thumbnail_y_offset(thumb_size, msy)
   1334     local layout = user_opts.layout
   1335     local offset = 0
   1336 
   1337     if layout == "bottombar" then
   1338         offset = 15 --+ margin
   1339     elseif layout == "topbar" then
   1340         offset = -(thumb_size.h * msy + 15)
   1341     elseif layout == "box" then
   1342         offset = 15
   1343     elseif layout == "slimbox" then
   1344         offset = 12
   1345     end
   1346 
   1347     return offset / msy
   1348 end
   1349 
   1350 
   1351 local osc_thumb_state = {
   1352     visible = false,
   1353     overlay_id = 1,
   1354 
   1355     last_path = nil,
   1356     last_x = nil,
   1357     last_y = nil,
   1358 }
   1359 
   1360 function hide_thumbnail()
   1361     osc_thumb_state.visible = false
   1362     osc_thumb_state.last_path = nil
   1363     mp.command_native({ "overlay-remove", osc_thumb_state.overlay_id })
   1364 end
   1365 
   1366 function display_thumbnail(pos, value, ass)
   1367     -- If thumbnails are not available, bail
   1368     if not (Thumbnailer.state.enabled and Thumbnailer.state.available) then
   1369         return
   1370     end
   1371 
   1372     local duration = mp.get_property_number("duration", nil)
   1373     if not ((duration == nil) or (value == nil)) then
   1374         target_position = duration * (value / 100)
   1375 
   1376         local msx, msy = get_virt_scale_factor()
   1377         local osd_w, osd_h = mp.get_osd_size()
   1378 
   1379         local thumb_size = Thumbnailer.state.thumbnail_size
   1380         local thumb_path, thumb_index, closest_index = Thumbnailer:get_thumbnail_path(target_position)
   1381 
   1382         local thumbs_ready = Thumbnailer.state.finished_thumbnails
   1383         local thumbs_total = Thumbnailer.state.thumbnail_count
   1384         local perc = math.floor((thumbs_ready / thumbs_total) * 100)
   1385 
   1386         local display_progress = thumbs_ready ~= thumbs_total and not thumbnailer_options.hide_progress
   1387 
   1388         local vertical_offset = thumbnailer_options.vertical_offset
   1389         local padding = thumbnailer_options.background_padding
   1390 
   1391         local pad = {
   1392             l = thumbnailer_options.pad_left, r = thumbnailer_options.pad_right,
   1393             t = thumbnailer_options.pad_top, b = thumbnailer_options.pad_bot
   1394         }
   1395         if thumbnailer_options.pad_in_screenspace then
   1396             pad.l = pad.l * msx
   1397             pad.r = pad.r * msx
   1398             pad.t = pad.t * msy
   1399             pad.b = pad.b * msy
   1400         end
   1401 
   1402         if thumbnailer_options.offset_by_pad then
   1403             vertical_offset = vertical_offset + (user_opts.layout == "topbar" and pad.t or pad.b)
   1404         end
   1405 
   1406         local ass_w = thumb_size.w * msx
   1407         local ass_h = thumb_size.h * msy
   1408         local y_offset = get_thumbnail_y_offset(thumb_size, 1)
   1409 
   1410         -- Constrain thumbnail display to window
   1411         -- (ie. don't let it go off-screen left/right)
   1412         if thumbnailer_options.constrain_to_screen and osd_w > (ass_w + pad.l + pad.r)/msx then
   1413             local padded_left = (pad.l + (ass_w / 2))
   1414             local padded_right = (pad.r + (ass_w / 2))
   1415             if pos.x - padded_left < 0 then
   1416                 pos.x = padded_left
   1417             elseif pos.x + padded_right > osd_w*msx then
   1418                 pos.x = osd_w*msx - padded_right
   1419             end
   1420         end
   1421 
   1422         local text_h = 30 * msy
   1423         local bg_h = ass_h + (display_progress and text_h or 0)
   1424         local bg_left = pos.x - ass_w/2
   1425         local framegraph_h = 10 * msy
   1426 
   1427         local bg_top = nil
   1428         local text_top = nil
   1429         local framegraph_top = nil
   1430 
   1431         if user_opts.layout == "topbar" then
   1432             bg_top = pos.y - ( y_offset + thumb_size.h ) + vertical_offset
   1433             text_top = bg_top + ass_h + framegraph_h
   1434             framegraph_top = bg_top + ass_h
   1435             vertical_offset = -vertical_offset
   1436         else
   1437             bg_top = pos.y - y_offset - bg_h - vertical_offset
   1438             text_top = bg_top
   1439             framegraph_top = bg_top + 20 * msy
   1440         end
   1441 
   1442         if display_progress then
   1443             if user_opts.layout == "topbar" then
   1444                 pad.b = math.max(0, pad.b - 30)
   1445             else
   1446                 pad.t = math.max(0, pad.t - 30)
   1447             end
   1448         end
   1449 
   1450 
   1451 
   1452         -- Draw background
   1453         ass:new_event()
   1454         ass:pos(bg_left, bg_top)
   1455         ass:append(("{\\bord0\\1c&H%s&\\1a&H%X&}"):format(thumbnailer_options.background_color, thumbnailer_options.background_alpha))
   1456         ass:draw_start()
   1457         ass:rect_cw(-pad.l, -pad.t, ass_w+pad.r, bg_h+pad.b)
   1458         ass:draw_stop()
   1459 
   1460         if display_progress then
   1461 
   1462             ass:new_event()
   1463             ass:pos(pos.x, text_top)
   1464             ass:an(8)
   1465             -- Scale text to correct size
   1466             ass:append(("{\\fs20\\bord0\\fscx%f\\fscy%f}"):format(100*msx, 100*msy))
   1467             ass:append(("%d%% - %d/%d"):format(perc, thumbs_ready, thumbs_total))
   1468 
   1469             -- Draw the generation progress
   1470             local block_w = thumb_size.w * (Thumbnailer.state.thumbnail_delta / duration) * msy
   1471             local block_max_x = thumb_size.w * msy
   1472 
   1473             -- Draw finished thumbnail blocks (white)
   1474             ass:new_event()
   1475             ass:pos(bg_left, framegraph_top)
   1476             ass:append(("{\\bord0\\1c&HFFFFFF&\\1a&H%X&"):format(0))
   1477             ass:draw_start(2)
   1478             for i, v in pairs(Thumbnailer.state.thumbnails) do
   1479                 if i ~= closest_index and v > 0 then
   1480                     ass:rect_cw((i-1)*block_w, 0, math.min(block_max_x, i*block_w), framegraph_h)
   1481                 end
   1482             end
   1483             ass:draw_stop()
   1484 
   1485             -- Draw in-progress thumbnail blocks (grayish green)
   1486             ass:new_event()
   1487             ass:pos(bg_left, framegraph_top)
   1488             ass:append(("{\\bord0\\1c&H44AA44&\\1a&H%X&"):format(0))
   1489             ass:draw_start(2)
   1490             for i, v in pairs(Thumbnailer.state.thumbnails) do
   1491                 if i ~= closest_index and v == 0 then
   1492                     ass:rect_cw((i-1)*block_w, 0, math.min(block_max_x, i*block_w), framegraph_h)
   1493                 end
   1494             end
   1495             ass:draw_stop()
   1496 
   1497             if closest_index ~= nil then
   1498                 ass:new_event()
   1499                 ass:pos(bg_left, framegraph_top)
   1500                 ass:append(("{\\bord0\\1c&H4444FF&\\1a&H%X&"):format(0))
   1501                 ass:draw_start(2)
   1502                 ass:rect_cw((closest_index-1)*block_w, 0, math.min(block_max_x, closest_index*block_w), framegraph_h)
   1503                 ass:draw_stop()
   1504             end
   1505         end
   1506 
   1507         if thumb_path then
   1508             local overlay_y_offset = get_thumbnail_y_offset(thumb_size, msy)
   1509 
   1510             local thumb_x = math.floor(pos.x / msx - thumb_size.w/2)
   1511             local thumb_y = math.floor(pos.y / msy - thumb_size.h - overlay_y_offset - vertical_offset/msy)
   1512 
   1513             osc_thumb_state.visible = true
   1514             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
   1515                 local overlay_add_args = {
   1516                     "overlay-add", osc_thumb_state.overlay_id,
   1517                     thumb_x, thumb_y,
   1518                     thumb_path,
   1519                     0,
   1520                     "bgra",
   1521                     thumb_size.w, thumb_size.h,
   1522                     4 * thumb_size.w
   1523                 }
   1524                 mp.command_native(overlay_add_args)
   1525 
   1526                 osc_thumb_state.last_path = thumb_path
   1527                 osc_thumb_state.last_x = thumb_x
   1528                 osc_thumb_state.last_y = thumb_y
   1529             end
   1530         end
   1531     end
   1532 end
   1533 
   1534 -- // mpv_thumbnail_script.lua // --
   1535 
   1536 local osc_param = { -- calculated by osc_init()
   1537     playresy = 0,                           -- canvas size Y
   1538     playresx = 0,                           -- canvas size X
   1539     display_aspect = 1,
   1540     unscaled_y = 0,
   1541     areas = {},
   1542     video_margins = {
   1543         l = 0, r = 0, t = 0, b = 0,         -- left/right/top/bottom
   1544     },
   1545 }
   1546 
   1547 local osc_styles = {
   1548     bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
   1549     smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
   1550     smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}",
   1551     smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
   1552     topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
   1553 
   1554     elementDown = "{\\1c&H999999}",
   1555     timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
   1556     vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
   1557     box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
   1558 
   1559     topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
   1560     smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
   1561     timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
   1562     timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
   1563     vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
   1564 
   1565     wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}",
   1566     wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}",
   1567     wcBar = "{\\1c&H000000}",
   1568 }
   1569 
   1570 -- internal states, do not touch
   1571 local state = {
   1572     showtime,                               -- time of last invocation (last mouse move)
   1573     osc_visible = false,
   1574     anistart,                               -- time when the animation started
   1575     anitype,                                -- current type of animation
   1576     animation,                              -- current animation alpha
   1577     mouse_down_counter = 0,                 -- used for softrepeat
   1578     active_element = nil,                   -- nil = none, 0 = background, 1+ = see elements[]
   1579     active_event_source = nil,              -- the "button" that issued the current event
   1580     rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
   1581     tc_ms = user_opts.timems,               -- Should the timecodes display their time with milliseconds
   1582     mp_screen_sizeX, mp_screen_sizeY,       -- last screen-resolution, to detect resolution changes to issue reINITs
   1583     initREQ = false,                        -- is a re-init request pending?
   1584     last_mouseX, last_mouseY,               -- last mouse position, to detect significant mouse movement
   1585     message_text,
   1586     message_timeout,
   1587     fullscreen = false,
   1588     tick_timer = nil,
   1589     tick_last_time = 0,                     -- when the last tick() was run
   1590     cache_state = nil,
   1591     idle = false,
   1592     enabled = true,
   1593     input_enabled = true,
   1594     showhide_enabled = false,
   1595     dmx_cache = 0,
   1596     using_video_margins = false,
   1597     border = true,
   1598     maximized = false,
   1599 }
   1600 
   1601 local window_control_box_width = 80
   1602 local tick_delay = 0.03
   1603 
   1604 --
   1605 -- Helperfunctions
   1606 --
   1607 
   1608 local margins_opts = {
   1609     {"l", "video-margin-ratio-left"},
   1610     {"r", "video-margin-ratio-right"},
   1611     {"t", "video-margin-ratio-top"},
   1612     {"b", "video-margin-ratio-bottom"},
   1613 }
   1614 
   1615 -- scale factor for translating between real and virtual ASS coordinates
   1616 function get_virt_scale_factor()
   1617     local w, h = mp.get_osd_size()
   1618     if w <= 0 or h <= 0 then
   1619         return 0, 0
   1620     end
   1621     return osc_param.playresx / w, osc_param.playresy / h
   1622 end
   1623 
   1624 -- return mouse position in virtual ASS coordinates (playresx/y)
   1625 function get_virt_mouse_pos()
   1626     local sx, sy = get_virt_scale_factor()
   1627     local x, y = mp.get_mouse_pos()
   1628     return x * sx, y * sy
   1629 end
   1630 
   1631 function set_virt_mouse_area(x0, y0, x1, y1, name)
   1632     local sx, sy = get_virt_scale_factor()
   1633     mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
   1634 end
   1635 
   1636 function scale_value(x0, x1, y0, y1, val)
   1637     local m = (y1 - y0) / (x1 - x0)
   1638     local b = y0 - (m * x0)
   1639     return (m * val) + b
   1640 end
   1641 
   1642 -- returns hitbox spanning coordinates (top left, bottom right corner)
   1643 -- according to alignment
   1644 function get_hitbox_coords(x, y, an, w, h)
   1645 
   1646     local alignments = {
   1647       [1] = function () return x, y-h, x+w, y end,
   1648       [2] = function () return x-(w/2), y-h, x+(w/2), y end,
   1649       [3] = function () return x-w, y-h, x, y end,
   1650 
   1651       [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
   1652       [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
   1653       [6] = function () return x-w, y-(h/2), x, y+(h/2) end,
   1654 
   1655       [7] = function () return x, y, x+w, y+h end,
   1656       [8] = function () return x-(w/2), y, x+(w/2), y+h end,
   1657       [9] = function () return x-w, y, x, y+h end,
   1658     }
   1659 
   1660     return alignments[an]()
   1661 end
   1662 
   1663 function get_hitbox_coords_geo(geometry)
   1664     return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
   1665         geometry.w, geometry.h)
   1666 end
   1667 
   1668 function get_element_hitbox(element)
   1669     return element.hitbox.x1, element.hitbox.y1,
   1670         element.hitbox.x2, element.hitbox.y2
   1671 end
   1672 
   1673 function mouse_hit(element)
   1674     return mouse_hit_coords(get_element_hitbox(element))
   1675 end
   1676 
   1677 function mouse_hit_coords(bX1, bY1, bX2, bY2)
   1678     local mX, mY = get_virt_mouse_pos()
   1679     return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
   1680 end
   1681 
   1682 function limit_range(min, max, val)
   1683     if val > max then
   1684         val = max
   1685     elseif val < min then
   1686         val = min
   1687     end
   1688     return val
   1689 end
   1690 
   1691 -- translate value into element coordinates
   1692 function get_slider_ele_pos_for(element, val)
   1693 
   1694     local ele_pos = scale_value(
   1695         element.slider.min.value, element.slider.max.value,
   1696         element.slider.min.ele_pos, element.slider.max.ele_pos,
   1697         val)
   1698 
   1699     return limit_range(
   1700         element.slider.min.ele_pos, element.slider.max.ele_pos,
   1701         ele_pos)
   1702 end
   1703 
   1704 -- translates global (mouse) coordinates to value
   1705 function get_slider_value_at(element, glob_pos)
   1706 
   1707     local val = scale_value(
   1708         element.slider.min.glob_pos, element.slider.max.glob_pos,
   1709         element.slider.min.value, element.slider.max.value,
   1710         glob_pos)
   1711 
   1712     return limit_range(
   1713         element.slider.min.value, element.slider.max.value,
   1714         val)
   1715 end
   1716 
   1717 -- get value at current mouse position
   1718 function get_slider_value(element)
   1719     return get_slider_value_at(element, get_virt_mouse_pos())
   1720 end
   1721 
   1722 function countone(val)
   1723     if not (user_opts.iamaprogrammer) then
   1724         val = val + 1
   1725     end
   1726     return val
   1727 end
   1728 
   1729 -- align:  -1 .. +1
   1730 -- frame:  size of the containing area
   1731 -- obj:    size of the object that should be positioned inside the area
   1732 -- margin: min. distance from object to frame (as long as -1 <= align <= +1)
   1733 function get_align(align, frame, obj, margin)
   1734     return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
   1735 end
   1736 
   1737 -- multiplies two alpha values, formular can probably be improved
   1738 function mult_alpha(alphaA, alphaB)
   1739     return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
   1740 end
   1741 
   1742 function add_area(name, x1, y1, x2, y2)
   1743     -- create area if needed
   1744     if (osc_param.areas[name] == nil) then
   1745         osc_param.areas[name] = {}
   1746     end
   1747     table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
   1748 end
   1749 
   1750 function ass_append_alpha(ass, alpha, modifier)
   1751     local ar = {}
   1752 
   1753     for ai, av in pairs(alpha) do
   1754         av = mult_alpha(av, modifier)
   1755         if state.animation then
   1756             av = mult_alpha(av, state.animation)
   1757         end
   1758         ar[ai] = av
   1759     end
   1760 
   1761     ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
   1762                ar[1], ar[2], ar[3], ar[4]))
   1763 end
   1764 
   1765 function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
   1766     if hexagon then
   1767         ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
   1768     else
   1769         ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
   1770     end
   1771 end
   1772 
   1773 function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
   1774     if hexagon then
   1775         ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
   1776     else
   1777         ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
   1778     end
   1779 end
   1780 
   1781 
   1782 --
   1783 -- Tracklist Management
   1784 --
   1785 
   1786 local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
   1787 
   1788 -- updates the OSC internal playlists, should be run each time the track-layout changes
   1789 function update_tracklist()
   1790     local tracktable = mp.get_property_native("track-list", {})
   1791 
   1792     -- by osc_id
   1793     tracks_osc = {}
   1794     tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
   1795     -- by mpv_id
   1796     tracks_mpv = {}
   1797     tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
   1798     for n = 1, #tracktable do
   1799         if not (tracktable[n].type == "unknown") then
   1800             local type = tracktable[n].type
   1801             local mpv_id = tonumber(tracktable[n].id)
   1802 
   1803             -- by osc_id
   1804             table.insert(tracks_osc[type], tracktable[n])
   1805 
   1806             -- by mpv_id
   1807             tracks_mpv[type][mpv_id] = tracktable[n]
   1808             tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
   1809         end
   1810     end
   1811 end
   1812 
   1813 -- return a nice list of tracks of the given type (video, audio, sub)
   1814 function get_tracklist(type)
   1815     local msg = "Available " .. nicetypes[type] .. " Tracks: "
   1816     if #tracks_osc[type] == 0 then
   1817         msg = msg .. "none"
   1818     else
   1819         for n = 1, #tracks_osc[type] do
   1820             local track = tracks_osc[type][n]
   1821             local lang, title, selected = "unknown", "", "○"
   1822             if not(track.lang == nil) then lang = track.lang end
   1823             if not(track.title == nil) then title = track.title end
   1824             if (track.id == tonumber(mp.get_property(type))) then
   1825                 selected = "●"
   1826             end
   1827             msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title
   1828         end
   1829     end
   1830     return msg
   1831 end
   1832 
   1833 -- relatively change the track of given <type> by <next> tracks
   1834     --(+1 -> next, -1 -> previous)
   1835 function set_track(type, next)
   1836     local current_track_mpv, current_track_osc
   1837     if (mp.get_property(type) == "no") then
   1838         current_track_osc = 0
   1839     else
   1840         current_track_mpv = tonumber(mp.get_property(type))
   1841         current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
   1842     end
   1843     local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
   1844     local new_track_mpv
   1845     if new_track_osc == 0 then
   1846         new_track_mpv = "no"
   1847     else
   1848         new_track_mpv = tracks_osc[type][new_track_osc].id
   1849     end
   1850 
   1851     mp.commandv("set", type, new_track_mpv)
   1852 
   1853         if (new_track_osc == 0) then
   1854         show_message(nicetypes[type] .. " Track: none")
   1855     else
   1856         show_message(nicetypes[type]  .. " Track: "
   1857             .. new_track_osc .. "/" .. #tracks_osc[type]
   1858             .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
   1859             .. (tracks_osc[type][new_track_osc].title or ""))
   1860     end
   1861 end
   1862 
   1863 -- get the currently selected track of <type>, OSC-style counted
   1864 function get_track(type)
   1865     local track = mp.get_property(type)
   1866     if track ~= "no" and track ~= nil then
   1867         local tr = tracks_mpv[type][tonumber(track)]
   1868         if tr then
   1869             return tr.osc_id
   1870         end
   1871     end
   1872     return 0
   1873 end
   1874 
   1875 -- WindowControl helpers
   1876 function window_controls_enabled()
   1877     val = user_opts.windowcontrols
   1878     if val == "auto" then
   1879         return not state.border
   1880     else
   1881         return val ~= "no"
   1882     end
   1883 end
   1884 
   1885 function window_controls_alignment()
   1886     return user_opts.windowcontrols_alignment
   1887 end
   1888 
   1889 --
   1890 -- Element Management
   1891 --
   1892 
   1893 local elements = {}
   1894 
   1895 function prepare_elements()
   1896 
   1897     -- remove elements without layout or invisble
   1898     local elements2 = {}
   1899     for n, element in pairs(elements) do
   1900         if not (element.layout == nil) and (element.visible) then
   1901             table.insert(elements2, element)
   1902         end
   1903     end
   1904     elements = elements2
   1905 
   1906     function elem_compare (a, b)
   1907         return a.layout.layer < b.layout.layer
   1908     end
   1909 
   1910     table.sort(elements, elem_compare)
   1911 
   1912 
   1913     for _,element in pairs(elements) do
   1914 
   1915         local elem_geo = element.layout.geometry
   1916 
   1917         -- Calculate the hitbox
   1918         local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
   1919         element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
   1920 
   1921         local style_ass = assdraw.ass_new()
   1922 
   1923         -- prepare static elements
   1924         style_ass:append("{}") -- hack to troll new_event into inserting a \n
   1925         style_ass:new_event()
   1926         style_ass:pos(elem_geo.x, elem_geo.y)
   1927         style_ass:an(elem_geo.an)
   1928         style_ass:append(element.layout.style)
   1929 
   1930         element.style_ass = style_ass
   1931 
   1932         local static_ass = assdraw.ass_new()
   1933 
   1934 
   1935         if (element.type == "box") then
   1936             --draw box
   1937             static_ass:draw_start()
   1938             ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
   1939                              element.layout.box.radius, element.layout.box.hexagon)
   1940             static_ass:draw_stop()
   1941 
   1942         elseif (element.type == "slider") then
   1943             --draw static slider parts
   1944 
   1945             local r1 = 0
   1946             local r2 = 0
   1947             local slider_lo = element.layout.slider
   1948             -- offset between element outline and drag-area
   1949             local foV = slider_lo.border + slider_lo.gap
   1950 
   1951             -- calculate positions of min and max points
   1952             if (slider_lo.stype ~= "bar") then
   1953                 r1 = elem_geo.h / 2
   1954                 element.slider.min.ele_pos = elem_geo.h / 2
   1955                 element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
   1956                 if (slider_lo.stype == "diamond") then
   1957                     r2 = (elem_geo.h - 2 * slider_lo.border) / 2
   1958                 elseif (slider_lo.stype == "knob") then
   1959                     r2 = r1
   1960                 end
   1961             else
   1962                 element.slider.min.ele_pos =
   1963                     slider_lo.border + slider_lo.gap
   1964                 element.slider.max.ele_pos =
   1965                     elem_geo.w - (slider_lo.border + slider_lo.gap)
   1966             end
   1967 
   1968             element.slider.min.glob_pos =
   1969                 element.hitbox.x1 + element.slider.min.ele_pos
   1970             element.slider.max.glob_pos =
   1971                 element.hitbox.x1 + element.slider.max.ele_pos
   1972 
   1973             -- -- --
   1974 
   1975             static_ass:draw_start()
   1976 
   1977             -- the box
   1978             ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1, slider_lo.stype == "diamond")
   1979 
   1980             -- the "hole"
   1981             ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border,
   1982                               elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border,
   1983                               r2, slider_lo.stype == "diamond")
   1984 
   1985             -- marker nibbles
   1986             if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
   1987                 local markers = element.slider.markerF()
   1988                 for _,marker in pairs(markers) do
   1989                     if (marker > element.slider.min.value) and
   1990                         (marker < element.slider.max.value) then
   1991 
   1992                         local s = get_slider_ele_pos_for(element, marker)
   1993 
   1994                         if (slider_lo.gap > 1) then -- draw triangles
   1995 
   1996                             local a = slider_lo.gap / 0.5 --0.866
   1997 
   1998                             --top
   1999                             if (slider_lo.nibbles_top) then
   2000                                 static_ass:move_to(s - (a/2), slider_lo.border)
   2001                                 static_ass:line_to(s + (a/2), slider_lo.border)
   2002                                 static_ass:line_to(s, foV)
   2003                             end
   2004 
   2005                             --bottom
   2006                             if (slider_lo.nibbles_bottom) then
   2007                                 static_ass:move_to(s - (a/2),
   2008                                     elem_geo.h - slider_lo.border)
   2009                                 static_ass:line_to(s,
   2010                                     elem_geo.h - foV)
   2011                                 static_ass:line_to(s + (a/2),
   2012                                     elem_geo.h - slider_lo.border)
   2013                             end
   2014 
   2015                         else -- draw 2x1px nibbles
   2016 
   2017                             --top
   2018                             if (slider_lo.nibbles_top) then
   2019                                 static_ass:rect_cw(s - 1, slider_lo.border,
   2020                                     s + 1, slider_lo.border + slider_lo.gap);
   2021                             end
   2022 
   2023                             --bottom
   2024                             if (slider_lo.nibbles_bottom) then
   2025                                 static_ass:rect_cw(s - 1,
   2026                                     elem_geo.h -slider_lo.border -slider_lo.gap,
   2027                                     s + 1, elem_geo.h - slider_lo.border);
   2028                             end
   2029                         end
   2030                     end
   2031                 end
   2032             end
   2033         end
   2034 
   2035         element.static_ass = static_ass
   2036 
   2037 
   2038         -- if the element is supposed to be disabled,
   2039         -- style it accordingly and kill the eventresponders
   2040         if not (element.enabled) then
   2041             element.layout.alpha[1] = 136
   2042             element.eventresponder = nil
   2043         end
   2044     end
   2045 end
   2046 
   2047 
   2048 --
   2049 -- Element Rendering
   2050 --
   2051 
   2052 function render_elements(master_ass)
   2053 
   2054     for n=1, #elements do
   2055         local element = elements[n]
   2056 
   2057         local style_ass = assdraw.ass_new()
   2058         style_ass:merge(element.style_ass)
   2059         ass_append_alpha(style_ass, element.layout.alpha, 0)
   2060 
   2061         if element.eventresponder and (state.active_element == n) then
   2062 
   2063             -- run render event functions
   2064             if not (element.eventresponder.render == nil) then
   2065                 element.eventresponder.render(element)
   2066             end
   2067 
   2068             if mouse_hit(element) then
   2069                 -- mouse down styling
   2070                 if (element.styledown) then
   2071                     style_ass:append(osc_styles.elementDown)
   2072                 end
   2073 
   2074                 if (element.softrepeat) and (state.mouse_down_counter >= 15
   2075                     and state.mouse_down_counter % 5 == 0) then
   2076 
   2077                     element.eventresponder[state.active_event_source.."_down"](element)
   2078                 end
   2079                 state.mouse_down_counter = state.mouse_down_counter + 1
   2080             end
   2081 
   2082         end
   2083 
   2084         local elem_ass = assdraw.ass_new()
   2085 
   2086         elem_ass:merge(style_ass)
   2087 
   2088         if not (element.type == "button") then
   2089             elem_ass:merge(element.static_ass)
   2090         end
   2091 
   2092         if (element.type == "slider") then
   2093 
   2094             local slider_lo = element.layout.slider
   2095             local elem_geo = element.layout.geometry
   2096             local s_min = element.slider.min.value
   2097             local s_max = element.slider.max.value
   2098 
   2099             -- draw pos marker
   2100             local foH, xp
   2101             local pos = element.slider.posF()
   2102             local foV = slider_lo.border + slider_lo.gap
   2103             local innerH = elem_geo.h - (2 * foV)
   2104             local seekRanges = element.slider.seekRangesF()
   2105             local seekRangeLineHeight = innerH / 5
   2106 
   2107             if slider_lo.stype ~= "bar" then
   2108                 foH = elem_geo.h / 2
   2109             else
   2110                 foH = slider_lo.border + slider_lo.gap
   2111             end
   2112 
   2113             if pos then
   2114                 xp = get_slider_ele_pos_for(element, pos)
   2115 
   2116                 if slider_lo.stype ~= "bar" then
   2117                     local r = (user_opts.seekbarhandlesize * innerH) / 2
   2118                     ass_draw_rr_h_cw(elem_ass, xp - r, foH - r,
   2119                                      xp + r, foH + r,
   2120                                      r, slider_lo.stype == "diamond")
   2121                 else
   2122                     local h = 0
   2123                     if seekRanges and user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
   2124                         h = seekRangeLineHeight
   2125                     end
   2126                     elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h)
   2127 
   2128                     if seekRanges and not user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
   2129                         -- Punch holes for the seekRanges to be drawn later
   2130                         for _,range in pairs(seekRanges) do
   2131                             if range["start"] < pos then
   2132                                 local pstart = get_slider_ele_pos_for(element, range["start"])
   2133                                 local pend = xp
   2134 
   2135                                 if pos > range["end"] then
   2136                                     pend = get_slider_ele_pos_for(element, range["end"])
   2137                                 end
   2138                                 elem_ass:rect_ccw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
   2139                             end
   2140                         end
   2141                     end
   2142                 end
   2143 
   2144                 if slider_lo.rtype == "slider" then
   2145                     ass_draw_rr_h_cw(elem_ass, foH - innerH / 6, foH - innerH / 6,
   2146                                      xp, foH + innerH / 6,
   2147                                      innerH / 6, slider_lo.stype == "diamond", 0)
   2148                     ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15,
   2149                                      elem_geo.w - foH + innerH / 15, foH + innerH / 15,
   2150                                      0, slider_lo.stype == "diamond", innerH / 15)
   2151                     for _,range in pairs(seekRanges or {}) do
   2152                         local pstart = get_slider_ele_pos_for(element, range["start"])
   2153                         local pend = get_slider_ele_pos_for(element, range["end"])
   2154                         ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21,
   2155                                           pend, foH + innerH / 21,
   2156                                           innerH / 21, slider_lo.stype == "diamond")
   2157                     end
   2158                 end
   2159             end
   2160 
   2161             if seekRanges then
   2162                 if slider_lo.rtype ~= "inverted" then
   2163                     elem_ass:draw_stop()
   2164                     elem_ass:merge(element.style_ass)
   2165                     ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
   2166                     elem_ass:merge(element.static_ass)
   2167                 end
   2168 
   2169                 for _,range in pairs(seekRanges) do
   2170                     local pstart = get_slider_ele_pos_for(element, range["start"])
   2171                     local pend = get_slider_ele_pos_for(element, range["end"])
   2172 
   2173                     if slider_lo.rtype == "slider" then
   2174                         ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21,
   2175                                          pend, foH + innerH / 21,
   2176                                          innerH / 21, slider_lo.stype == "diamond")
   2177                     elseif slider_lo.rtype == "line" then
   2178                         if slider_lo.stype == "bar" then
   2179                             elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
   2180                         else
   2181                             ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8, foH - innerH / 8,
   2182                                              pend + innerH / 8, foH + innerH / 8,
   2183                                              innerH / 8, slider_lo.stype == "diamond")
   2184                         end
   2185                     elseif slider_lo.rtype == "bar" then
   2186                         if slider_lo.stype ~= "bar" then
   2187                             ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV,
   2188                                              pend + innerH / 2, foV + innerH,
   2189                                              innerH / 2, slider_lo.stype == "diamond")
   2190                         elseif range["end"] >= (pos or 0) then
   2191                             elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV)
   2192                         else
   2193                             elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
   2194                         end
   2195                     elseif slider_lo.rtype == "inverted" then
   2196                         if slider_lo.stype ~= "bar" then
   2197                             ass_draw_rr_h_ccw(elem_ass, pstart, (elem_geo.h / 2) - 1, pend,
   2198                                               (elem_geo.h / 2) + 1,
   2199                                               1, slider_lo.stype == "diamond")
   2200                         else
   2201                             elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1, pend, (elem_geo.h / 2) + 1)
   2202                         end
   2203                     end
   2204                 end
   2205             end
   2206 
   2207             elem_ass:draw_stop()
   2208 
   2209             -- add tooltip
   2210             if not (element.slider.tooltipF == nil) then
   2211 
   2212                 if mouse_hit(element) then
   2213                     local sliderpos = get_slider_value(element)
   2214                     local tooltiplabel = element.slider.tooltipF(sliderpos)
   2215 
   2216                     local an = slider_lo.tooltip_an
   2217 
   2218                     local ty
   2219 
   2220                     if (an == 2) then
   2221                         ty = element.hitbox.y1 - slider_lo.border
   2222                     else
   2223                         ty = element.hitbox.y1 + elem_geo.h/2
   2224                     end
   2225 
   2226                     local tx = get_virt_mouse_pos()
   2227                     if (slider_lo.adjust_tooltip) then
   2228                         if (an == 2) then
   2229                             if (sliderpos < (s_min + 3)) then
   2230                                 an = an - 1
   2231                             elseif (sliderpos > (s_max - 3)) then
   2232                                 an = an + 1
   2233                             end
   2234                         elseif (sliderpos > (s_max-s_min)/2) then
   2235                             an = an + 1
   2236                             tx = tx - 5
   2237                         else
   2238                             an = an - 1
   2239                             tx = tx + 10
   2240                         end
   2241                     end
   2242 
   2243                     -- tooltip label
   2244                     elem_ass:new_event()
   2245                     elem_ass:pos(tx, ty)
   2246                     elem_ass:an(an)
   2247                     elem_ass:append(slider_lo.tooltip_style)
   2248                     ass_append_alpha(elem_ass, slider_lo.alpha, 0)
   2249                     elem_ass:append(tooltiplabel)
   2250 
   2251                     -- mpv_thumbnail_script.lua --
   2252                     display_thumbnail({x=get_virt_mouse_pos(), y=ty, a=an}, sliderpos, elem_ass)
   2253                     -- // mpv_thumbnail_script.lua // --
   2254 
   2255                 end
   2256             end
   2257 
   2258         elseif (element.type == "button") then
   2259 
   2260             local buttontext
   2261             if type(element.content) == "function" then
   2262                 buttontext = element.content() -- function objects
   2263             elseif not (element.content == nil) then
   2264                 buttontext = element.content -- text objects
   2265             end
   2266 
   2267             local maxchars = element.layout.button.maxchars
   2268             if not (maxchars == nil) and (#buttontext > maxchars) then
   2269                 local max_ratio = 1.25  -- up to 25% more chars while shrinking
   2270                 local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
   2271                 if (#buttontext > limit) then
   2272                     while (#buttontext > limit) do
   2273                         buttontext = buttontext:gsub(".[\128-\191]*$", "")
   2274                     end
   2275                     buttontext = buttontext .. "..."
   2276                 end
   2277                 local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
   2278                 local stretch = (maxchars/#buttontext)*100
   2279                 buttontext = string.format("{\\fscx%f}",
   2280                     (maxchars/#buttontext)*100) .. buttontext
   2281             end
   2282 
   2283             elem_ass:append(buttontext)
   2284         end
   2285 
   2286         master_ass:merge(elem_ass)
   2287     end
   2288 end
   2289 
   2290 --
   2291 -- Message display
   2292 --
   2293 
   2294 -- pos is 1 based
   2295 function limited_list(prop, pos)
   2296     local proplist = mp.get_property_native(prop, {})
   2297     local count = #proplist
   2298     if count == 0 then
   2299         return count, proplist
   2300     end
   2301 
   2302     local fs = tonumber(mp.get_property('options/osd-font-size'))
   2303     local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
   2304     if max % 2 == 0 then
   2305         max = max - 1
   2306     end
   2307     local delta = math.ceil(max / 2) - 1
   2308     local begi = math.max(math.min(pos - delta, count - max + 1), 1)
   2309     local endi = math.min(begi + max - 1, count)
   2310 
   2311     local reslist = {}
   2312     for i=begi, endi do
   2313         local item = proplist[i]
   2314         item.current = (i == pos) and true or nil
   2315         table.insert(reslist, item)
   2316     end
   2317     return count, reslist
   2318 end
   2319 
   2320 function get_playlist()
   2321     local pos = mp.get_property_number('playlist-pos', 0) + 1
   2322     local count, limlist = limited_list('playlist', pos)
   2323     if count == 0 then
   2324         return 'Empty playlist.'
   2325     end
   2326 
   2327     local message = string.format('Playlist [%d/%d]:\n', pos, count)
   2328     for i, v in ipairs(limlist) do
   2329         local title = v.title
   2330         local _, filename = utils.split_path(v.filename)
   2331         if title == nil then
   2332             title = filename
   2333         end
   2334         message = string.format('%s %s %s\n', message,
   2335             (v.current and '●' or '○'), title)
   2336     end
   2337     return message
   2338 end
   2339 
   2340 function get_chapterlist()
   2341     local pos = mp.get_property_number('chapter', 0) + 1
   2342     local count, limlist = limited_list('chapter-list', pos)
   2343     if count == 0 then
   2344         return 'No chapters.'
   2345     end
   2346 
   2347     local message = string.format('Chapters [%d/%d]:\n', pos, count)
   2348     for i, v in ipairs(limlist) do
   2349         local time = mp.format_time(v.time)
   2350         local title = v.title
   2351         if title == nil then
   2352             title = string.format('Chapter %02d', i)
   2353         end
   2354         message = string.format('%s[%s] %s %s\n', message, time,
   2355             (v.current and '●' or '○'), title)
   2356     end
   2357     return message
   2358 end
   2359 
   2360 function show_message(text, duration)
   2361 
   2362     --print("text: "..text.."   duration: " .. duration)
   2363     if duration == nil then
   2364         duration = tonumber(mp.get_property("options/osd-duration")) / 1000
   2365     elseif not type(duration) == "number" then
   2366         print("duration: " .. duration)
   2367     end
   2368 
   2369     -- cut the text short, otherwise the following functions
   2370     -- may slow down massively on huge input
   2371     text = string.sub(text, 0, 4000)
   2372 
   2373     -- replace actual linebreaks with ASS linebreaks
   2374     text = string.gsub(text, "\n", "\\N")
   2375 
   2376     state.message_text = text
   2377     state.message_timeout = mp.get_time() + duration
   2378 end
   2379 
   2380 function render_message(ass)
   2381     if not(state.message_timeout == nil) and not(state.message_text == nil)
   2382         and state.message_timeout > mp.get_time() then
   2383         local _, lines = string.gsub(state.message_text, "\\N", "")
   2384 
   2385         local fontsize = tonumber(mp.get_property("options/osd-font-size"))
   2386         local outline = tonumber(mp.get_property("options/osd-border-size"))
   2387         local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
   2388         local counterscale = osc_param.playresy / osc_param.unscaled_y
   2389 
   2390         fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
   2391         outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
   2392 
   2393         local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
   2394 
   2395 
   2396         ass:new_event()
   2397         ass:append(style .. state.message_text)
   2398     else
   2399         state.message_text = nil
   2400         state.message_timeout = nil
   2401     end
   2402 end
   2403 
   2404 --
   2405 -- Initialisation and Layout
   2406 --
   2407 
   2408 function new_element(name, type)
   2409     elements[name] = {}
   2410     elements[name].type = type
   2411 
   2412     -- add default stuff
   2413     elements[name].eventresponder = {}
   2414     elements[name].visible = true
   2415     elements[name].enabled = true
   2416     elements[name].softrepeat = false
   2417     elements[name].styledown = (type == "button")
   2418     elements[name].state = {}
   2419 
   2420     if (type == "slider") then
   2421         elements[name].slider = {min = {value = 0}, max = {value = 100}}
   2422     end
   2423 
   2424 
   2425     return elements[name]
   2426 end
   2427 
   2428 function add_layout(name)
   2429     if not (elements[name] == nil) then
   2430         -- new layout
   2431         elements[name].layout = {}
   2432 
   2433         -- set layout defaults
   2434         elements[name].layout.layer = 50
   2435         elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
   2436 
   2437         if (elements[name].type == "button") then
   2438             elements[name].layout.button = {
   2439                 maxchars = nil,
   2440             }
   2441         elseif (elements[name].type == "slider") then
   2442             -- slider defaults
   2443             elements[name].layout.slider = {
   2444                 border = 1,
   2445                 gap = 1,
   2446                 nibbles_top = true,
   2447                 nibbles_bottom = true,
   2448                 stype = "slider",
   2449                 adjust_tooltip = true,
   2450                 tooltip_style = "",
   2451                 tooltip_an = 2,
   2452                 alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
   2453             }
   2454         elseif (elements[name].type == "box") then
   2455             elements[name].layout.box = {radius = 0}
   2456         end
   2457 
   2458         return elements[name].layout
   2459     else
   2460         msg.error("Can't add_layout to element \""..name.."\", doesn't exist.")
   2461     end
   2462 end
   2463 
   2464 -- Window Controls
   2465 function window_controls(topbar)
   2466     local wc_geo = {
   2467         x = 0,
   2468         y = 30 + user_opts.barmargin,
   2469         an = 1,
   2470         w = osc_param.playresx,
   2471         h = 30,
   2472     }
   2473 
   2474     local alignment = window_controls_alignment()
   2475     local controlbox_w = window_control_box_width
   2476     local titlebox_w = wc_geo.w - controlbox_w
   2477 
   2478     -- Default alignment is "right"
   2479     local controlbox_left = wc_geo.w - controlbox_w
   2480     local titlebox_left = wc_geo.x + 5
   2481 
   2482     if alignment == "left" then
   2483         controlbox_left = wc_geo.x
   2484         titlebox_left = wc_geo.x + controlbox_w + 5
   2485     end
   2486 
   2487     add_area("window-controls",
   2488              get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
   2489                                controlbox_w, wc_geo.h))
   2490 
   2491     local lo
   2492 
   2493     -- Background Bar
   2494     new_element("wcbar", "box")
   2495     lo = add_layout("wcbar")
   2496     lo.geometry = wc_geo
   2497     lo.layer = 10
   2498     lo.style = osc_styles.wcBar
   2499     lo.alpha[1] = user_opts.boxalpha
   2500 
   2501     local button_y = wc_geo.y - (wc_geo.h / 2)
   2502     local first_geo =
   2503         {x = controlbox_left + 5, y = button_y, an = 4, w = 25, h = 25}
   2504     local second_geo =
   2505         {x = controlbox_left + 30, y = button_y, an = 4, w = 25, h = 25}
   2506     local third_geo =
   2507         {x = controlbox_left + 55, y = button_y, an = 4, w = 25, h = 25}
   2508 
   2509     -- Window control buttons use symbols in the custom mpv osd font
   2510     -- because the official unicode codepoints are sufficiently
   2511     -- exotic that a system might lack an installed font with them,
   2512     -- and libass will complain that they are not present in the
   2513     -- default font, even if another font with them is available.
   2514 
   2515     -- Close: 🗙
   2516     ne = new_element("close", "button")
   2517     ne.content = "\238\132\149"
   2518     ne.eventresponder["mbtn_left_up"] =
   2519         function () mp.commandv("quit") end
   2520     lo = add_layout("close")
   2521     lo.geometry = alignment == "left" and first_geo or third_geo
   2522     lo.style = osc_styles.wcButtons
   2523 
   2524     -- Minimize: 🗕
   2525     ne = new_element("minimize", "button")
   2526     ne.content = "\238\132\146"
   2527     ne.eventresponder["mbtn_left_up"] =
   2528         function () mp.commandv("cycle", "window-minimized") end
   2529     lo = add_layout("minimize")
   2530     lo.geometry = alignment == "left" and second_geo or first_geo
   2531     lo.style = osc_styles.wcButtons
   2532 
   2533     -- Maximize: 🗖 /🗗
   2534     ne = new_element("maximize", "button")
   2535     if state.maximized then
   2536         ne.content = "\238\132\148"
   2537     else
   2538         ne.content = "\238\132\147"
   2539     end
   2540     ne.eventresponder["mbtn_left_up"] =
   2541         function () mp.commandv("cycle", "window-maximized") end
   2542     lo = add_layout("maximize")
   2543     lo.geometry = alignment == "left" and third_geo or second_geo
   2544     lo.style = osc_styles.wcButtons
   2545 
   2546     -- deadzone below window controls
   2547     local sh_area_y0, sh_area_y1
   2548     sh_area_y0 = user_opts.barmargin
   2549     sh_area_y1 = (wc_geo.y + (wc_geo.h / 2)) +
   2550                  get_align(1 - (2 * user_opts.deadzonesize),
   2551                  osc_param.playresy - (wc_geo.y + (wc_geo.h / 2)), 0, 0)
   2552     add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1)
   2553 
   2554     if topbar then
   2555         -- The title is already there as part of the top bar
   2556         return
   2557     else
   2558         -- Apply boxvideo margins to the control bar
   2559         osc_param.video_margins.t = wc_geo.h / osc_param.playresy
   2560     end
   2561 
   2562     -- Window Title
   2563     ne = new_element("wctitle", "button")
   2564     ne.content = function ()
   2565         local title = mp.command_native({"expand-text", user_opts.title})
   2566         -- escape ASS, and strip newlines and trailing slashes
   2567         title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
   2568         return not (title == "") and title or "mpv"
   2569     end
   2570     lo = add_layout("wctitle")
   2571     lo.geometry =
   2572         { x = titlebox_left, y = wc_geo.y - 3, an = 1, w = titlebox_w, h = wc_geo.h }
   2573     lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
   2574         osc_styles.wcTitle,
   2575         titlebox_left, wc_geo.y - wc_geo.h, titlebox_w, wc_geo.y + wc_geo.h)
   2576 end
   2577 
   2578 --
   2579 -- Layouts
   2580 --
   2581 
   2582 local layouts = {}
   2583 
   2584 -- Classic box layout
   2585 layouts["box"] = function ()
   2586 
   2587     local osc_geo = {
   2588         w = 550,    -- width
   2589         h = 138,    -- height
   2590         r = 10,     -- corner-radius
   2591         p = 15,     -- padding
   2592     }
   2593 
   2594     -- make sure the OSC actually fits into the video
   2595     if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then
   2596         osc_param.playresy = (osc_geo.w+(2*osc_geo.p))/osc_param.display_aspect
   2597         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
   2598     end
   2599 
   2600     -- position of the controller according to video aspect and valignment
   2601     local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
   2602         osc_geo.w, 0))
   2603     local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
   2604         osc_geo.h, 0))
   2605 
   2606     -- position offset for contents aligned at the borders of the box
   2607     local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2
   2608     local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2
   2609 
   2610     osc_param.areas = {} -- delete areas
   2611 
   2612     -- area for active mouse input
   2613     add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
   2614 
   2615     -- area for show/hide
   2616     local sh_area_y0, sh_area_y1
   2617     if user_opts.valign > 0 then
   2618         -- deadzone above OSC
   2619         sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
   2620             posY - (osc_geo.h / 2), 0, 0)
   2621         sh_area_y1 = osc_param.playresy
   2622     else
   2623         -- deadzone below OSC
   2624         sh_area_y0 = 0
   2625         sh_area_y1 = (posY + (osc_geo.h / 2)) +
   2626             get_align(1 - (2*user_opts.deadzonesize),
   2627             osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
   2628     end
   2629     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
   2630 
   2631     -- fetch values
   2632     local osc_w, osc_h, osc_r, osc_p =
   2633         osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p
   2634 
   2635     local lo
   2636 
   2637     --
   2638     -- Background box
   2639     --
   2640 
   2641     new_element("bgbox", "box")
   2642     lo = add_layout("bgbox")
   2643 
   2644     lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
   2645     lo.layer = 10
   2646     lo.style = osc_styles.box
   2647     lo.alpha[1] = user_opts.boxalpha
   2648     lo.alpha[3] = user_opts.boxalpha
   2649     lo.box.radius = osc_r
   2650 
   2651     --
   2652     -- Title row
   2653     --
   2654 
   2655     local titlerowY = posY - pos_offsetY - 10
   2656 
   2657     lo = add_layout("title")
   2658     lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
   2659     lo.style = osc_styles.vidtitle
   2660     lo.button.maxchars = user_opts.boxmaxchars
   2661 
   2662     lo = add_layout("pl_prev")
   2663     lo.geometry =
   2664         {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12}
   2665     lo.style = osc_styles.topButtons
   2666 
   2667     lo = add_layout("pl_next")
   2668     lo.geometry =
   2669         {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12}
   2670     lo.style = osc_styles.topButtons
   2671 
   2672     --
   2673     -- Big buttons
   2674     --
   2675 
   2676     local bigbtnrowY = posY - pos_offsetY + 35
   2677     local bigbtndist = 60
   2678 
   2679     lo = add_layout("playpause")
   2680     lo.geometry =
   2681         {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
   2682     lo.style = osc_styles.bigButtons
   2683 
   2684     lo = add_layout("skipback")
   2685     lo.geometry =
   2686         {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
   2687     lo.style = osc_styles.bigButtons
   2688 
   2689     lo = add_layout("skipfrwd")
   2690     lo.geometry =
   2691         {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
   2692     lo.style = osc_styles.bigButtons
   2693 
   2694     lo = add_layout("ch_prev")
   2695     lo.geometry =
   2696         {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
   2697     lo.style = osc_styles.bigButtons
   2698 
   2699     lo = add_layout("ch_next")
   2700     lo.geometry =
   2701         {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
   2702     lo.style = osc_styles.bigButtons
   2703 
   2704     lo = add_layout("cy_audio")
   2705     lo.geometry =
   2706         {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18}
   2707     lo.style = osc_styles.smallButtonsL
   2708 
   2709     lo = add_layout("cy_sub")
   2710     lo.geometry =
   2711         {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18}
   2712     lo.style = osc_styles.smallButtonsL
   2713 
   2714     lo = add_layout("tog_fs")
   2715     lo.geometry =
   2716         {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25}
   2717     lo.style = osc_styles.smallButtonsR
   2718 
   2719     lo = add_layout("volume")
   2720     lo.geometry =
   2721         {x = posX+pos_offsetX - (25 * 2) - osc_geo.p,
   2722          y = bigbtnrowY, an = 4, w = 25, h = 25}
   2723     lo.style = osc_styles.smallButtonsR
   2724 
   2725     --
   2726     -- Seekbar
   2727     --
   2728 
   2729     lo = add_layout("seekbar")
   2730     lo.geometry =
   2731         {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15}
   2732     lo.style = osc_styles.timecodes
   2733     lo.slider.tooltip_style = osc_styles.vidtitle
   2734     lo.slider.stype = user_opts["seekbarstyle"]
   2735     lo.slider.rtype = user_opts["seekrangestyle"]
   2736 
   2737     --
   2738     -- Timecodes + Cache
   2739     --
   2740 
   2741     local bottomrowY = posY + pos_offsetY - 5
   2742 
   2743     lo = add_layout("tc_left")
   2744     lo.geometry =
   2745         {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18}
   2746     lo.style = osc_styles.timecodes
   2747 
   2748     lo = add_layout("tc_right")
   2749     lo.geometry =
   2750         {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18}
   2751     lo.style = osc_styles.timecodes
   2752 
   2753     lo = add_layout("cache")
   2754     lo.geometry =
   2755         {x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
   2756     lo.style = osc_styles.timecodes
   2757 
   2758 end
   2759 
   2760 -- slim box layout
   2761 layouts["slimbox"] = function ()
   2762 
   2763     local osc_geo = {
   2764         w = 660,    -- width
   2765         h = 70,     -- height
   2766         r = 10,     -- corner-radius
   2767     }
   2768 
   2769     -- make sure the OSC actually fits into the video
   2770     if (osc_param.playresx < (osc_geo.w)) then
   2771         osc_param.playresy = (osc_geo.w)/osc_param.display_aspect
   2772         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
   2773     end
   2774 
   2775     -- position of the controller according to video aspect and valignment
   2776     local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
   2777         osc_geo.w, 0))
   2778     local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
   2779         osc_geo.h, 0))
   2780 
   2781     osc_param.areas = {} -- delete areas
   2782 
   2783     -- area for active mouse input
   2784     add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
   2785 
   2786     -- area for show/hide
   2787     local sh_area_y0, sh_area_y1
   2788     if user_opts.valign > 0 then
   2789         -- deadzone above OSC
   2790         sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
   2791             posY - (osc_geo.h / 2), 0, 0)
   2792         sh_area_y1 = osc_param.playresy
   2793     else
   2794         -- deadzone below OSC
   2795         sh_area_y0 = 0
   2796         sh_area_y1 = (posY + (osc_geo.h / 2)) +
   2797             get_align(1 - (2*user_opts.deadzonesize),
   2798             osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
   2799     end
   2800     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
   2801 
   2802     local lo
   2803 
   2804     local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100
   2805 
   2806     -- styles
   2807     local styles = {
   2808         box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
   2809         timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
   2810         tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}",
   2811     }
   2812 
   2813 
   2814     new_element("bgbox", "box")
   2815     lo = add_layout("bgbox")
   2816 
   2817     lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
   2818     lo.layer = 10
   2819     lo.style = osc_styles.box
   2820     lo.alpha[1] = user_opts.boxalpha
   2821     lo.alpha[3] = 0
   2822     if not (user_opts["seekbarstyle"] == "bar") then
   2823         lo.box.radius = osc_geo.r
   2824         lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
   2825     end
   2826 
   2827 
   2828     lo = add_layout("seekbar")
   2829     lo.geometry =
   2830         {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
   2831     lo.style = osc_styles.timecodes
   2832     lo.slider.border = 0
   2833     lo.slider.gap = 1.5
   2834     lo.slider.tooltip_style = styles.tooltip
   2835     lo.slider.stype = user_opts["seekbarstyle"]
   2836     lo.slider.rtype = user_opts["seekrangestyle"]
   2837     lo.slider.adjust_tooltip = false
   2838 
   2839     --
   2840     -- Timecodes
   2841     --
   2842 
   2843     lo = add_layout("tc_left")
   2844     lo.geometry =
   2845         {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1,
   2846         an = 7, w = tc_w, h = ele_h}
   2847     lo.style = styles.timecodes
   2848     lo.alpha[3] = user_opts.boxalpha
   2849 
   2850     lo = add_layout("tc_right")
   2851     lo.geometry =
   2852         {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1,
   2853         an = 9, w = tc_w, h = ele_h}
   2854     lo.style = styles.timecodes
   2855     lo.alpha[3] = user_opts.boxalpha
   2856 
   2857     -- Cache
   2858 
   2859     lo = add_layout("cache")
   2860     lo.geometry =
   2861         {x = posX, y = posY + 1,
   2862         an = 8, w = tc_w, h = ele_h}
   2863     lo.style = styles.timecodes
   2864     lo.alpha[3] = user_opts.boxalpha
   2865 
   2866 
   2867 end
   2868 
   2869 function bar_layout(direction)
   2870     local osc_geo = {
   2871         x = -2,
   2872         y,
   2873         an = (direction < 0) and 7 or 1,
   2874         w,
   2875         h = 56,
   2876     }
   2877 
   2878     local padX = 9
   2879     local padY = 3
   2880     local buttonW = 27
   2881     local tcW = (state.tc_ms) and 170 or 110
   2882     local tsW = 90
   2883     local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
   2884 
   2885     -- Special topbar handling when window controls are present
   2886     local padwc_l
   2887     local padwc_r
   2888     if direction < 0 or not window_controls_enabled() then
   2889         padwc_l = 0
   2890         padwc_r = 0
   2891     elseif window_controls_alignment() == "left" then
   2892         padwc_l = window_control_box_width
   2893         padwc_r = 0
   2894     else
   2895         padwc_l = 0
   2896         padwc_r = window_control_box_width
   2897     end
   2898 
   2899     if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
   2900         osc_param.playresy = minW / osc_param.display_aspect
   2901         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
   2902     end
   2903 
   2904     osc_geo.y = direction * (54 + user_opts.barmargin)
   2905     osc_geo.w = osc_param.playresx + 4
   2906     if direction < 0 then
   2907         osc_geo.y = osc_geo.y + osc_param.playresy
   2908     end
   2909 
   2910     local line1 = osc_geo.y - direction * (9 + padY)
   2911     local line2 = osc_geo.y - direction * (36 + padY)
   2912 
   2913     osc_param.areas = {}
   2914 
   2915     add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
   2916                                         osc_geo.w, osc_geo.h))
   2917 
   2918     local sh_area_y0, sh_area_y1
   2919     if direction > 0 then
   2920         -- deadzone below OSC
   2921         sh_area_y0 = user_opts.barmargin
   2922         sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) +
   2923                      get_align(1 - (2*user_opts.deadzonesize),
   2924                      osc_param.playresy - (osc_geo.y + (osc_geo.h / 2)), 0, 0)
   2925     else
   2926         -- deadzone above OSC
   2927         sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
   2928                                osc_geo.y - (osc_geo.h / 2), 0, 0)
   2929         sh_area_y1 = osc_param.playresy - user_opts.barmargin
   2930     end
   2931     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
   2932 
   2933     local lo, geo
   2934 
   2935     -- Background bar
   2936     new_element("bgbox", "box")
   2937     lo = add_layout("bgbox")
   2938 
   2939     lo.geometry = osc_geo
   2940     lo.layer = 10
   2941     lo.style = osc_styles.box
   2942     lo.alpha[1] = user_opts.boxalpha
   2943 
   2944 
   2945     -- Playlist prev/next
   2946     geo = { x = osc_geo.x + padX, y = line1,
   2947             an = 4, w = 18, h = 18 - padY }
   2948     lo = add_layout("pl_prev")
   2949     lo.geometry = geo
   2950     lo.style = osc_styles.topButtonsBar
   2951 
   2952     geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
   2953     lo = add_layout("pl_next")
   2954     lo.geometry = geo
   2955     lo.style = osc_styles.topButtonsBar
   2956 
   2957     local t_l = geo.x + geo.w + padX
   2958 
   2959     -- Cache
   2960     geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
   2961             an = 6, w = 150, h = geo.h }
   2962     lo = add_layout("cache")
   2963     lo.geometry = geo
   2964     lo.style = osc_styles.vidtitleBar
   2965 
   2966     local t_r = geo.x - geo.w - padX*2
   2967 
   2968     -- Title
   2969     geo = { x = t_l, y = geo.y, an = 4,
   2970             w = t_r - t_l, h = geo.h }
   2971     lo = add_layout("title")
   2972     lo.geometry = geo
   2973     lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
   2974         osc_styles.vidtitleBar,
   2975         geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
   2976 
   2977 
   2978     -- Playback control buttons
   2979     geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4,
   2980             w = buttonW, h = 36 - padY*2}
   2981     lo = add_layout("playpause")
   2982     lo.geometry = geo
   2983     lo.style = osc_styles.smallButtonsBar
   2984 
   2985     geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
   2986     lo = add_layout("ch_prev")
   2987     lo.geometry = geo
   2988     lo.style = osc_styles.smallButtonsBar
   2989 
   2990     geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
   2991     lo = add_layout("ch_next")
   2992     lo.geometry = geo
   2993     lo.style = osc_styles.smallButtonsBar
   2994 
   2995     -- Left timecode
   2996     geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
   2997             w = tcW, h = geo.h }
   2998     lo = add_layout("tc_left")
   2999     lo.geometry = geo
   3000     lo.style = osc_styles.timecodesBar
   3001 
   3002     local sb_l = geo.x + padX
   3003 
   3004     -- Fullscreen button
   3005     geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4,
   3006             w = buttonW, h = geo.h }
   3007     lo = add_layout("tog_fs")
   3008     lo.geometry = geo
   3009     lo.style = osc_styles.smallButtonsBar
   3010 
   3011     -- Volume
   3012     geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
   3013     lo = add_layout("volume")
   3014     lo.geometry = geo
   3015     lo.style = osc_styles.smallButtonsBar
   3016 
   3017     -- Track selection buttons
   3018     geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
   3019     lo = add_layout("cy_sub")
   3020     lo.geometry = geo
   3021     lo.style = osc_styles.smallButtonsBar
   3022 
   3023     geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
   3024     lo = add_layout("cy_audio")
   3025     lo.geometry = geo
   3026     lo.style = osc_styles.smallButtonsBar
   3027 
   3028 
   3029     -- Right timecode
   3030     geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an,
   3031             w = tcW, h = geo.h }
   3032     lo = add_layout("tc_right")
   3033     lo.geometry = geo
   3034     lo.style = osc_styles.timecodesBar
   3035 
   3036     local sb_r = geo.x - padX
   3037 
   3038 
   3039     -- Seekbar
   3040     geo = { x = sb_l, y = geo.y, an = geo.an,
   3041             w = math.max(0, sb_r - sb_l), h = geo.h }
   3042     new_element("bgbar1", "box")
   3043     lo = add_layout("bgbar1")
   3044 
   3045     lo.geometry = geo
   3046     lo.layer = 15
   3047     lo.style = osc_styles.timecodesBar
   3048     lo.alpha[1] =
   3049         math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
   3050     if not (user_opts["seekbarstyle"] == "bar") then
   3051         lo.box.radius = geo.h / 2
   3052         lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
   3053     end
   3054 
   3055     lo = add_layout("seekbar")
   3056     lo.geometry = geo
   3057     lo.style = osc_styles.timecodesBar
   3058     lo.slider.border = 0
   3059     lo.slider.gap = 2
   3060     lo.slider.tooltip_style = osc_styles.timePosBar
   3061     lo.slider.tooltip_an = 5
   3062     lo.slider.stype = user_opts["seekbarstyle"]
   3063     lo.slider.rtype = user_opts["seekrangestyle"]
   3064 
   3065     if direction < 0 then
   3066         osc_param.video_margins.b = osc_geo.h / osc_param.playresy
   3067     else
   3068         osc_param.video_margins.t = osc_geo.h / osc_param.playresy
   3069     end
   3070 end
   3071 
   3072 layouts["bottombar"] = function()
   3073     bar_layout(-1)
   3074 end
   3075 
   3076 layouts["topbar"] = function()
   3077     bar_layout(1)
   3078 end
   3079 
   3080 -- Validate string type user options
   3081 function validate_user_opts()
   3082     if layouts[user_opts.layout] == nil then
   3083         msg.warn("Invalid setting \""..user_opts.layout.."\" for layout")
   3084         user_opts.layout = "bottombar"
   3085     end
   3086 
   3087     if user_opts.seekbarstyle ~= "bar" and
   3088        user_opts.seekbarstyle ~= "diamond" and
   3089        user_opts.seekbarstyle ~= "knob" then
   3090         msg.warn("Invalid setting \"" .. user_opts.seekbarstyle
   3091             .. "\" for seekbarstyle")
   3092         user_opts.seekbarstyle = "bar"
   3093     end
   3094 
   3095     if user_opts.seekrangestyle ~= "bar" and
   3096        user_opts.seekrangestyle ~= "line" and
   3097        user_opts.seekrangestyle ~= "slider" and
   3098        user_opts.seekrangestyle ~= "inverted" and
   3099        user_opts.seekrangestyle ~= "none" then
   3100         msg.warn("Invalid setting \"" .. user_opts.seekrangestyle
   3101             .. "\" for seekrangestyle")
   3102         user_opts.seekrangestyle = "inverted"
   3103     end
   3104 
   3105     if user_opts.seekrangestyle == "slider" and
   3106        user_opts.seekbarstyle == "bar" then
   3107         msg.warn("Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported")
   3108         user_opts.seekrangestyle = "inverted"
   3109     end
   3110 end
   3111 
   3112 
   3113 -- OSC INIT
   3114 function osc_init()
   3115     msg.debug("osc_init")
   3116 
   3117     -- set canvas resolution according to display aspect and scaling setting
   3118     local baseResY = 720
   3119     local display_w, display_h, display_aspect = mp.get_osd_size()
   3120     local scale = 1
   3121 
   3122     if (mp.get_property("video") == "no") then -- dummy/forced window
   3123         scale = user_opts.scaleforcedwindow
   3124     elseif state.fullscreen then
   3125         scale = user_opts.scalefullscreen
   3126     else
   3127         scale = user_opts.scalewindowed
   3128     end
   3129 
   3130     if user_opts.vidscale then
   3131         osc_param.unscaled_y = baseResY
   3132     else
   3133         osc_param.unscaled_y = display_h
   3134     end
   3135     osc_param.playresy = osc_param.unscaled_y / scale
   3136     if (display_aspect > 0) then
   3137         osc_param.display_aspect = display_aspect
   3138     end
   3139     osc_param.playresx = osc_param.playresy * osc_param.display_aspect
   3140 
   3141     -- stop seeking with the slider to prevent skipping files
   3142     state.active_element = nil
   3143 
   3144 
   3145 
   3146 
   3147     elements = {}
   3148 
   3149     -- some often needed stuff
   3150     local pl_count = mp.get_property_number("playlist-count", 0)
   3151     local have_pl = (pl_count > 1)
   3152     local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
   3153     local have_ch = (mp.get_property_number("chapters", 0) > 0)
   3154     local loop = mp.get_property("loop-playlist", "no")
   3155 
   3156     local ne
   3157 
   3158     -- title
   3159     ne = new_element("title", "button")
   3160 
   3161     ne.content = function ()
   3162         local title = mp.command_native({"expand-text", user_opts.title})
   3163         -- escape ASS, and strip newlines and trailing slashes
   3164         title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
   3165         return not (title == "") and title or "mpv"
   3166     end
   3167 
   3168     ne.eventresponder["mbtn_left_up"] = function ()
   3169         local title = mp.get_property_osd("media-title")
   3170         if (have_pl) then
   3171             title = string.format("[%d/%d] %s", countone(pl_pos - 1),
   3172                                   pl_count, title)
   3173         end
   3174         show_message(title)
   3175     end
   3176 
   3177     ne.eventresponder["mbtn_right_up"] =
   3178         function () show_message(mp.get_property_osd("filename")) end
   3179 
   3180     -- playlist buttons
   3181 
   3182     -- prev
   3183     ne = new_element("pl_prev", "button")
   3184 
   3185     ne.content = "\238\132\144"
   3186     ne.enabled = (pl_pos > 1) or (loop ~= "no")
   3187     ne.eventresponder["mbtn_left_up"] =
   3188         function ()
   3189             mp.commandv("playlist-prev", "weak")
   3190             show_message(get_playlist(), 3)
   3191         end
   3192     ne.eventresponder["shift+mbtn_left_up"] =
   3193         function () show_message(get_playlist(), 3) end
   3194     ne.eventresponder["mbtn_right_up"] =
   3195         function () show_message(get_playlist(), 3) end
   3196 
   3197     --next
   3198     ne = new_element("pl_next", "button")
   3199 
   3200     ne.content = "\238\132\129"
   3201     ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
   3202     ne.eventresponder["mbtn_left_up"] =
   3203         function ()
   3204             mp.commandv("playlist-next", "weak")
   3205             show_message(get_playlist(), 3)
   3206         end
   3207     ne.eventresponder["shift+mbtn_left_up"] =
   3208         function () show_message(get_playlist(), 3) end
   3209     ne.eventresponder["mbtn_right_up"] =
   3210         function () show_message(get_playlist(), 3) end
   3211 
   3212 
   3213     -- big buttons
   3214 
   3215     --playpause
   3216     ne = new_element("playpause", "button")
   3217 
   3218     ne.content = function ()
   3219         if mp.get_property("pause") == "yes" then
   3220             return ("\238\132\129")
   3221         else
   3222             return ("\238\128\130")
   3223         end
   3224     end
   3225     ne.eventresponder["mbtn_left_up"] =
   3226         function () mp.commandv("cycle", "pause") end
   3227 
   3228     --skipback
   3229     ne = new_element("skipback", "button")
   3230 
   3231     ne.softrepeat = true
   3232     ne.content = "\238\128\132"
   3233     ne.eventresponder["mbtn_left_down"] =
   3234         function () mp.commandv("seek", -5, "relative", "keyframes") end
   3235     ne.eventresponder["shift+mbtn_left_down"] =
   3236         function () mp.commandv("frame-back-step") end
   3237     ne.eventresponder["mbtn_right_down"] =
   3238         function () mp.commandv("seek", -30, "relative", "keyframes") end
   3239 
   3240     --skipfrwd
   3241     ne = new_element("skipfrwd", "button")
   3242 
   3243     ne.softrepeat = true
   3244     ne.content = "\238\128\133"
   3245     ne.eventresponder["mbtn_left_down"] =
   3246         function () mp.commandv("seek", 10, "relative", "keyframes") end
   3247     ne.eventresponder["shift+mbtn_left_down"] =
   3248         function () mp.commandv("frame-step") end
   3249     ne.eventresponder["mbtn_right_down"] =
   3250         function () mp.commandv("seek", 60, "relative", "keyframes") end
   3251 
   3252     --ch_prev
   3253     ne = new_element("ch_prev", "button")
   3254 
   3255     ne.enabled = have_ch
   3256     ne.content = "\238\132\132"
   3257     ne.eventresponder["mbtn_left_up"] =
   3258         function ()
   3259             mp.commandv("add", "chapter", -1)
   3260             show_message(get_chapterlist(), 3)
   3261         end
   3262     ne.eventresponder["shift+mbtn_left_up"] =
   3263         function () show_message(get_chapterlist(), 3) end
   3264     ne.eventresponder["mbtn_right_up"] =
   3265         function () show_message(get_chapterlist(), 3) end
   3266 
   3267     --ch_next
   3268     ne = new_element("ch_next", "button")
   3269 
   3270     ne.enabled = have_ch
   3271     ne.content = "\238\132\133"
   3272     ne.eventresponder["mbtn_left_up"] =
   3273         function ()
   3274             mp.commandv("add", "chapter", 1)
   3275             show_message(get_chapterlist(), 3)
   3276         end
   3277     ne.eventresponder["shift+mbtn_left_up"] =
   3278         function () show_message(get_chapterlist(), 3) end
   3279     ne.eventresponder["mbtn_right_up"] =
   3280         function () show_message(get_chapterlist(), 3) end
   3281 
   3282     --
   3283     update_tracklist()
   3284 
   3285     --cy_audio
   3286     ne = new_element("cy_audio", "button")
   3287 
   3288     ne.enabled = (#tracks_osc.audio > 0)
   3289     ne.content = function ()
   3290         local aid = "–"
   3291         if not (get_track("audio") == 0) then
   3292             aid = get_track("audio")
   3293         end
   3294         return ("\238\132\134" .. osc_styles.smallButtonsLlabel
   3295             .. " " .. aid .. "/" .. #tracks_osc.audio)
   3296     end
   3297     ne.eventresponder["mbtn_left_up"] =
   3298         function () set_track("audio", 1) end
   3299     ne.eventresponder["mbtn_right_up"] =
   3300         function () set_track("audio", -1) end
   3301     ne.eventresponder["shift+mbtn_left_down"] =
   3302         function () show_message(get_tracklist("audio"), 2) end
   3303 
   3304     --cy_sub
   3305     ne = new_element("cy_sub", "button")
   3306 
   3307     ne.enabled = (#tracks_osc.sub > 0)
   3308     ne.content = function ()
   3309         local sid = "–"
   3310         if not (get_track("sub") == 0) then
   3311             sid = get_track("sub")
   3312         end
   3313         return ("\238\132\135" .. osc_styles.smallButtonsLlabel
   3314             .. " " .. sid .. "/" .. #tracks_osc.sub)
   3315     end
   3316     ne.eventresponder["mbtn_left_up"] =
   3317         function () set_track("sub", 1) end
   3318     ne.eventresponder["mbtn_right_up"] =
   3319         function () set_track("sub", -1) end
   3320     ne.eventresponder["shift+mbtn_left_down"] =
   3321         function () show_message(get_tracklist("sub"), 2) end
   3322 
   3323     --tog_fs
   3324     ne = new_element("tog_fs", "button")
   3325     ne.content = function ()
   3326         if (state.fullscreen) then
   3327             return ("\238\132\137")
   3328         else
   3329             return ("\238\132\136")
   3330         end
   3331     end
   3332     ne.eventresponder["mbtn_left_up"] =
   3333         function () mp.commandv("cycle", "fullscreen") end
   3334 
   3335     --seekbar
   3336     ne = new_element("seekbar", "slider")
   3337 
   3338     ne.enabled = not (mp.get_property("percent-pos") == nil)
   3339     ne.slider.markerF = function ()
   3340         local duration = mp.get_property_number("duration", nil)
   3341         if not (duration == nil) then
   3342             local chapters = mp.get_property_native("chapter-list", {})
   3343             local markers = {}
   3344             for n = 1, #chapters do
   3345                 markers[n] = (chapters[n].time / duration * 100)
   3346             end
   3347             return markers
   3348         else
   3349             return {}
   3350         end
   3351     end
   3352     ne.slider.posF =
   3353         function () return mp.get_property_number("percent-pos", nil) end
   3354     ne.slider.tooltipF = function (pos)
   3355         local duration = mp.get_property_number("duration", nil)
   3356         if not ((duration == nil) or (pos == nil)) then
   3357             possec = duration * (pos / 100)
   3358             return mp.format_time(possec)
   3359         else
   3360             return ""
   3361         end
   3362     end
   3363     ne.slider.seekRangesF = function()
   3364         if user_opts.seekrangestyle == "none" then
   3365             return nil
   3366         end
   3367         local cache_state = state.cache_state
   3368         if not cache_state then
   3369             return nil
   3370         end
   3371         local duration = mp.get_property_number("duration", nil)
   3372         if (duration == nil) or duration <= 0 then
   3373             return nil
   3374         end
   3375         local ranges = cache_state["seekable-ranges"]
   3376         if #ranges == 0 then
   3377             return nil
   3378         end
   3379         local nranges = {}
   3380         for _, range in pairs(ranges) do
   3381             nranges[#nranges + 1] = {
   3382                 ["start"] = 100 * range["start"] / duration,
   3383                 ["end"] = 100 * range["end"] / duration,
   3384             }
   3385         end
   3386         return nranges
   3387     end
   3388     ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
   3389         function (element)
   3390             -- mouse move events may pile up during seeking and may still get
   3391             -- sent when the user is done seeking, so we need to throw away
   3392             -- identical seeks
   3393             local seekto = get_slider_value(element)
   3394             if (element.state.lastseek == nil) or
   3395                 (not (element.state.lastseek == seekto)) then
   3396                     mp.commandv("seek", seekto, "absolute-percent",
   3397                         user_opts.seekbarkeyframes and "keyframes" or "exact")
   3398                     element.state.lastseek = seekto
   3399             end
   3400 
   3401         end
   3402     ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks
   3403         function (element) mp.commandv("seek", get_slider_value(element),
   3404             "absolute-percent", "exact") end
   3405     ne.eventresponder["reset"] =
   3406         function (element) element.state.lastseek = nil end
   3407 
   3408 
   3409     -- tc_left (current pos)
   3410     ne = new_element("tc_left", "button")
   3411 
   3412     ne.content = function ()
   3413         if (state.tc_ms) then
   3414             return (mp.get_property_osd("playback-time/full"))
   3415         else
   3416             return (mp.get_property_osd("playback-time"))
   3417         end
   3418     end
   3419     ne.eventresponder["mbtn_left_up"] = function ()
   3420         state.tc_ms = not state.tc_ms
   3421         request_init()
   3422     end
   3423 
   3424     -- tc_right (total/remaining time)
   3425     ne = new_element("tc_right", "button")
   3426 
   3427     ne.visible = (mp.get_property_number("duration", 0) > 0)
   3428     ne.content = function ()
   3429         if (state.rightTC_trem) then
   3430             if state.tc_ms then
   3431                 return ("-"..mp.get_property_osd("playtime-remaining/full"))
   3432             else
   3433                 return ("-"..mp.get_property_osd("playtime-remaining"))
   3434             end
   3435         else
   3436             if state.tc_ms then
   3437                 return (mp.get_property_osd("duration/full"))
   3438             else
   3439                 return (mp.get_property_osd("duration"))
   3440             end
   3441         end
   3442     end
   3443     ne.eventresponder["mbtn_left_up"] =
   3444         function () state.rightTC_trem = not state.rightTC_trem end
   3445 
   3446     -- cache
   3447     ne = new_element("cache", "button")
   3448 
   3449     ne.content = function ()
   3450         local cache_state = state.cache_state
   3451         if not (cache_state and cache_state["seekable-ranges"] and
   3452             #cache_state["seekable-ranges"] > 0) then
   3453             -- probably not a network stream
   3454             return ""
   3455         end
   3456         local dmx_cache = mp.get_property_number("demuxer-cache-duration")
   3457         if dmx_cache and (dmx_cache > state.dmx_cache * 1.1 or
   3458                 dmx_cache < state.dmx_cache * 0.9) then
   3459             state.dmx_cache = dmx_cache
   3460         else
   3461             dmx_cache = state.dmx_cache
   3462         end
   3463         local min = math.floor(dmx_cache / 60)
   3464         local sec = dmx_cache % 60
   3465         return "Cache: " .. (min > 0 and
   3466             string.format("%sm%02.0fs", min, sec) or
   3467             string.format("%3.0fs", dmx_cache))
   3468     end
   3469 
   3470     -- volume
   3471     ne = new_element("volume", "button")
   3472 
   3473     ne.content = function()
   3474         local volume = mp.get_property_number("volume", 0)
   3475         local mute = mp.get_property_native("mute")
   3476         local volicon = {"\238\132\139", "\238\132\140",
   3477                          "\238\132\141", "\238\132\142"}
   3478         if volume == 0 or mute then
   3479             return "\238\132\138"
   3480         else
   3481             return volicon[math.min(4,math.ceil(volume / (100/3)))]
   3482         end
   3483     end
   3484     ne.eventresponder["mbtn_left_up"] =
   3485         function () mp.commandv("cycle", "mute") end
   3486 
   3487     ne.eventresponder["wheel_up_press"] =
   3488         function () mp.commandv("osd-auto", "add", "volume", 5) end
   3489     ne.eventresponder["wheel_down_press"] =
   3490         function () mp.commandv("osd-auto", "add", "volume", -5) end
   3491 
   3492 
   3493     -- load layout
   3494     layouts[user_opts.layout]()
   3495 
   3496     -- load window controls
   3497     if window_controls_enabled() then
   3498         window_controls(user_opts.layout == "topbar")
   3499     end
   3500 
   3501     --do something with the elements
   3502     prepare_elements()
   3503 
   3504     if user_opts.boxvideo then
   3505         -- check whether any margin option has a non-default value
   3506         local margins_used = false
   3507 
   3508         for _, opt in ipairs(margins_opts) do
   3509             if mp.get_property_number(opt[2], 0.0) ~= 0.0 then
   3510                 margins_used = true
   3511             end
   3512         end
   3513 
   3514         if not margins_used then
   3515             local margins = osc_param.video_margins
   3516             for _, opt in ipairs(margins_opts) do
   3517                 local v = margins[opt[1]]
   3518                 if v ~= 0 then
   3519                     mp.set_property_number(opt[2], v)
   3520                     state.using_video_margins = true
   3521                 end
   3522             end
   3523         end
   3524     else
   3525         reset_margins()
   3526     end
   3527 
   3528     update_margins()
   3529 end
   3530 
   3531 function reset_margins()
   3532     if state.using_video_margins then
   3533         for _, opt in ipairs(margins_opts) do
   3534             mp.set_property_number(opt[2], 0.0)
   3535         end
   3536         state.using_video_margins = false
   3537     end
   3538 end
   3539 
   3540 function update_margins()
   3541     local margins = osc_param.video_margins
   3542 
   3543     -- Don't report margins if it's visible only temporarily. At least for
   3544     -- console.lua this makes no sense.
   3545     if (not state.osc_visible) or (user_opts.hidetimeout >= 0) then
   3546         margins = {l = 0, r = 0, t = 0, b = 0}
   3547     end
   3548 
   3549     utils.shared_script_property_set("osc-margins",
   3550         string.format("%f,%f,%f,%f", margins.l, margins.r, margins.t, margins.b))
   3551 end
   3552 
   3553 function shutdown()
   3554     reset_margins()
   3555     utils.shared_script_property_set("osc-margins", nil)
   3556 end
   3557 
   3558 --
   3559 -- Other important stuff
   3560 --
   3561 
   3562 
   3563 function show_osc()
   3564     -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
   3565     if not state.enabled then return end
   3566 
   3567     msg.trace("show_osc")
   3568     --remember last time of invocation (mouse move)
   3569     state.showtime = mp.get_time()
   3570 
   3571     osc_visible(true)
   3572 
   3573     if (user_opts.fadeduration > 0) then
   3574         state.anitype = nil
   3575     end
   3576 end
   3577 
   3578 function hide_osc()
   3579     msg.trace("hide_osc")
   3580     if not state.enabled then
   3581         -- typically hide happens at render() from tick(), but now tick() is
   3582         -- no-op and won't render again to remove the osc, so do that manually.
   3583         state.osc_visible = false
   3584         render_wipe()
   3585     elseif (user_opts.fadeduration > 0) then
   3586         if not(state.osc_visible == false) then
   3587             state.anitype = "out"
   3588             request_tick()
   3589         end
   3590     else
   3591         osc_visible(false)
   3592     end
   3593 end
   3594 
   3595 function osc_visible(visible)
   3596     if state.osc_visible ~= visible then
   3597         state.osc_visible = visible
   3598         update_margins()
   3599     end
   3600     request_tick()
   3601 end
   3602 
   3603 function pause_state(name, enabled)
   3604     state.paused = enabled
   3605     request_tick()
   3606 end
   3607 
   3608 function cache_state(name, st)
   3609     state.cache_state = st
   3610     request_tick()
   3611 end
   3612 
   3613 -- Request that tick() is called (which typically re-renders the OSC).
   3614 -- The tick is then either executed immediately, or rate-limited if it was
   3615 -- called a small time ago.
   3616 function request_tick()
   3617     if state.tick_timer == nil then
   3618         state.tick_timer = mp.add_timeout(0, tick)
   3619     end
   3620 
   3621     if not state.tick_timer:is_enabled() then
   3622         local now = mp.get_time()
   3623         local timeout = tick_delay - (now - state.tick_last_time)
   3624         if timeout < 0 then
   3625             timeout = 0
   3626         end
   3627         state.tick_timer.timeout = timeout
   3628         state.tick_timer:resume()
   3629     end
   3630 end
   3631 
   3632 function mouse_leave()
   3633     if user_opts.hidetimeout >= 0 then
   3634         hide_osc()
   3635     end
   3636     -- reset mouse position
   3637     state.last_mouseX, state.last_mouseY = nil, nil
   3638 end
   3639 
   3640 function request_init()
   3641     state.initREQ = true
   3642 end
   3643 
   3644 function render_wipe()
   3645     msg.trace("render_wipe()")
   3646     mp.set_osd_ass(0, 0, "{}")
   3647 end
   3648 
   3649 function render()
   3650     msg.trace("rendering")
   3651     local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
   3652     local mouseX, mouseY = get_virt_mouse_pos()
   3653     local now = mp.get_time()
   3654 
   3655     -- check if display changed, if so request reinit
   3656     if not (state.mp_screen_sizeX == current_screen_sizeX
   3657         and state.mp_screen_sizeY == current_screen_sizeY) then
   3658 
   3659         request_init()
   3660 
   3661         state.mp_screen_sizeX = current_screen_sizeX
   3662         state.mp_screen_sizeY = current_screen_sizeY
   3663     end
   3664 
   3665     -- init management
   3666     if state.initREQ then
   3667         osc_init()
   3668         state.initREQ = false
   3669 
   3670         -- store initial mouse position
   3671         if (state.last_mouseX == nil or state.last_mouseY == nil)
   3672             and not (mouseX == nil or mouseY == nil) then
   3673 
   3674             state.last_mouseX, state.last_mouseY = mouseX, mouseY
   3675         end
   3676     end
   3677 
   3678 
   3679     -- fade animation
   3680     if not(state.anitype == nil) then
   3681 
   3682         if (state.anistart == nil) then
   3683             state.anistart = now
   3684         end
   3685 
   3686         if (now < state.anistart + (user_opts.fadeduration/1000)) then
   3687 
   3688             if (state.anitype == "in") then --fade in
   3689                 osc_visible(true)
   3690                 state.animation = scale_value(state.anistart,
   3691                     (state.anistart + (user_opts.fadeduration/1000)),
   3692                     255, 0, now)
   3693             elseif (state.anitype == "out") then --fade out
   3694                 state.animation = scale_value(state.anistart,
   3695                     (state.anistart + (user_opts.fadeduration/1000)),
   3696                     0, 255, now)
   3697             end
   3698 
   3699         else
   3700             if (state.anitype == "out") then
   3701                 osc_visible(false)
   3702             end
   3703             state.anistart = nil
   3704             state.animation = nil
   3705             state.anitype =  nil
   3706         end
   3707     else
   3708         state.anistart = nil
   3709         state.animation = nil
   3710         state.anitype =  nil
   3711     end
   3712 
   3713     --mouse show/hide area
   3714     for k,cords in pairs(osc_param.areas["showhide"]) do
   3715         set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
   3716     end
   3717     if osc_param.areas["showhide_wc"] then
   3718         for k,cords in pairs(osc_param.areas["showhide_wc"]) do
   3719             set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc")
   3720         end
   3721     else
   3722         set_virt_mouse_area(0, 0, 0, 0, "showhide_wc")
   3723     end
   3724     do_enable_keybindings()
   3725 
   3726     --mouse input area
   3727     local mouse_over_osc = false
   3728 
   3729     for _,cords in ipairs(osc_param.areas["input"]) do
   3730         if state.osc_visible then -- activate only when OSC is actually visible
   3731             set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
   3732         end
   3733         if state.osc_visible ~= state.input_enabled then
   3734             if state.osc_visible then
   3735                 mp.enable_key_bindings("input")
   3736             else
   3737                 mp.disable_key_bindings("input")
   3738             end
   3739             state.input_enabled = state.osc_visible
   3740         end
   3741 
   3742         if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
   3743             mouse_over_osc = true
   3744         end
   3745     end
   3746 
   3747     if osc_param.areas["window-controls"] then
   3748         for _,cords in ipairs(osc_param.areas["window-controls"]) do
   3749             if state.osc_visible then -- activate only when OSC is actually visible
   3750                 set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls")
   3751                 mp.enable_key_bindings("window-controls")
   3752             else
   3753                 mp.disable_key_bindings("window-controls")
   3754             end
   3755 
   3756             if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
   3757                 mouse_over_osc = true
   3758             end
   3759         end
   3760     end
   3761 
   3762     -- autohide
   3763     if not (state.showtime == nil) and (user_opts.hidetimeout >= 0)
   3764         and (state.showtime + (user_opts.hidetimeout/1000) < now)
   3765         and (state.active_element == nil) and not (mouse_over_osc) then
   3766 
   3767         hide_osc()
   3768     end
   3769 
   3770 
   3771     -- actual rendering
   3772     local ass = assdraw.ass_new()
   3773 
   3774     -- Messages
   3775     render_message(ass)
   3776 
   3777     -- mpv_thumbnail_script.lua --
   3778     local thumb_was_visible = osc_thumb_state.visible
   3779     osc_thumb_state.visible = false
   3780     -- // mpv_thumbnail_script.lua // --
   3781 
   3782     -- actual OSC
   3783     if state.osc_visible then
   3784         render_elements(ass)
   3785     end
   3786 
   3787     -- mpv_thumbnail_script.lua --
   3788     if not osc_thumb_state.visible and thumb_was_visible then
   3789         hide_thumbnail()
   3790     end
   3791     -- // mpv_thumbnail_script.lua // --
   3792 
   3793     -- submit
   3794     mp.set_osd_ass(osc_param.playresy * osc_param.display_aspect,
   3795                    osc_param.playresy, ass.text)
   3796 
   3797 
   3798 
   3799 
   3800 end
   3801 
   3802 --
   3803 -- Eventhandling
   3804 --
   3805 
   3806 local function element_has_action(element, action)
   3807     return element and element.eventresponder and
   3808         element.eventresponder[action]
   3809 end
   3810 
   3811 function process_event(source, what)
   3812     local action = string.format("%s%s", source,
   3813         what and ("_" .. what) or "")
   3814 
   3815     if what == "down" or what == "press" then
   3816 
   3817         for n = 1, #elements do
   3818 
   3819             if mouse_hit(elements[n]) and
   3820                 elements[n].eventresponder and
   3821                 (elements[n].eventresponder[source .. "_up"] or
   3822                     elements[n].eventresponder[action]) then
   3823 
   3824                 if what == "down" then
   3825                     state.active_element = n
   3826                     state.active_event_source = source
   3827                 end
   3828                 -- fire the down or press event if the element has one
   3829                 if element_has_action(elements[n], action) then
   3830                     elements[n].eventresponder[action](elements[n])
   3831                 end
   3832 
   3833             end
   3834         end
   3835 
   3836     elseif what == "up" then
   3837 
   3838         if elements[state.active_element] then
   3839             local n = state.active_element
   3840 
   3841             if n == 0 then
   3842                 --click on background (does not work)
   3843             elseif element_has_action(elements[n], action) and
   3844                 mouse_hit(elements[n]) then
   3845 
   3846                 elements[n].eventresponder[action](elements[n])
   3847             end
   3848 
   3849             --reset active element
   3850             if element_has_action(elements[n], "reset") then
   3851                 elements[n].eventresponder["reset"](elements[n])
   3852             end
   3853 
   3854         end
   3855         state.active_element = nil
   3856         state.mouse_down_counter = 0
   3857 
   3858     elseif source == "mouse_move" then
   3859 
   3860         local mouseX, mouseY = get_virt_mouse_pos()
   3861         if (user_opts.minmousemove == 0) or
   3862             (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
   3863                 ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
   3864                     or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
   3865                 )
   3866             ) then
   3867             show_osc()
   3868         end
   3869         state.last_mouseX, state.last_mouseY = mouseX, mouseY
   3870 
   3871         local n = state.active_element
   3872         if element_has_action(elements[n], action) then
   3873             elements[n].eventresponder[action](elements[n])
   3874         end
   3875         request_tick()
   3876     end
   3877 end
   3878 
   3879 -- called by mpv on every frame
   3880 function tick()
   3881     if (not state.enabled) then return end
   3882 
   3883     if (state.idle) then
   3884 
   3885         -- render idle message
   3886         msg.trace("idle message")
   3887         local icon_x, icon_y = 320 - 26, 140
   3888 
   3889         local ass = assdraw.ass_new()
   3890         ass:new_event()
   3891         ass:pos(icon_x, icon_y)
   3892         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}")
   3893         ass:new_event()
   3894         ass:pos(icon_x, icon_y)
   3895         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}")
   3896         ass:new_event()
   3897         ass:pos(icon_x, icon_y)
   3898         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}")
   3899         ass:new_event()
   3900         ass:pos(icon_x, icon_y)
   3901         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}")
   3902         ass:new_event()
   3903         ass:pos(icon_x, icon_y)
   3904         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}")
   3905         ass:new_event()
   3906         ass:pos(icon_x, icon_y)
   3907         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}")
   3908         ass:new_event()
   3909         ass:pos(320, icon_y+65)
   3910         ass:an(8)
   3911         ass:append("Drop files or URLs to play here.")
   3912         mp.set_osd_ass(640, 360, ass.text)
   3913 
   3914         if state.showhide_enabled then
   3915             mp.disable_key_bindings("showhide")
   3916             mp.disable_key_bindings("showhide_wc")
   3917             state.showhide_enabled = false
   3918         end
   3919 
   3920 
   3921     elseif (state.fullscreen and user_opts.showfullscreen)
   3922         or (not state.fullscreen and user_opts.showwindowed) then
   3923 
   3924         -- render the OSC
   3925         render()
   3926     else
   3927         -- Flush OSD
   3928         mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "")
   3929     end
   3930 
   3931     state.tick_last_time = mp.get_time()
   3932 
   3933     if state.anitype ~= nil then
   3934         request_tick()
   3935     end
   3936 end
   3937 
   3938 function do_enable_keybindings()
   3939     if state.enabled then
   3940         if not state.showhide_enabled then
   3941             mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
   3942             mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor")
   3943         end
   3944         state.showhide_enabled = true
   3945     end
   3946 end
   3947 
   3948 function enable_osc(enable)
   3949     state.enabled = enable
   3950     if enable then
   3951         do_enable_keybindings()
   3952     else
   3953         hide_osc() -- acts immediately when state.enabled == false
   3954         if state.showhide_enabled then
   3955             mp.disable_key_bindings("showhide")
   3956             mp.disable_key_bindings("showhide_wc")
   3957         end
   3958         state.showhide_enabled = false
   3959     end
   3960 end
   3961 
   3962 -- mpv_thumbnail_script.lua --
   3963 
   3964 local builtin_osc_enabled = mp.get_property_native('osc')
   3965 if builtin_osc_enabled then
   3966     local err = "You must disable the built-in OSC with osc=no in your configuration!"
   3967     mp.osd_message(err, 5)
   3968     msg.error(err)
   3969 
   3970     -- This may break, but since we can, let's try to just disable the builtin OSC.
   3971     mp.set_property_native('osc', false)
   3972 end
   3973 
   3974 -- // mpv_thumbnail_script.lua // --
   3975 
   3976 validate_user_opts()
   3977 
   3978 mp.register_event("shutdown", shutdown)
   3979 mp.register_event("start-file", request_init)
   3980 mp.register_event("tracks-changed", request_init)
   3981 mp.observe_property("playlist", nil, request_init)
   3982 
   3983 mp.register_script_message("osc-message", show_message)
   3984 mp.register_script_message("osc-chapterlist", function(dur)
   3985     show_message(get_chapterlist(), dur)
   3986 end)
   3987 mp.register_script_message("osc-playlist", function(dur)
   3988     show_message(get_playlist(), dur)
   3989 end)
   3990 mp.register_script_message("osc-tracklist", function(dur)
   3991     local msg = {}
   3992     for k,v in pairs(nicetypes) do
   3993         table.insert(msg, get_tracklist(k))
   3994     end
   3995     show_message(table.concat(msg, '\n\n'), dur)
   3996 end)
   3997 
   3998 mp.observe_property("fullscreen", "bool",
   3999     function(name, val)
   4000         state.fullscreen = val
   4001         request_init()
   4002     end
   4003 )
   4004 mp.observe_property("border", "bool",
   4005     function(name, val)
   4006         state.border = val
   4007         request_init()
   4008     end
   4009 )
   4010 mp.observe_property("window-maximized", "bool",
   4011     function(name, val)
   4012         state.maximized = val
   4013         request_init()
   4014     end
   4015 )
   4016 mp.observe_property("idle-active", "bool",
   4017     function(name, val)
   4018         state.idle = val
   4019         request_tick()
   4020     end
   4021 )
   4022 mp.observe_property("pause", "bool", pause_state)
   4023 mp.observe_property("demuxer-cache-state", "native", cache_state)
   4024 mp.observe_property("vo-configured", "bool", function(name, val)
   4025     request_tick()
   4026 end)
   4027 mp.observe_property("playback-time", "number", function(name, val)
   4028     request_tick()
   4029 end)
   4030 
   4031 -- mouse show/hide bindings
   4032 mp.set_key_bindings({
   4033     {"mouse_move",              function(e) process_event("mouse_move", nil) end},
   4034     {"mouse_leave",             mouse_leave},
   4035 }, "showhide", "force")
   4036 mp.set_key_bindings({
   4037     {"mouse_move",              function(e) process_event("mouse_move", nil) end},
   4038     {"mouse_leave",             mouse_leave},
   4039 }, "showhide_wc", "force")
   4040 do_enable_keybindings()
   4041 
   4042 --mouse input bindings
   4043 mp.set_key_bindings({
   4044     {"mbtn_left",           function(e) process_event("mbtn_left", "up") end,
   4045                             function(e) process_event("mbtn_left", "down")  end},
   4046     {"shift+mbtn_left",     function(e) process_event("shift+mbtn_left", "up") end,
   4047                             function(e) process_event("shift+mbtn_left", "down")  end},
   4048     {"mbtn_right",          function(e) process_event("mbtn_right", "up") end,
   4049                             function(e) process_event("mbtn_right", "down")  end},
   4050     -- alias to shift_mbtn_left for single-handed mouse use
   4051     {"mbtn_mid",            function(e) process_event("shift+mbtn_left", "up") end,
   4052                             function(e) process_event("shift+mbtn_left", "down")  end},
   4053     {"wheel_up",            function(e) process_event("wheel_up", "press") end},
   4054     {"wheel_down",          function(e) process_event("wheel_down", "press") end},
   4055     {"mbtn_left_dbl",       "ignore"},
   4056     {"shift+mbtn_left_dbl", "ignore"},
   4057     {"mbtn_right_dbl",      "ignore"},
   4058 }, "input", "force")
   4059 mp.enable_key_bindings("input")
   4060 
   4061 mp.set_key_bindings({
   4062     {"mbtn_left",           function(e) process_event("mbtn_left", "up") end,
   4063                             function(e) process_event("mbtn_left", "down")  end},
   4064 }, "window-controls", "force")
   4065 mp.enable_key_bindings("window-controls")
   4066 
   4067 user_opts.hidetimeout_orig = user_opts.hidetimeout
   4068 
   4069 function always_on(val)
   4070     if val then
   4071         user_opts.hidetimeout = -1 -- disable autohide
   4072         if state.enabled then show_osc() end
   4073     else
   4074         user_opts.hidetimeout = user_opts.hidetimeout_orig
   4075         if state.enabled then hide_osc() end
   4076     end
   4077 end
   4078 
   4079 -- mode can be auto/always/never/cycle
   4080 -- the modes only affect internal variables and not stored on its own.
   4081 function visibility_mode(mode, no_osd)
   4082     if mode == "cycle" then
   4083         if not state.enabled then
   4084             mode = "auto"
   4085         elseif user_opts.hidetimeout >= 0 then
   4086             mode = "always"
   4087         else
   4088             mode = "never"
   4089         end
   4090     end
   4091 
   4092     if mode == "auto" then
   4093         always_on(false)
   4094         enable_osc(true)
   4095     elseif mode == "always" then
   4096         enable_osc(true)
   4097         always_on(true)
   4098     elseif mode == "never" then
   4099         enable_osc(false)
   4100     else
   4101         msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
   4102         return
   4103     end
   4104 
   4105     if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
   4106         mp.osd_message("OSC visibility: " .. mode)
   4107     end
   4108 
   4109     update_margins()
   4110 end
   4111 
   4112 visibility_mode(user_opts.visibility, true)
   4113 mp.register_script_message("osc-visibility", visibility_mode)
   4114 mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
   4115 
   4116 set_virt_mouse_area(0, 0, 0, 0, "input")
   4117 set_virt_mouse_area(0, 0, 0, 0, "window-controls")