Advertisement
Guest User

mpv_thumbnail_script_client_osc.lua - Fixed issue with OSC

a guest
Jan 30th, 2021
157
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 125.81 KB | None | 0 0
  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 a2de250 (branch master)
  19. https://github.com/TheAMM/mpv_thumbnail_script
  20. Built on 2018-02-07 20:36:55
  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: 97816bbef0f97cfda7abdbe560707481d5f68ccd
  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. --
  1242. -- Parameters
  1243. --
  1244.  
  1245. -- default user option values
  1246. -- do not touch, change them in osc.conf
  1247. local user_opts = {
  1248. showwindowed = true, -- show OSC when windowed?
  1249. showfullscreen = true, -- show OSC when fullscreen?
  1250. scalewindowed = 1, -- scaling of the controller when windowed
  1251. scalefullscreen = 1, -- scaling of the controller when fullscreen
  1252. scaleforcedwindow = 2, -- scaling when rendered on a forced window
  1253. vidscale = true, -- scale the controller with the video?
  1254. valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom)
  1255. halign = 0, -- horizontal alignment, -1 (left) to 1 (right)
  1256. barmargin = 0, -- vertical margin of top/bottombar
  1257. boxalpha = 80, -- alpha of the background box,
  1258. -- 0 (opaque) to 255 (fully transparent)
  1259. hidetimeout = 500, -- duration in ms until the OSC hides if no
  1260. -- mouse movement. enforced non-negative for the
  1261. -- user, but internally negative is "always-on".
  1262. fadeduration = 200, -- duration of fade out in ms, 0 = no fade
  1263. deadzonesize = 0.5, -- size of deadzone
  1264. minmousemove = 0, -- minimum amount of pixels the mouse has to
  1265. -- move between ticks to make the OSC show up
  1266. iamaprogrammer = false, -- use native mpv values and disable OSC
  1267. -- internal track list management (and some
  1268. -- functions that depend on it)
  1269. layout = "bottombar",
  1270. seekbarstyle = "bar", -- slider (diamond marker), knob (circle
  1271. -- marker with guide), or bar (fill)
  1272. title = "${media-title}", -- string compatible with property-expansion
  1273. -- to be shown as OSC title
  1274. tooltipborder = 1, -- border of tooltip in bottom/topbar
  1275. timetotal = false, -- display total time instead of remaining time?
  1276. timems = false, -- display timecodes with milliseconds?
  1277. seekranges = true, -- display seek ranges?
  1278. visibility = "auto", -- only used at init to set visibility_mode(...)
  1279. boxmaxchars = 80, -- title crop threshold for box layout
  1280. }
  1281.  
  1282. -- read_options may modify hidetimeout, so save the original default value in
  1283. -- case the user set hidetimeout < 0 and we need the default instead.
  1284. local hidetimeout_def = user_opts.hidetimeout
  1285. -- read options from config and command-line
  1286. opt.read_options(user_opts, "osc")
  1287. if user_opts.hidetimeout < 0 then
  1288. user_opts.hidetimeout = hidetimeout_def
  1289. msg.warn("hidetimeout cannot be negative. Using " .. user_opts.hidetimeout)
  1290. end
  1291.  
  1292.  
  1293. -- mpv_thumbnail_script.lua --
  1294.  
  1295. -- Patch in msg.trace
  1296. if not msg.trace then
  1297. msg.trace = function(...) return mp.log("trace", ...) end
  1298. end
  1299.  
  1300. -- Patch in utils.format_bytes_humanized
  1301. if not utils.format_bytes_humanized then
  1302. utils.format_bytes_humanized = function(b)
  1303. local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"}
  1304. local i = 1
  1305. while b >= 1024 do
  1306. b = b / 1024
  1307. i = i + 1
  1308. end
  1309. return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1))
  1310. end
  1311. end
  1312.  
  1313. Thumbnailer:register_client()
  1314.  
  1315. function get_thumbnail_y_offset(thumb_size, msy)
  1316. local layout = user_opts.layout
  1317. local offset = 0
  1318.  
  1319. if layout == "bottombar" then
  1320. offset = 15 --+ margin
  1321. elseif layout == "topbar" then
  1322. offset = -(thumb_size.h * msy + 15)
  1323. elseif layout == "box" then
  1324. offset = 15
  1325. elseif layout == "slimbox" then
  1326. offset = 12
  1327. end
  1328.  
  1329. return offset / msy
  1330. end
  1331.  
  1332.  
  1333. local osc_thumb_state = {
  1334. visible = false,
  1335. overlay_id = 1,
  1336.  
  1337. last_path = nil,
  1338. last_x = nil,
  1339. last_y = nil,
  1340. }
  1341.  
  1342. function hide_thumbnail()
  1343. osc_thumb_state.visible = false
  1344. osc_thumb_state.last_path = nil
  1345. mp.command_native({ "overlay-remove", osc_thumb_state.overlay_id })
  1346. end
  1347.  
  1348. function display_thumbnail(pos, value, ass)
  1349. -- If thumbnails are not available, bail
  1350. if not (Thumbnailer.state.enabled and Thumbnailer.state.available) then
  1351. return
  1352. end
  1353.  
  1354. local duration = mp.get_property_number("duration", nil)
  1355. if not ((duration == nil) or (value == nil)) then
  1356. target_position = duration * (value / 100)
  1357.  
  1358. local msx, msy = get_virt_scale_factor()
  1359. local osd_w, osd_h = mp.get_osd_size()
  1360.  
  1361. local thumb_size = Thumbnailer.state.thumbnail_size
  1362. local thumb_path, thumb_index, closest_index = Thumbnailer:get_thumbnail_path(target_position)
  1363.  
  1364. local thumbs_ready = Thumbnailer.state.finished_thumbnails
  1365. local thumbs_total = Thumbnailer.state.thumbnail_count
  1366. local perc = math.floor((thumbs_ready / thumbs_total) * 100)
  1367.  
  1368. local display_progress = thumbs_ready ~= thumbs_total and not thumbnailer_options.hide_progress
  1369.  
  1370. local vertical_offset = thumbnailer_options.vertical_offset
  1371. local padding = thumbnailer_options.background_padding
  1372.  
  1373. local pad = {
  1374. l = thumbnailer_options.pad_left, r = thumbnailer_options.pad_right,
  1375. t = thumbnailer_options.pad_top, b = thumbnailer_options.pad_bot
  1376. }
  1377. if thumbnailer_options.pad_in_screenspace then
  1378. pad.l = pad.l * msx
  1379. pad.r = pad.r * msx
  1380. pad.t = pad.t * msy
  1381. pad.b = pad.b * msy
  1382. end
  1383.  
  1384. if thumbnailer_options.offset_by_pad then
  1385. vertical_offset = vertical_offset + (user_opts.layout == "topbar" and pad.t or pad.b)
  1386. end
  1387.  
  1388. local ass_w = thumb_size.w * msx
  1389. local ass_h = thumb_size.h * msy
  1390. local y_offset = get_thumbnail_y_offset(thumb_size, 1)
  1391.  
  1392. -- Constrain thumbnail display to window
  1393. -- (ie. don't let it go off-screen left/right)
  1394. if thumbnailer_options.constrain_to_screen and osd_w > (ass_w + pad.l + pad.r)/msx then
  1395. local padded_left = (pad.l + (ass_w / 2))
  1396. local padded_right = (pad.r + (ass_w / 2))
  1397. if pos.x - padded_left < 0 then
  1398. pos.x = padded_left
  1399. elseif pos.x + padded_right > osd_w*msx then
  1400. pos.x = osd_w*msx - padded_right
  1401. end
  1402. end
  1403.  
  1404. local text_h = 30 * msy
  1405. local bg_h = ass_h + (display_progress and text_h or 0)
  1406. local bg_left = pos.x - ass_w/2
  1407. local framegraph_h = 10 * msy
  1408.  
  1409. local bg_top = nil
  1410. local text_top = nil
  1411. local framegraph_top = nil
  1412.  
  1413. if user_opts.layout == "topbar" then
  1414. bg_top = pos.y - ( y_offset + thumb_size.h ) + vertical_offset
  1415. text_top = bg_top + ass_h + framegraph_h
  1416. framegraph_top = bg_top + ass_h
  1417. vertical_offset = -vertical_offset
  1418. else
  1419. bg_top = pos.y - y_offset - bg_h - vertical_offset
  1420. text_top = bg_top
  1421. framegraph_top = bg_top + 20 * msy
  1422. end
  1423.  
  1424. if display_progress then
  1425. if user_opts.layout == "topbar" then
  1426. pad.b = math.max(0, pad.b - 30)
  1427. else
  1428. pad.t = math.max(0, pad.t - 30)
  1429. end
  1430. end
  1431.  
  1432.  
  1433.  
  1434. -- Draw background
  1435. ass:new_event()
  1436. ass:pos(bg_left, bg_top)
  1437. ass:append(("{\\bord0\\1c&H%s&\\1a&H%X&}"):format(thumbnailer_options.background_color, thumbnailer_options.background_alpha))
  1438. ass:draw_start()
  1439. ass:rect_cw(-pad.l, -pad.t, ass_w+pad.r, bg_h+pad.b)
  1440. ass:draw_stop()
  1441.  
  1442. if display_progress then
  1443.  
  1444. ass:new_event()
  1445. ass:pos(pos.x, text_top)
  1446. ass:an(8)
  1447. -- Scale text to correct size
  1448. ass:append(("{\\fs20\\bord0\\fscx%f\\fscy%f}"):format(100*msx, 100*msy))
  1449. ass:append(("%d%% - %d/%d"):format(perc, thumbs_ready, thumbs_total))
  1450.  
  1451. -- Draw the generation progress
  1452. local block_w = thumb_size.w * (Thumbnailer.state.thumbnail_delta / duration) * msy
  1453. local block_max_x = thumb_size.w * msy
  1454.  
  1455. -- Draw finished thumbnail blocks (white)
  1456. ass:new_event()
  1457. ass:pos(bg_left, framegraph_top)
  1458. ass:append(("{\\bord0\\1c&HFFFFFF&\\1a&H%X&"):format(0))
  1459. ass:draw_start(2)
  1460. for i, v in pairs(Thumbnailer.state.thumbnails) do
  1461. if i ~= closest_index and v > 0 then
  1462. ass:rect_cw((i-1)*block_w, 0, math.min(block_max_x, i*block_w), framegraph_h)
  1463. end
  1464. end
  1465. ass:draw_stop()
  1466.  
  1467. -- Draw in-progress thumbnail blocks (grayish green)
  1468. ass:new_event()
  1469. ass:pos(bg_left, framegraph_top)
  1470. ass:append(("{\\bord0\\1c&H44AA44&\\1a&H%X&"):format(0))
  1471. ass:draw_start(2)
  1472. for i, v in pairs(Thumbnailer.state.thumbnails) do
  1473. if i ~= closest_index and v == 0 then
  1474. ass:rect_cw((i-1)*block_w, 0, math.min(block_max_x, i*block_w), framegraph_h)
  1475. end
  1476. end
  1477. ass:draw_stop()
  1478.  
  1479. if closest_index ~= nil then
  1480. ass:new_event()
  1481. ass:pos(bg_left, framegraph_top)
  1482. ass:append(("{\\bord0\\1c&H4444FF&\\1a&H%X&"):format(0))
  1483. ass:draw_start(2)
  1484. ass:rect_cw((closest_index-1)*block_w, 0, math.min(block_max_x, closest_index*block_w), framegraph_h)
  1485. ass:draw_stop()
  1486. end
  1487. end
  1488.  
  1489. if thumb_path then
  1490. local overlay_y_offset = get_thumbnail_y_offset(thumb_size, msy)
  1491.  
  1492. local thumb_x = math.floor(pos.x / msx - thumb_size.w/2)
  1493. local thumb_y = math.floor(pos.y / msy - thumb_size.h - overlay_y_offset - vertical_offset/msy)
  1494.  
  1495. osc_thumb_state.visible = true
  1496. 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
  1497. local overlay_add_args = {
  1498. "overlay-add", osc_thumb_state.overlay_id,
  1499. thumb_x, thumb_y,
  1500. thumb_path,
  1501. 0,
  1502. "bgra",
  1503. thumb_size.w, thumb_size.h,
  1504. 4 * thumb_size.w
  1505. }
  1506. mp.command_native(overlay_add_args)
  1507.  
  1508. osc_thumb_state.last_path = thumb_path
  1509. osc_thumb_state.last_x = thumb_x
  1510. osc_thumb_state.last_y = thumb_y
  1511. end
  1512. end
  1513. end
  1514. end
  1515.  
  1516. -- // mpv_thumbnail_script.lua // --
  1517.  
  1518.  
  1519. local osc_param = { -- calculated by osc_init()
  1520. playresy = 0, -- canvas size Y
  1521. playresx = 0, -- canvas size X
  1522. display_aspect = 1,
  1523. unscaled_y = 0,
  1524. areas = {},
  1525. }
  1526.  
  1527. local osc_styles = {
  1528. bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
  1529. smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
  1530. smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}",
  1531. smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
  1532. topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
  1533.  
  1534. elementDown = "{\\1c&H999999}",
  1535. timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
  1536. vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
  1537. box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
  1538.  
  1539. topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
  1540. smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
  1541. timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
  1542. timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
  1543. vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
  1544. }
  1545.  
  1546. -- internal states, do not touch
  1547. local state = {
  1548. showtime, -- time of last invocation (last mouse move)
  1549. osc_visible = false,
  1550. anistart, -- time when the animation started
  1551. anitype, -- current type of animation
  1552. animation, -- current animation alpha
  1553. mouse_down_counter = 0, -- used for softrepeat
  1554. active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
  1555. active_event_source = nil, -- the "button" that issued the current event
  1556. rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
  1557. tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
  1558. mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
  1559. initREQ = false, -- is a re-init request pending?
  1560. last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
  1561. message_text,
  1562. message_timeout,
  1563. fullscreen = false,
  1564. timer = nil,
  1565. cache_idle = false,
  1566. idle = false,
  1567. enabled = true,
  1568. input_enabled = true,
  1569. showhide_enabled = false,
  1570. }
  1571.  
  1572.  
  1573.  
  1574.  
  1575. --
  1576. -- Helperfunctions
  1577. --
  1578.  
  1579. -- scale factor for translating between real and virtual ASS coordinates
  1580. function get_virt_scale_factor()
  1581. local w, h = mp.get_osd_size()
  1582. if w <= 0 or h <= 0 then
  1583. return 0, 0
  1584. end
  1585. return osc_param.playresx / w, osc_param.playresy / h
  1586. end
  1587.  
  1588. -- return mouse position in virtual ASS coordinates (playresx/y)
  1589. function get_virt_mouse_pos()
  1590. local sx, sy = get_virt_scale_factor()
  1591. local x, y = mp.get_mouse_pos()
  1592. return x * sx, y * sy
  1593. end
  1594.  
  1595. function set_virt_mouse_area(x0, y0, x1, y1, name)
  1596. local sx, sy = get_virt_scale_factor()
  1597. mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
  1598. end
  1599.  
  1600. function scale_value(x0, x1, y0, y1, val)
  1601. local m = (y1 - y0) / (x1 - x0)
  1602. local b = y0 - (m * x0)
  1603. return (m * val) + b
  1604. end
  1605.  
  1606. -- returns hitbox spanning coordinates (top left, bottom right corner)
  1607. -- according to alignment
  1608. function get_hitbox_coords(x, y, an, w, h)
  1609.  
  1610. local alignments = {
  1611. [1] = function () return x, y-h, x+w, y end,
  1612. [2] = function () return x-(w/2), y-h, x+(w/2), y end,
  1613. [3] = function () return x-w, y-h, x, y end,
  1614.  
  1615. [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
  1616. [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
  1617. [6] = function () return x-w, y-(h/2), x, y+(h/2) end,
  1618.  
  1619. [7] = function () return x, y, x+w, y+h end,
  1620. [8] = function () return x-(w/2), y, x+(w/2), y+h end,
  1621. [9] = function () return x-w, y, x, y+h end,
  1622. }
  1623.  
  1624. return alignments[an]()
  1625. end
  1626.  
  1627. function get_hitbox_coords_geo(geometry)
  1628. return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
  1629. geometry.w, geometry.h)
  1630. end
  1631.  
  1632. function get_element_hitbox(element)
  1633. return element.hitbox.x1, element.hitbox.y1,
  1634. element.hitbox.x2, element.hitbox.y2
  1635. end
  1636.  
  1637. function mouse_hit(element)
  1638. return mouse_hit_coords(get_element_hitbox(element))
  1639. end
  1640.  
  1641. function mouse_hit_coords(bX1, bY1, bX2, bY2)
  1642. local mX, mY = get_virt_mouse_pos()
  1643. return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
  1644. end
  1645.  
  1646. function limit_range(min, max, val)
  1647. if val > max then
  1648. val = max
  1649. elseif val < min then
  1650. val = min
  1651. end
  1652. return val
  1653. end
  1654.  
  1655. -- translate value into element coordinates
  1656. function get_slider_ele_pos_for(element, val)
  1657.  
  1658. local ele_pos = scale_value(
  1659. element.slider.min.value, element.slider.max.value,
  1660. element.slider.min.ele_pos, element.slider.max.ele_pos,
  1661. val)
  1662.  
  1663. return limit_range(
  1664. element.slider.min.ele_pos, element.slider.max.ele_pos,
  1665. ele_pos)
  1666. end
  1667.  
  1668. -- translates global (mouse) coordinates to value
  1669. function get_slider_value_at(element, glob_pos)
  1670.  
  1671. local val = scale_value(
  1672. element.slider.min.glob_pos, element.slider.max.glob_pos,
  1673. element.slider.min.value, element.slider.max.value,
  1674. glob_pos)
  1675.  
  1676. return limit_range(
  1677. element.slider.min.value, element.slider.max.value,
  1678. val)
  1679. end
  1680.  
  1681. -- get value at current mouse position
  1682. function get_slider_value(element)
  1683. return get_slider_value_at(element, get_virt_mouse_pos())
  1684. end
  1685.  
  1686. function countone(val)
  1687. if not (user_opts.iamaprogrammer) then
  1688. val = val + 1
  1689. end
  1690. return val
  1691. end
  1692.  
  1693. -- align: -1 .. +1
  1694. -- frame: size of the containing area
  1695. -- obj: size of the object that should be positioned inside the area
  1696. -- margin: min. distance from object to frame (as long as -1 <= align <= +1)
  1697. function get_align(align, frame, obj, margin)
  1698. return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
  1699. end
  1700.  
  1701. -- multiplies two alpha values, formular can probably be improved
  1702. function mult_alpha(alphaA, alphaB)
  1703. return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
  1704. end
  1705.  
  1706. function add_area(name, x1, y1, x2, y2)
  1707. -- create area if needed
  1708. if (osc_param.areas[name] == nil) then
  1709. osc_param.areas[name] = {}
  1710. end
  1711. table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
  1712. end
  1713.  
  1714.  
  1715. --
  1716. -- Tracklist Management
  1717. --
  1718.  
  1719. local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
  1720.  
  1721. -- updates the OSC internal playlists, should be run each time the track-layout changes
  1722. function update_tracklist()
  1723. local tracktable = mp.get_property_native("track-list", {})
  1724.  
  1725. -- by osc_id
  1726. tracks_osc = {}
  1727. tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
  1728. -- by mpv_id
  1729. tracks_mpv = {}
  1730. tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
  1731. for n = 1, #tracktable do
  1732. if not (tracktable[n].type == "unknown") then
  1733. local type = tracktable[n].type
  1734. local mpv_id = tonumber(tracktable[n].id)
  1735.  
  1736. -- by osc_id
  1737. table.insert(tracks_osc[type], tracktable[n])
  1738.  
  1739. -- by mpv_id
  1740. tracks_mpv[type][mpv_id] = tracktable[n]
  1741. tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
  1742. end
  1743. end
  1744. end
  1745.  
  1746. -- return a nice list of tracks of the given type (video, audio, sub)
  1747. function get_tracklist(type)
  1748. local msg = "Available " .. nicetypes[type] .. " Tracks: "
  1749. if #tracks_osc[type] == 0 then
  1750. msg = msg .. "none"
  1751. else
  1752. for n = 1, #tracks_osc[type] do
  1753. local track = tracks_osc[type][n]
  1754. local lang, title, selected = "unknown", "", "○"
  1755. if not(track.lang == nil) then lang = track.lang end
  1756. if not(track.title == nil) then title = track.title end
  1757. if (track.id == tonumber(mp.get_property(type))) then
  1758. selected = "●"
  1759. end
  1760. msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title
  1761. end
  1762. end
  1763. return msg
  1764. end
  1765.  
  1766. -- relatively change the track of given <type> by <next> tracks
  1767. --(+1 -> next, -1 -> previous)
  1768. function set_track(type, next)
  1769. local current_track_mpv, current_track_osc
  1770. if (mp.get_property(type) == "no") then
  1771. current_track_osc = 0
  1772. else
  1773. current_track_mpv = tonumber(mp.get_property(type))
  1774. current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
  1775. end
  1776. local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
  1777. local new_track_mpv
  1778. if new_track_osc == 0 then
  1779. new_track_mpv = "no"
  1780. else
  1781. new_track_mpv = tracks_osc[type][new_track_osc].id
  1782. end
  1783.  
  1784. mp.commandv("set", type, new_track_mpv)
  1785.  
  1786. if (new_track_osc == 0) then
  1787. show_message(nicetypes[type] .. " Track: none")
  1788. else
  1789. show_message(nicetypes[type] .. " Track: "
  1790. .. new_track_osc .. "/" .. #tracks_osc[type]
  1791. .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
  1792. .. (tracks_osc[type][new_track_osc].title or ""))
  1793. end
  1794. end
  1795.  
  1796. -- get the currently selected track of <type>, OSC-style counted
  1797. function get_track(type)
  1798. local track = mp.get_property(type)
  1799. if track ~= "no" and track ~= nil then
  1800. local tr = tracks_mpv[type][tonumber(track)]
  1801. if tr then
  1802. return tr.osc_id
  1803. end
  1804. end
  1805. return 0
  1806. end
  1807.  
  1808.  
  1809. --
  1810. -- Element Management
  1811. --
  1812.  
  1813. local elements = {}
  1814.  
  1815. function prepare_elements()
  1816.  
  1817. -- remove elements without layout or invisble
  1818. local elements2 = {}
  1819. for n, element in pairs(elements) do
  1820. if not (element.layout == nil) and (element.visible) then
  1821. table.insert(elements2, element)
  1822. end
  1823. end
  1824. elements = elements2
  1825.  
  1826. function elem_compare (a, b)
  1827. return a.layout.layer < b.layout.layer
  1828. end
  1829.  
  1830. table.sort(elements, elem_compare)
  1831.  
  1832.  
  1833. for _,element in pairs(elements) do
  1834.  
  1835. local elem_geo = element.layout.geometry
  1836.  
  1837. -- Calculate the hitbox
  1838. local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
  1839. element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
  1840.  
  1841. local style_ass = assdraw.ass_new()
  1842.  
  1843. -- prepare static elements
  1844. style_ass:append("{}") -- hack to troll new_event into inserting a \n
  1845. style_ass:new_event()
  1846. style_ass:pos(elem_geo.x, elem_geo.y)
  1847. style_ass:an(elem_geo.an)
  1848. style_ass:append(element.layout.style)
  1849.  
  1850. element.style_ass = style_ass
  1851.  
  1852. local static_ass = assdraw.ass_new()
  1853.  
  1854.  
  1855. if (element.type == "box") then
  1856. --draw box
  1857. static_ass:draw_start()
  1858. static_ass:round_rect_cw(0, 0, elem_geo.w, elem_geo.h,
  1859. element.layout.box.radius)
  1860. static_ass:draw_stop()
  1861.  
  1862.  
  1863. elseif (element.type == "slider") then
  1864. --draw static slider parts
  1865.  
  1866. local slider_lo = element.layout.slider
  1867. -- offset between element outline and drag-area
  1868. local foV = slider_lo.border + slider_lo.gap
  1869.  
  1870. -- calculate positions of min and max points
  1871. if (slider_lo.stype == "slider") or
  1872. (slider_lo.stype == "knob") then
  1873. element.slider.min.ele_pos = elem_geo.h / 2
  1874. element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
  1875.  
  1876. elseif (slider_lo.stype == "bar") then
  1877. element.slider.min.ele_pos =
  1878. slider_lo.border + slider_lo.gap
  1879. element.slider.max.ele_pos =
  1880. elem_geo.w - (slider_lo.border + slider_lo.gap)
  1881. end
  1882.  
  1883. element.slider.min.glob_pos =
  1884. element.hitbox.x1 + element.slider.min.ele_pos
  1885. element.slider.max.glob_pos =
  1886. element.hitbox.x1 + element.slider.max.ele_pos
  1887.  
  1888. -- -- --
  1889.  
  1890. static_ass:draw_start()
  1891.  
  1892. -- the box
  1893. static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h);
  1894.  
  1895. -- the "hole"
  1896. static_ass:rect_ccw(slider_lo.border, slider_lo.border,
  1897. elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border)
  1898.  
  1899. -- marker nibbles
  1900. if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
  1901. local markers = element.slider.markerF()
  1902. for _,marker in pairs(markers) do
  1903. if (marker > element.slider.min.value) and
  1904. (marker < element.slider.max.value) then
  1905.  
  1906. local s = get_slider_ele_pos_for(element, marker)
  1907.  
  1908. if (slider_lo.gap > 1) then -- draw triangles
  1909.  
  1910. local a = slider_lo.gap / 0.5 --0.866
  1911.  
  1912. --top
  1913. if (slider_lo.nibbles_top) then
  1914. static_ass:move_to(s - (a/2), slider_lo.border)
  1915. static_ass:line_to(s + (a/2), slider_lo.border)
  1916. static_ass:line_to(s, foV)
  1917. end
  1918.  
  1919. --bottom
  1920. if (slider_lo.nibbles_bottom) then
  1921. static_ass:move_to(s - (a/2),
  1922. elem_geo.h - slider_lo.border)
  1923. static_ass:line_to(s,
  1924. elem_geo.h - foV)
  1925. static_ass:line_to(s + (a/2),
  1926. elem_geo.h - slider_lo.border)
  1927. end
  1928.  
  1929. else -- draw 2x1px nibbles
  1930.  
  1931. --top
  1932. if (slider_lo.nibbles_top) then
  1933. static_ass:rect_cw(s - 1, slider_lo.border,
  1934. s + 1, slider_lo.border + slider_lo.gap);
  1935. end
  1936.  
  1937. --bottom
  1938. if (slider_lo.nibbles_bottom) then
  1939. static_ass:rect_cw(s - 1,
  1940. elem_geo.h -slider_lo.border -slider_lo.gap,
  1941. s + 1, elem_geo.h - slider_lo.border);
  1942. end
  1943. end
  1944. end
  1945. end
  1946. end
  1947. end
  1948.  
  1949. element.static_ass = static_ass
  1950.  
  1951.  
  1952. -- if the element is supposed to be disabled,
  1953. -- style it accordingly and kill the eventresponders
  1954. if not (element.enabled) then
  1955. element.layout.alpha[1] = 136
  1956. element.eventresponder = nil
  1957. end
  1958. end
  1959. end
  1960.  
  1961.  
  1962. --
  1963. -- Element Rendering
  1964. --
  1965.  
  1966. function render_elements(master_ass)
  1967.  
  1968. for n=1, #elements do
  1969. local element = elements[n]
  1970.  
  1971. local style_ass = assdraw.ass_new()
  1972. style_ass:merge(element.style_ass)
  1973.  
  1974. --alpha
  1975. local ar = element.layout.alpha
  1976. if not (state.animation == nil) then
  1977. ar = {}
  1978. for ai, av in pairs(element.layout.alpha) do
  1979. ar[ai] = mult_alpha(av, state.animation)
  1980. end
  1981. end
  1982.  
  1983. style_ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
  1984. ar[1], ar[2], ar[3], ar[4]))
  1985.  
  1986. if element.eventresponder and (state.active_element == n) then
  1987.  
  1988. -- run render event functions
  1989. if not (element.eventresponder.render == nil) then
  1990. element.eventresponder.render(element)
  1991. end
  1992.  
  1993. if mouse_hit(element) then
  1994. -- mouse down styling
  1995. if (element.styledown) then
  1996. style_ass:append(osc_styles.elementDown)
  1997. end
  1998.  
  1999. if (element.softrepeat) and (state.mouse_down_counter >= 15
  2000. and state.mouse_down_counter % 5 == 0) then
  2001.  
  2002. element.eventresponder[state.active_event_source.."_down"](element)
  2003. end
  2004. state.mouse_down_counter = state.mouse_down_counter + 1
  2005. end
  2006.  
  2007. end
  2008.  
  2009. local elem_ass = assdraw.ass_new()
  2010.  
  2011. elem_ass:merge(style_ass)
  2012.  
  2013. if not (element.type == "button") then
  2014. elem_ass:merge(element.static_ass)
  2015. end
  2016.  
  2017. if (element.type == "slider") then
  2018.  
  2019. local slider_lo = element.layout.slider
  2020. local elem_geo = element.layout.geometry
  2021. local s_min = element.slider.min.value
  2022. local s_max = element.slider.max.value
  2023.  
  2024. -- draw pos marker
  2025. local pos = element.slider.posF()
  2026.  
  2027. if not (pos == nil) then
  2028.  
  2029. local foV = slider_lo.border + slider_lo.gap
  2030. local foH = 0
  2031. if (slider_lo.stype == "slider") or
  2032. (slider_lo.stype == "knob") then
  2033. foH = elem_geo.h / 2
  2034. elseif (slider_lo.stype == "bar") then
  2035. foH = slider_lo.border + slider_lo.gap
  2036. end
  2037.  
  2038. local xp = get_slider_ele_pos_for(element, pos)
  2039.  
  2040. -- the filling
  2041. local innerH = elem_geo.h - (2*foV)
  2042.  
  2043. if (slider_lo.stype == "bar") then
  2044. elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV)
  2045. elseif (slider_lo.stype == "slider") then
  2046. elem_ass:move_to(xp, foV)
  2047. elem_ass:line_to(xp+(innerH/2), (innerH/2)+foV)
  2048. elem_ass:line_to(xp, (innerH)+foV)
  2049. elem_ass:line_to(xp-(innerH/2), (innerH/2)+foV)
  2050. elseif (slider_lo.stype == "knob") then
  2051. elem_ass:rect_cw(xp, (9*innerH/20) + foV,
  2052. elem_geo.w - foH, (11*innerH/20) + foV)
  2053. elem_ass:rect_cw(foH, (3*innerH/8) + foV,
  2054. xp, (5*innerH/8) + foV)
  2055. elem_ass:round_rect_cw(xp - innerH/2, foV,
  2056. xp + innerH/2, foV + innerH, innerH/2.0)
  2057. end
  2058. end
  2059.  
  2060. -- seek ranges
  2061. local seekRanges = element.slider.seekRangesF()
  2062. if not (seekRanges == nil) then
  2063. for _,range in pairs(seekRanges) do
  2064. local pstart = get_slider_ele_pos_for(element, range["start"])
  2065. local pend = get_slider_ele_pos_for(element, range["end"])
  2066. elem_ass:rect_ccw(pstart, (elem_geo.h/2)-1, pend, (elem_geo.h/2) + 1)
  2067. end
  2068. end
  2069.  
  2070. elem_ass:draw_stop()
  2071.  
  2072. -- add tooltip
  2073. if not (element.slider.tooltipF == nil) then
  2074.  
  2075. if mouse_hit(element) then
  2076. local sliderpos = get_slider_value(element)
  2077. local tooltiplabel = element.slider.tooltipF(sliderpos)
  2078.  
  2079. local an = slider_lo.tooltip_an
  2080.  
  2081. local ty
  2082.  
  2083. if (an == 2) then
  2084. ty = element.hitbox.y1 - slider_lo.border
  2085. else
  2086. ty = element.hitbox.y1 + elem_geo.h/2
  2087. end
  2088.  
  2089. local tx = get_virt_mouse_pos()
  2090. if (slider_lo.adjust_tooltip) then
  2091. if (an == 2) then
  2092. if (sliderpos < (s_min + 3)) then
  2093. an = an - 1
  2094. elseif (sliderpos > (s_max - 3)) then
  2095. an = an + 1
  2096. end
  2097. elseif (sliderpos > (s_max-s_min)/2) then
  2098. an = an + 1
  2099. tx = tx - 5
  2100. else
  2101. an = an - 1
  2102. tx = tx + 10
  2103. end
  2104. end
  2105.  
  2106. -- tooltip label
  2107. elem_ass:new_event()
  2108. elem_ass:pos(tx, ty)
  2109. elem_ass:an(an)
  2110. elem_ass:append(slider_lo.tooltip_style)
  2111.  
  2112. --alpha
  2113. local ar = slider_lo.alpha
  2114. if not (state.animation == nil) then
  2115. ar = {}
  2116. for ai, av in pairs(slider_lo.alpha) do
  2117. ar[ai] = mult_alpha(av, state.animation)
  2118. end
  2119. end
  2120. elem_ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
  2121. ar[1], ar[2], ar[3], ar[4]))
  2122.  
  2123. elem_ass:append(tooltiplabel)
  2124.  
  2125. -- mpv_thumbnail_script.lua --
  2126. display_thumbnail({x=get_virt_mouse_pos(), y=ty, a=an}, sliderpos, elem_ass)
  2127. -- // mpv_thumbnail_script.lua // --
  2128.  
  2129. end
  2130. end
  2131.  
  2132. elseif (element.type == "button") then
  2133.  
  2134. local buttontext
  2135. if type(element.content) == "function" then
  2136. buttontext = element.content() -- function objects
  2137. elseif not (element.content == nil) then
  2138. buttontext = element.content -- text objects
  2139. end
  2140.  
  2141. local maxchars = element.layout.button.maxchars
  2142. if not (maxchars == nil) and (#buttontext > maxchars) then
  2143. local max_ratio = 1.25 -- up to 25% more chars while shrinking
  2144. local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
  2145. if (#buttontext > limit) then
  2146. while (#buttontext > limit) do
  2147. buttontext = buttontext:gsub(".[\128-\191]*$", "")
  2148. end
  2149. buttontext = buttontext .. "..."
  2150. end
  2151. local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
  2152. local stretch = (maxchars/#buttontext)*100
  2153. buttontext = string.format("{\\fscx%f}",
  2154. (maxchars/#buttontext)*100) .. buttontext
  2155. end
  2156.  
  2157. elem_ass:append(buttontext)
  2158. end
  2159.  
  2160. master_ass:merge(elem_ass)
  2161. end
  2162. end
  2163.  
  2164. --
  2165. -- Message display
  2166. --
  2167.  
  2168. -- pos is 1 based
  2169. function limited_list(prop, pos)
  2170. local proplist = mp.get_property_native(prop, {})
  2171. local count = #proplist
  2172. if count == 0 then
  2173. return count, proplist
  2174. end
  2175.  
  2176. local fs = tonumber(mp.get_property('options/osd-font-size'))
  2177. local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
  2178. if max % 2 == 0 then
  2179. max = max - 1
  2180. end
  2181. local delta = math.ceil(max / 2) - 1
  2182. local begi = math.max(math.min(pos - delta, count - max + 1), 1)
  2183. local endi = math.min(begi + max - 1, count)
  2184.  
  2185. local reslist = {}
  2186. for i=begi, endi do
  2187. local item = proplist[i]
  2188. item.current = (i == pos) and true or nil
  2189. table.insert(reslist, item)
  2190. end
  2191. return count, reslist
  2192. end
  2193.  
  2194. function get_playlist()
  2195. local pos = mp.get_property_number('playlist-pos', 0) + 1
  2196. local count, limlist = limited_list('playlist', pos)
  2197. if count == 0 then
  2198. return 'Empty playlist.'
  2199. end
  2200.  
  2201. local message = string.format('Playlist [%d/%d]:\n', pos, count)
  2202. for i, v in ipairs(limlist) do
  2203. local title = v.title
  2204. local _, filename = utils.split_path(v.filename)
  2205. if title == nil then
  2206. title = filename
  2207. end
  2208. message = string.format('%s %s %s\n', message,
  2209. (v.current and '●' or '○'), title)
  2210. end
  2211. return message
  2212. end
  2213.  
  2214. function get_chapterlist()
  2215. local pos = mp.get_property_number('chapter', 0) + 1
  2216. local count, limlist = limited_list('chapter-list', pos)
  2217. if count == 0 then
  2218. return 'No chapters.'
  2219. end
  2220.  
  2221. local message = string.format('Chapters [%d/%d]:\n', pos, count)
  2222. for i, v in ipairs(limlist) do
  2223. local time = mp.format_time(v.time)
  2224. local title = v.title
  2225. if title == nil then
  2226. title = string.format('Chapter %02d', i)
  2227. end
  2228. message = string.format('%s[%s] %s %s\n', message, time,
  2229. (v.current and '●' or '○'), title)
  2230. end
  2231. return message
  2232. end
  2233.  
  2234. function show_message(text, duration)
  2235.  
  2236. --print("text: "..text.." duration: " .. duration)
  2237. if duration == nil then
  2238. duration = tonumber(mp.get_property("options/osd-duration")) / 1000
  2239. elseif not type(duration) == "number" then
  2240. print("duration: " .. duration)
  2241. end
  2242.  
  2243. -- cut the text short, otherwise the following functions
  2244. -- may slow down massively on huge input
  2245. text = string.sub(text, 0, 4000)
  2246.  
  2247. -- replace actual linebreaks with ASS linebreaks
  2248. text = string.gsub(text, "\n", "\\N")
  2249.  
  2250. state.message_text = text
  2251. state.message_timeout = mp.get_time() + duration
  2252. end
  2253.  
  2254. function render_message(ass)
  2255. if not(state.message_timeout == nil) and not(state.message_text == nil)
  2256. and state.message_timeout > mp.get_time() then
  2257. local _, lines = string.gsub(state.message_text, "\\N", "")
  2258.  
  2259. local fontsize = tonumber(mp.get_property("options/osd-font-size"))
  2260. local outline = tonumber(mp.get_property("options/osd-border-size"))
  2261. local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
  2262. local counterscale = osc_param.playresy / osc_param.unscaled_y
  2263.  
  2264. fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
  2265. outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
  2266.  
  2267. local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
  2268.  
  2269.  
  2270. ass:new_event()
  2271. ass:append(style .. state.message_text)
  2272. else
  2273. state.message_text = nil
  2274. state.message_timeout = nil
  2275. end
  2276. end
  2277.  
  2278. --
  2279. -- Initialisation and Layout
  2280. --
  2281.  
  2282. function new_element(name, type)
  2283. elements[name] = {}
  2284. elements[name].type = type
  2285.  
  2286. -- add default stuff
  2287. elements[name].eventresponder = {}
  2288. elements[name].visible = true
  2289. elements[name].enabled = true
  2290. elements[name].softrepeat = false
  2291. elements[name].styledown = (type == "button")
  2292. elements[name].state = {}
  2293.  
  2294. if (type == "slider") then
  2295. elements[name].slider = {min = {value = 0}, max = {value = 100}}
  2296. end
  2297.  
  2298.  
  2299. return elements[name]
  2300. end
  2301.  
  2302. function add_layout(name)
  2303. if not (elements[name] == nil) then
  2304. -- new layout
  2305. elements[name].layout = {}
  2306.  
  2307. -- set layout defaults
  2308. elements[name].layout.layer = 50
  2309. elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
  2310.  
  2311. if (elements[name].type == "button") then
  2312. elements[name].layout.button = {
  2313. maxchars = nil,
  2314. }
  2315. elseif (elements[name].type == "slider") then
  2316. -- slider defaults
  2317. elements[name].layout.slider = {
  2318. border = 1,
  2319. gap = 1,
  2320. nibbles_top = true,
  2321. nibbles_bottom = true,
  2322. stype = "slider",
  2323. adjust_tooltip = true,
  2324. tooltip_style = "",
  2325. tooltip_an = 2,
  2326. alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
  2327. }
  2328. elseif (elements[name].type == "box") then
  2329. elements[name].layout.box = {radius = 0}
  2330. end
  2331.  
  2332. return elements[name].layout
  2333. else
  2334. msg.error("Can't add_layout to element \""..name.."\", doesn't exist.")
  2335. end
  2336. end
  2337.  
  2338. --
  2339. -- Layouts
  2340. --
  2341.  
  2342. local layouts = {}
  2343.  
  2344. -- Classic box layout
  2345. layouts["box"] = function ()
  2346.  
  2347. local osc_geo = {
  2348. w = 550, -- width
  2349. h = 138, -- height
  2350. r = 10, -- corner-radius
  2351. p = 15, -- padding
  2352. }
  2353.  
  2354. -- make sure the OSC actually fits into the video
  2355. if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then
  2356. osc_param.playresy = (osc_geo.w+(2*osc_geo.p))/osc_param.display_aspect
  2357. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  2358. end
  2359.  
  2360. -- position of the controller according to video aspect and valignment
  2361. local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
  2362. osc_geo.w, 0))
  2363. local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
  2364. osc_geo.h, 0))
  2365.  
  2366. -- position offset for contents aligned at the borders of the box
  2367. local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2
  2368. local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2
  2369.  
  2370. osc_param.areas = {} -- delete areas
  2371.  
  2372. -- area for active mouse input
  2373. add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
  2374.  
  2375. -- area for show/hide
  2376. local sh_area_y0, sh_area_y1
  2377. if user_opts.valign > 0 then
  2378. -- deadzone above OSC
  2379. sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
  2380. posY - (osc_geo.h / 2), 0, 0)
  2381. sh_area_y1 = osc_param.playresy
  2382. else
  2383. -- deadzone below OSC
  2384. sh_area_y0 = 0
  2385. sh_area_y1 = (posY + (osc_geo.h / 2)) +
  2386. get_align(1 - (2*user_opts.deadzonesize),
  2387. osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
  2388. end
  2389. add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  2390.  
  2391. -- fetch values
  2392. local osc_w, osc_h, osc_r, osc_p =
  2393. osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p
  2394.  
  2395. local lo
  2396.  
  2397. --
  2398. -- Background box
  2399. --
  2400.  
  2401. new_element("bgbox", "box")
  2402. lo = add_layout("bgbox")
  2403.  
  2404. lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
  2405. lo.layer = 10
  2406. lo.style = osc_styles.box
  2407. lo.alpha[1] = user_opts.boxalpha
  2408. lo.alpha[3] = user_opts.boxalpha
  2409. lo.box.radius = osc_r
  2410.  
  2411. --
  2412. -- Title row
  2413. --
  2414.  
  2415. local titlerowY = posY - pos_offsetY - 10
  2416.  
  2417. lo = add_layout("title")
  2418. lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
  2419. lo.style = osc_styles.vidtitle
  2420. lo.button.maxchars = user_opts.boxmaxchars
  2421.  
  2422. lo = add_layout("pl_prev")
  2423. lo.geometry =
  2424. {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12}
  2425. lo.style = osc_styles.topButtons
  2426.  
  2427. lo = add_layout("pl_next")
  2428. lo.geometry =
  2429. {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12}
  2430. lo.style = osc_styles.topButtons
  2431.  
  2432. --
  2433. -- Big buttons
  2434. --
  2435.  
  2436. local bigbtnrowY = posY - pos_offsetY + 35
  2437. local bigbtndist = 60
  2438.  
  2439. lo = add_layout("playpause")
  2440. lo.geometry =
  2441. {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
  2442. lo.style = osc_styles.bigButtons
  2443.  
  2444. lo = add_layout("skipback")
  2445. lo.geometry =
  2446. {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
  2447. lo.style = osc_styles.bigButtons
  2448.  
  2449. lo = add_layout("skipfrwd")
  2450. lo.geometry =
  2451. {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
  2452. lo.style = osc_styles.bigButtons
  2453.  
  2454. lo = add_layout("ch_prev")
  2455. lo.geometry =
  2456. {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
  2457. lo.style = osc_styles.bigButtons
  2458.  
  2459. lo = add_layout("ch_next")
  2460. lo.geometry =
  2461. {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
  2462. lo.style = osc_styles.bigButtons
  2463.  
  2464. lo = add_layout("cy_audio")
  2465. lo.geometry =
  2466. {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18}
  2467. lo.style = osc_styles.smallButtonsL
  2468.  
  2469. lo = add_layout("cy_sub")
  2470. lo.geometry =
  2471. {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18}
  2472. lo.style = osc_styles.smallButtonsL
  2473.  
  2474. lo = add_layout("tog_fs")
  2475. lo.geometry =
  2476. {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25}
  2477. lo.style = osc_styles.smallButtonsR
  2478.  
  2479. lo = add_layout("volume")
  2480. lo.geometry =
  2481. {x = posX+pos_offsetX - (25 * 2) - osc_geo.p,
  2482. y = bigbtnrowY, an = 4, w = 25, h = 25}
  2483. lo.style = osc_styles.smallButtonsR
  2484.  
  2485. --
  2486. -- Seekbar
  2487. --
  2488.  
  2489. lo = add_layout("seekbar")
  2490. lo.geometry =
  2491. {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15}
  2492. lo.style = osc_styles.timecodes
  2493. lo.slider.tooltip_style = osc_styles.vidtitle
  2494. lo.slider.stype = user_opts["seekbarstyle"]
  2495. if lo.slider.stype == "knob" then
  2496. lo.slider.border = 0
  2497. end
  2498.  
  2499. --
  2500. -- Timecodes + Cache
  2501. --
  2502.  
  2503. local bottomrowY = posY + pos_offsetY - 5
  2504.  
  2505. lo = add_layout("tc_left")
  2506. lo.geometry =
  2507. {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18}
  2508. lo.style = osc_styles.timecodes
  2509.  
  2510. lo = add_layout("tc_right")
  2511. lo.geometry =
  2512. {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18}
  2513. lo.style = osc_styles.timecodes
  2514.  
  2515. lo = add_layout("cache")
  2516. lo.geometry =
  2517. {x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
  2518. lo.style = osc_styles.timecodes
  2519.  
  2520. end
  2521.  
  2522. -- slim box layout
  2523. layouts["slimbox"] = function ()
  2524.  
  2525. local osc_geo = {
  2526. w = 660, -- width
  2527. h = 70, -- height
  2528. r = 10, -- corner-radius
  2529. }
  2530.  
  2531. -- make sure the OSC actually fits into the video
  2532. if (osc_param.playresx < (osc_geo.w)) then
  2533. osc_param.playresy = (osc_geo.w)/osc_param.display_aspect
  2534. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  2535. end
  2536.  
  2537. -- position of the controller according to video aspect and valignment
  2538. local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
  2539. osc_geo.w, 0))
  2540. local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
  2541. osc_geo.h, 0))
  2542.  
  2543. osc_param.areas = {} -- delete areas
  2544.  
  2545. -- area for active mouse input
  2546. add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
  2547.  
  2548. -- area for show/hide
  2549. local sh_area_y0, sh_area_y1
  2550. if user_opts.valign > 0 then
  2551. -- deadzone above OSC
  2552. sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
  2553. posY - (osc_geo.h / 2), 0, 0)
  2554. sh_area_y1 = osc_param.playresy
  2555. else
  2556. -- deadzone below OSC
  2557. sh_area_y0 = 0
  2558. sh_area_y1 = (posY + (osc_geo.h / 2)) +
  2559. get_align(1 - (2*user_opts.deadzonesize),
  2560. osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
  2561. end
  2562. add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  2563.  
  2564. local lo
  2565.  
  2566. local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100
  2567.  
  2568. -- styles
  2569. local styles = {
  2570. box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
  2571. timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
  2572. tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}",
  2573. }
  2574.  
  2575.  
  2576. new_element("bgbox", "box")
  2577. lo = add_layout("bgbox")
  2578.  
  2579. lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
  2580. lo.layer = 10
  2581. lo.style = osc_styles.box
  2582. lo.alpha[1] = user_opts.boxalpha
  2583. lo.alpha[3] = 0
  2584. if not (user_opts["seekbarstyle"] == "bar") then
  2585. lo.box.radius = osc_geo.r
  2586. end
  2587.  
  2588.  
  2589. lo = add_layout("seekbar")
  2590. lo.geometry =
  2591. {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
  2592. lo.style = osc_styles.timecodes
  2593. lo.slider.border = 0
  2594. lo.slider.gap = 1.5
  2595. lo.slider.tooltip_style = styles.tooltip
  2596. lo.slider.stype = user_opts["seekbarstyle"]
  2597. lo.slider.adjust_tooltip = false
  2598.  
  2599. --
  2600. -- Timecodes
  2601. --
  2602.  
  2603. lo = add_layout("tc_left")
  2604. lo.geometry =
  2605. {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1,
  2606. an = 7, w = tc_w, h = ele_h}
  2607. lo.style = styles.timecodes
  2608. lo.alpha[3] = user_opts.boxalpha
  2609.  
  2610. lo = add_layout("tc_right")
  2611. lo.geometry =
  2612. {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1,
  2613. an = 9, w = tc_w, h = ele_h}
  2614. lo.style = styles.timecodes
  2615. lo.alpha[3] = user_opts.boxalpha
  2616.  
  2617. -- Cache
  2618.  
  2619. lo = add_layout("cache")
  2620. lo.geometry =
  2621. {x = posX, y = posY + 1,
  2622. an = 8, w = tc_w, h = ele_h}
  2623. lo.style = styles.timecodes
  2624. lo.alpha[3] = user_opts.boxalpha
  2625.  
  2626.  
  2627. end
  2628.  
  2629. layouts["bottombar"] = function()
  2630. local osc_geo = {
  2631. x = -2,
  2632. y = osc_param.playresy - 54 - user_opts.barmargin,
  2633. an = 7,
  2634. w = osc_param.playresx + 4,
  2635. h = 56,
  2636. }
  2637.  
  2638. local padX = 9
  2639. local padY = 3
  2640. local buttonW = 27
  2641. local tcW = (state.tc_ms) and 170 or 110
  2642. local tsW = 90
  2643. local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
  2644.  
  2645. if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
  2646. osc_param.playresy = minW / osc_param.display_aspect
  2647. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  2648. osc_geo.y = osc_param.playresy - 54 - user_opts.barmargin
  2649. osc_geo.w = osc_param.playresx + 4
  2650. end
  2651.  
  2652. local line1 = osc_geo.y + 9 + padY
  2653. local line2 = osc_geo.y + 36 + padY
  2654.  
  2655. osc_param.areas = {}
  2656.  
  2657. add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
  2658. osc_geo.w, osc_geo.h))
  2659.  
  2660. local sh_area_y0, sh_area_y1
  2661. sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
  2662. osc_geo.y - (osc_geo.h / 2), 0, 0)
  2663. sh_area_y1 = osc_param.playresy - user_opts.barmargin
  2664. add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  2665.  
  2666. local lo, geo
  2667.  
  2668. -- Background bar
  2669. new_element("bgbox", "box")
  2670. lo = add_layout("bgbox")
  2671.  
  2672. lo.geometry = osc_geo
  2673. lo.layer = 10
  2674. lo.style = osc_styles.box
  2675. lo.alpha[1] = user_opts.boxalpha
  2676.  
  2677.  
  2678. -- Playlist prev/next
  2679. geo = { x = osc_geo.x + padX, y = line1,
  2680. an = 4, w = 18, h = 18 - padY }
  2681. lo = add_layout("pl_prev")
  2682. lo.geometry = geo
  2683. lo.style = osc_styles.topButtonsBar
  2684.  
  2685. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2686. lo = add_layout("pl_next")
  2687. lo.geometry = geo
  2688. lo.style = osc_styles.topButtonsBar
  2689.  
  2690. local t_l = geo.x + geo.w + padX
  2691.  
  2692. -- Cache
  2693. geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
  2694. an = 6, w = 150, h = geo.h }
  2695. lo = add_layout("cache")
  2696. lo.geometry = geo
  2697. lo.style = osc_styles.vidtitleBar
  2698.  
  2699. local t_r = geo.x - geo.w - padX*2
  2700.  
  2701. -- Title
  2702. geo = { x = t_l, y = geo.y, an = 4,
  2703. w = t_r - t_l, h = geo.h }
  2704. lo = add_layout("title")
  2705. lo.geometry = geo
  2706. lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
  2707. osc_styles.vidtitleBar,
  2708. geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
  2709.  
  2710.  
  2711. -- Playback control buttons
  2712. geo = { x = osc_geo.x + padX, y = line2, an = 4,
  2713. w = buttonW, h = 36 - padY*2}
  2714. lo = add_layout("playpause")
  2715. lo.geometry = geo
  2716. lo.style = osc_styles.smallButtonsBar
  2717.  
  2718. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2719. lo = add_layout("ch_prev")
  2720. lo.geometry = geo
  2721. lo.style = osc_styles.smallButtonsBar
  2722.  
  2723. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2724. lo = add_layout("ch_next")
  2725. lo.geometry = geo
  2726. lo.style = osc_styles.smallButtonsBar
  2727.  
  2728. -- Left timecode
  2729. geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
  2730. w = tcW, h = geo.h }
  2731. lo = add_layout("tc_left")
  2732. lo.geometry = geo
  2733. lo.style = osc_styles.timecodesBar
  2734.  
  2735. local sb_l = geo.x + padX
  2736.  
  2737. -- Fullscreen button
  2738. geo = { x = osc_geo.x + osc_geo.w - buttonW - padX, y = geo.y, an = 4,
  2739. w = buttonW, h = geo.h }
  2740. lo = add_layout("tog_fs")
  2741. lo.geometry = geo
  2742. lo.style = osc_styles.smallButtonsBar
  2743.  
  2744. -- Volume
  2745. geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2746. lo = add_layout("volume")
  2747. lo.geometry = geo
  2748. lo.style = osc_styles.smallButtonsBar
  2749.  
  2750. -- Track selection buttons
  2751. geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
  2752. lo = add_layout("cy_sub")
  2753. lo.geometry = geo
  2754. lo.style = osc_styles.smallButtonsBar
  2755.  
  2756. geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2757. lo = add_layout("cy_audio")
  2758. lo.geometry = geo
  2759. lo.style = osc_styles.smallButtonsBar
  2760.  
  2761.  
  2762. -- Right timecode
  2763. geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an,
  2764. w = tcW, h = geo.h }
  2765. lo = add_layout("tc_right")
  2766. lo.geometry = geo
  2767. lo.style = osc_styles.timecodesBar
  2768.  
  2769. local sb_r = geo.x - padX
  2770.  
  2771.  
  2772. -- Seekbar
  2773. geo = { x = sb_l, y = geo.y, an = geo.an,
  2774. w = math.max(0, sb_r - sb_l), h = geo.h }
  2775. new_element("bgbar1", "box")
  2776. lo = add_layout("bgbar1")
  2777.  
  2778. lo.geometry = geo
  2779. lo.layer = 15
  2780. lo.style = osc_styles.timecodesBar
  2781. lo.alpha[1] =
  2782. math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
  2783.  
  2784. lo = add_layout("seekbar")
  2785. lo.geometry = geo
  2786. lo.style = osc_styles.timecodes
  2787. lo.slider.border = 0
  2788. lo.slider.gap = 2
  2789. lo.slider.tooltip_style = osc_styles.timePosBar
  2790. lo.slider.tooltip_an = 5
  2791. lo.slider.stype = user_opts["seekbarstyle"]
  2792. end
  2793.  
  2794. layouts["topbar"] = function()
  2795. local osc_geo = {
  2796. x = -2,
  2797. y = 54 + user_opts.barmargin,
  2798. an = 1,
  2799. w = osc_param.playresx + 4,
  2800. h = 56,
  2801. }
  2802.  
  2803. local padX = 9
  2804. local padY = 3
  2805. local buttonW = 27
  2806. local tcW = (state.tc_ms) and 170 or 110
  2807. local tsW = 90
  2808. local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
  2809.  
  2810. if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
  2811. osc_param.playresy = minW / osc_param.display_aspect
  2812. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  2813. osc_geo.y = 54 + user_opts.barmargin
  2814. osc_geo.w = osc_param.playresx + 4
  2815. end
  2816.  
  2817. local line1 = osc_geo.y - 36 - padY
  2818. local line2 = osc_geo.y - 9 - padY
  2819.  
  2820. osc_param.areas = {}
  2821.  
  2822. add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
  2823. osc_geo.w, osc_geo.h))
  2824.  
  2825. local sh_area_y0, sh_area_y1
  2826. sh_area_y0 = user_opts.barmargin
  2827. sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) +
  2828. get_align(1 - (2*user_opts.deadzonesize),
  2829. osc_param.playresy - (osc_geo.y + (osc_geo.h / 2)), 0, 0)
  2830. add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  2831.  
  2832. local lo, geo
  2833.  
  2834. -- Background bar
  2835. new_element("bgbox", "box")
  2836. lo = add_layout("bgbox")
  2837.  
  2838. lo.geometry = osc_geo
  2839. lo.layer = 10
  2840. lo.style = osc_styles.box
  2841. lo.alpha[1] = user_opts.boxalpha
  2842.  
  2843.  
  2844. -- Playback control buttons
  2845. geo = { x = osc_geo.x + padX, y = line1, an = 4,
  2846. w = buttonW, h = 36 - padY*2 }
  2847. lo = add_layout("playpause")
  2848. lo.geometry = geo
  2849. lo.style = osc_styles.smallButtonsBar
  2850.  
  2851. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2852. lo = add_layout("ch_prev")
  2853. lo.geometry = geo
  2854. lo.style = osc_styles.smallButtonsBar
  2855.  
  2856. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2857. lo = add_layout("ch_next")
  2858. lo.geometry = geo
  2859. lo.style = osc_styles.smallButtonsBar
  2860.  
  2861.  
  2862. -- Left timecode
  2863. geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
  2864. w = tcW, h = geo.h }
  2865. lo = add_layout("tc_left")
  2866. lo.geometry = geo
  2867. lo.style = osc_styles.timecodesBar
  2868.  
  2869. local sb_l = geo.x + padX
  2870.  
  2871. -- Fullscreen button
  2872. geo = { x = osc_geo.x + osc_geo.w - buttonW - padX, y = geo.y, an = 4,
  2873. w = buttonW, h = geo.h }
  2874. lo = add_layout("tog_fs")
  2875. lo.geometry = geo
  2876. lo.style = osc_styles.smallButtonsBar
  2877.  
  2878. -- Volume
  2879. geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2880. lo = add_layout("volume")
  2881. lo.geometry = geo
  2882. lo.style = osc_styles.smallButtonsBar
  2883.  
  2884. -- Track selection buttons
  2885. geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
  2886. lo = add_layout("cy_sub")
  2887. lo.geometry = geo
  2888. lo.style = osc_styles.smallButtonsBar
  2889.  
  2890. geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2891. lo = add_layout("cy_audio")
  2892. lo.geometry = geo
  2893. lo.style = osc_styles.smallButtonsBar
  2894.  
  2895.  
  2896. -- Right timecode
  2897. geo = { x = geo.x - geo.w - padX - tcW - 10, y = geo.y, an = 4,
  2898. w = tcW, h = geo.h }
  2899. lo = add_layout("tc_right")
  2900. lo.geometry = geo
  2901. lo.style = osc_styles.timecodesBar
  2902.  
  2903. local sb_r = geo.x - padX
  2904.  
  2905.  
  2906. -- Seekbar
  2907. geo = { x = sb_l, y = user_opts.barmargin, an = 7,
  2908. w = math.max(0, sb_r - sb_l), h = geo.h }
  2909. new_element("bgbar1", "box")
  2910. lo = add_layout("bgbar1")
  2911.  
  2912. lo.geometry = geo
  2913. lo.layer = 15
  2914. lo.style = osc_styles.timecodesBar
  2915. lo.alpha[1] =
  2916. math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
  2917.  
  2918. lo = add_layout("seekbar")
  2919. lo.geometry = geo
  2920. lo.style = osc_styles.timecodesBar
  2921. lo.slider.border = 0
  2922. lo.slider.gap = 2
  2923. lo.slider.tooltip_style = osc_styles.timePosBar
  2924. lo.slider.stype = user_opts["seekbarstyle"]
  2925. lo.slider.tooltip_an = 5
  2926.  
  2927.  
  2928. -- Playlist prev/next
  2929. geo = { x = osc_geo.x + padX, y = line2, an = 4, w = 18, h = 18 - padY }
  2930. lo = add_layout("pl_prev")
  2931. lo.geometry = geo
  2932. lo.style = osc_styles.topButtonsBar
  2933.  
  2934. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  2935. lo = add_layout("pl_next")
  2936. lo.geometry = geo
  2937. lo.style = osc_styles.topButtonsBar
  2938.  
  2939. local t_l = geo.x + geo.w + padX
  2940.  
  2941. -- Cache
  2942. geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
  2943. an = 6, w = 150, h = geo.h }
  2944. lo = add_layout("cache")
  2945. lo.geometry = geo
  2946. lo.style = osc_styles.vidtitleBar
  2947.  
  2948. local t_r = geo.x - geo.w - padX*2
  2949.  
  2950. -- Title
  2951. geo = { x = t_l, y = geo.y, an = 4,
  2952. w = t_r - t_l, h = geo.h }
  2953. lo = add_layout("title")
  2954. lo.geometry = geo
  2955. lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
  2956. osc_styles.vidtitleBar,
  2957. geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
  2958. end
  2959.  
  2960. -- Validate string type user options
  2961. function validate_user_opts()
  2962. if layouts[user_opts.layout] == nil then
  2963. msg.warn("Invalid setting \""..user_opts.layout.."\" for layout")
  2964. user_opts.layout = "box"
  2965. end
  2966.  
  2967. if user_opts.seekbarstyle ~= "slider" and
  2968. user_opts.seekbarstyle ~= "bar" and
  2969. user_opts.seekbarstyle ~= "knob" then
  2970. msg.warn("Invalid setting \"" .. user_opts.seekbarstyle
  2971. .. "\" for seekbarstyle")
  2972. user_opts.seekbarstyle = "slider"
  2973. end
  2974. end
  2975.  
  2976.  
  2977. -- OSC INIT
  2978. function osc_init()
  2979. msg.debug("osc_init")
  2980.  
  2981. -- set canvas resolution according to display aspect and scaling setting
  2982. local baseResY = 720
  2983. local display_w, display_h, display_aspect = mp.get_osd_size()
  2984. local scale = 1
  2985.  
  2986. if (mp.get_property("video") == "no") then -- dummy/forced window
  2987. scale = user_opts.scaleforcedwindow
  2988. elseif state.fullscreen then
  2989. scale = user_opts.scalefullscreen
  2990. else
  2991. scale = user_opts.scalewindowed
  2992. end
  2993.  
  2994. if user_opts.vidscale then
  2995. osc_param.unscaled_y = baseResY
  2996. else
  2997. osc_param.unscaled_y = display_h
  2998. end
  2999. osc_param.playresy = osc_param.unscaled_y / scale
  3000. if (display_aspect > 0) then
  3001. osc_param.display_aspect = display_aspect
  3002. end
  3003. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  3004.  
  3005.  
  3006.  
  3007.  
  3008.  
  3009. elements = {}
  3010.  
  3011. -- some often needed stuff
  3012. local pl_count = mp.get_property_number("playlist-count", 0)
  3013. local have_pl = (pl_count > 1)
  3014. local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
  3015. local have_ch = (mp.get_property_number("chapters", 0) > 0)
  3016. local loop = mp.get_property("loop-playlist", "no")
  3017.  
  3018. local ne
  3019.  
  3020. -- title
  3021. ne = new_element("title", "button")
  3022.  
  3023. ne.content = function ()
  3024. local title = mp.command_native({"expand-text", user_opts.title})
  3025. -- escape ASS, and strip newlines and trailing slashes
  3026. title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
  3027. return not (title == "") and title or "mpv"
  3028. end
  3029.  
  3030. ne.eventresponder["mbtn_left_up"] = function ()
  3031. local title = mp.get_property_osd("media-title")
  3032. if (have_pl) then
  3033. title = string.format("[%d/%d] %s", countone(pl_pos - 1),
  3034. pl_count, title)
  3035. end
  3036. show_message(title)
  3037. end
  3038.  
  3039. ne.eventresponder["mbtn_right_up"] =
  3040. function () show_message(mp.get_property_osd("filename")) end
  3041.  
  3042. -- playlist buttons
  3043.  
  3044. -- prev
  3045. ne = new_element("pl_prev", "button")
  3046.  
  3047. ne.content = "\238\132\144"
  3048. ne.enabled = (pl_pos > 1) or (loop ~= "no")
  3049. ne.eventresponder["mbtn_left_up"] =
  3050. function ()
  3051. mp.commandv("playlist-prev", "weak")
  3052. show_message(get_playlist(), 3)
  3053. end
  3054. ne.eventresponder["shift+mbtn_left_up"] =
  3055. function () show_message(get_playlist(), 3) end
  3056. ne.eventresponder["mbtn_right_up"] =
  3057. function () show_message(get_playlist(), 3) end
  3058.  
  3059. --next
  3060. ne = new_element("pl_next", "button")
  3061.  
  3062. ne.content = "\238\132\129"
  3063. ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
  3064. ne.eventresponder["mbtn_left_up"] =
  3065. function ()
  3066. mp.commandv("playlist-next", "weak")
  3067. show_message(get_playlist(), 3)
  3068. end
  3069. ne.eventresponder["shift+mbtn_left_up"] =
  3070. function () show_message(get_playlist(), 3) end
  3071. ne.eventresponder["mbtn_right_up"] =
  3072. function () show_message(get_playlist(), 3) end
  3073.  
  3074.  
  3075. -- big buttons
  3076.  
  3077. --playpause
  3078. ne = new_element("playpause", "button")
  3079.  
  3080. ne.content = function ()
  3081. if mp.get_property("pause") == "yes" then
  3082. return ("\238\132\129")
  3083. else
  3084. return ("\238\128\130")
  3085. end
  3086. end
  3087. ne.eventresponder["mbtn_left_up"] =
  3088. function () mp.commandv("cycle", "pause") end
  3089.  
  3090. --skipback
  3091. ne = new_element("skipback", "button")
  3092.  
  3093. ne.softrepeat = true
  3094. ne.content = "\238\128\132"
  3095. ne.eventresponder["mbtn_left_down"] =
  3096. function () mp.commandv("seek", -5, "relative", "keyframes") end
  3097. ne.eventresponder["shift+mbtn_left_down"] =
  3098. function () mp.commandv("frame-back-step") end
  3099. ne.eventresponder["mbtn_right_down"] =
  3100. function () mp.commandv("seek", -30, "relative", "keyframes") end
  3101.  
  3102. --skipfrwd
  3103. ne = new_element("skipfrwd", "button")
  3104.  
  3105. ne.softrepeat = true
  3106. ne.content = "\238\128\133"
  3107. ne.eventresponder["mbtn_left_down"] =
  3108. function () mp.commandv("seek", 10, "relative", "keyframes") end
  3109. ne.eventresponder["shift+mbtn_left_down"] =
  3110. function () mp.commandv("frame-step") end
  3111. ne.eventresponder["mbtn_right_down"] =
  3112. function () mp.commandv("seek", 60, "relative", "keyframes") end
  3113.  
  3114. --ch_prev
  3115. ne = new_element("ch_prev", "button")
  3116.  
  3117. ne.enabled = have_ch
  3118. ne.content = "\238\132\132"
  3119. ne.eventresponder["mbtn_left_up"] =
  3120. function ()
  3121. mp.commandv("add", "chapter", -1)
  3122. show_message(get_chapterlist(), 3)
  3123. end
  3124. ne.eventresponder["shift+mbtn_left_up"] =
  3125. function () show_message(get_chapterlist(), 3) end
  3126. ne.eventresponder["mbtn_right_up"] =
  3127. function () show_message(get_chapterlist(), 3) end
  3128.  
  3129. --ch_next
  3130. ne = new_element("ch_next", "button")
  3131.  
  3132. ne.enabled = have_ch
  3133. ne.content = "\238\132\133"
  3134. ne.eventresponder["mbtn_left_up"] =
  3135. function ()
  3136. mp.commandv("add", "chapter", 1)
  3137. show_message(get_chapterlist(), 3)
  3138. end
  3139. ne.eventresponder["shift+mbtn_left_up"] =
  3140. function () show_message(get_chapterlist(), 3) end
  3141. ne.eventresponder["mbtn_right_up"] =
  3142. function () show_message(get_chapterlist(), 3) end
  3143.  
  3144. --
  3145. update_tracklist()
  3146.  
  3147. --cy_audio
  3148. ne = new_element("cy_audio", "button")
  3149.  
  3150. ne.enabled = (#tracks_osc.audio > 0)
  3151. ne.content = function ()
  3152. local aid = "–"
  3153. if not (get_track("audio") == 0) then
  3154. aid = get_track("audio")
  3155. end
  3156. return ("\238\132\134" .. osc_styles.smallButtonsLlabel
  3157. .. " " .. aid .. "/" .. #tracks_osc.audio)
  3158. end
  3159. ne.eventresponder["mbtn_left_up"] =
  3160. function () set_track("audio", 1) end
  3161. ne.eventresponder["mbtn_right_up"] =
  3162. function () set_track("audio", -1) end
  3163. ne.eventresponder["shift+mbtn_left_down"] =
  3164. function () show_message(get_tracklist("audio"), 2) end
  3165.  
  3166. --cy_sub
  3167. ne = new_element("cy_sub", "button")
  3168.  
  3169. ne.enabled = (#tracks_osc.sub > 0)
  3170. ne.content = function ()
  3171. local sid = "–"
  3172. if not (get_track("sub") == 0) then
  3173. sid = get_track("sub")
  3174. end
  3175. return ("\238\132\135" .. osc_styles.smallButtonsLlabel
  3176. .. " " .. sid .. "/" .. #tracks_osc.sub)
  3177. end
  3178. ne.eventresponder["mbtn_left_up"] =
  3179. function () set_track("sub", 1) end
  3180. ne.eventresponder["mbtn_right_up"] =
  3181. function () set_track("sub", -1) end
  3182. ne.eventresponder["shift+mbtn_left_down"] =
  3183. function () show_message(get_tracklist("sub"), 2) end
  3184.  
  3185. --tog_fs
  3186. ne = new_element("tog_fs", "button")
  3187. ne.content = function ()
  3188. if (state.fullscreen) then
  3189. return ("\238\132\137")
  3190. else
  3191. return ("\238\132\136")
  3192. end
  3193. end
  3194. ne.eventresponder["mbtn_left_up"] =
  3195. function () mp.commandv("cycle", "fullscreen") end
  3196.  
  3197.  
  3198. --seekbar
  3199. ne = new_element("seekbar", "slider")
  3200.  
  3201. ne.enabled = not (mp.get_property("percent-pos") == nil)
  3202. ne.slider.markerF = function ()
  3203. local duration = mp.get_property_number("duration", nil)
  3204. if not (duration == nil) then
  3205. local chapters = mp.get_property_native("chapter-list", {})
  3206. local markers = {}
  3207. for n = 1, #chapters do
  3208. markers[n] = (chapters[n].time / duration * 100)
  3209. end
  3210. return markers
  3211. else
  3212. return {}
  3213. end
  3214. end
  3215. ne.slider.posF =
  3216. function () return mp.get_property_number("percent-pos", nil) end
  3217. ne.slider.tooltipF = function (pos)
  3218. local duration = mp.get_property_number("duration", nil)
  3219. if not ((duration == nil) or (pos == nil)) then
  3220. possec = duration * (pos / 100)
  3221. return mp.format_time(possec)
  3222. else
  3223. return ""
  3224. end
  3225. end
  3226. ne.slider.seekRangesF = function()
  3227. if not (user_opts.seekranges) then
  3228. return nil
  3229. end
  3230. local cache_state = mp.get_property_native("demuxer-cache-state", nil)
  3231. if not cache_state then
  3232. return nil
  3233. end
  3234. local duration = mp.get_property_number("duration", nil)
  3235. if (duration == nil) or duration <= 0 then
  3236. return nil
  3237. end
  3238. local ranges = cache_state["seekable-ranges"]
  3239. for _, range in pairs(ranges) do
  3240. range["start"] = 100 * range["start"] / duration
  3241. range["end"] = 100 * range["end"] / duration
  3242. end
  3243. return ranges
  3244. end
  3245. ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
  3246. function (element)
  3247. -- mouse move events may pile up during seeking and may still get
  3248. -- sent when the user is done seeking, so we need to throw away
  3249. -- identical seeks
  3250. local seekto = get_slider_value(element)
  3251. if (element.state.lastseek == nil) or
  3252. (not (element.state.lastseek == seekto)) then
  3253. mp.commandv("seek", seekto,
  3254. "absolute-percent", "keyframes")
  3255. element.state.lastseek = seekto
  3256. end
  3257.  
  3258. end
  3259. ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks
  3260. function (element) mp.commandv("seek", get_slider_value(element),
  3261. "absolute-percent", "exact") end
  3262. ne.eventresponder["reset"] =
  3263. function (element) element.state.lastseek = nil end
  3264.  
  3265.  
  3266. -- tc_left (current pos)
  3267. ne = new_element("tc_left", "button")
  3268.  
  3269. ne.content = function ()
  3270. if (state.tc_ms) then
  3271. return (mp.get_property_osd("playback-time/full"))
  3272. else
  3273. return (mp.get_property_osd("playback-time"))
  3274. end
  3275. end
  3276. ne.eventresponder["mbtn_left_up"] = function ()
  3277. state.tc_ms = not state.tc_ms
  3278. request_init()
  3279. end
  3280.  
  3281. -- tc_right (total/remaining time)
  3282. ne = new_element("tc_right", "button")
  3283.  
  3284. ne.visible = (mp.get_property_number("duration", 0) > 0)
  3285. ne.content = function ()
  3286. if (state.rightTC_trem) then
  3287. if state.tc_ms then
  3288. return ("-"..mp.get_property_osd("playtime-remaining/full"))
  3289. else
  3290. return ("-"..mp.get_property_osd("playtime-remaining"))
  3291. end
  3292. else
  3293. if state.tc_ms then
  3294. return (mp.get_property_osd("duration/full"))
  3295. else
  3296. return (mp.get_property_osd("duration"))
  3297. end
  3298. end
  3299. end
  3300. ne.eventresponder["mbtn_left_up"] =
  3301. function () state.rightTC_trem = not state.rightTC_trem end
  3302.  
  3303. -- cache
  3304. ne = new_element("cache", "button")
  3305.  
  3306. ne.content = function ()
  3307. local dmx_cache = mp.get_property_number("demuxer-cache-duration")
  3308. local cache_used = mp.get_property_number("cache-used")
  3309. local dmx_cache_state = mp.get_property_native("demuxer-cache-state", {})
  3310. local is_network = mp.get_property_native("demuxer-via-network")
  3311. local show_cache = cache_used and not dmx_cache_state["eof"]
  3312. if dmx_cache then
  3313. dmx_cache = string.format("%3.0fs", dmx_cache)
  3314. end
  3315. if dmx_cache_state["fw-bytes"] then
  3316. cache_used = (cache_used or 0)*1024 + dmx_cache_state["fw-bytes"]
  3317. end
  3318. if (is_network and dmx_cache) or show_cache then
  3319. -- Only show dmx-cache-duration by itself if it's a network file.
  3320. -- Cache can be forced even for local files, so always show that.
  3321. return string.format("Cache: %s%s%s",
  3322. (dmx_cache and dmx_cache or ""),
  3323. ((dmx_cache and show_cache) and " | " or ""),
  3324. (show_cache and
  3325. utils.format_bytes_humanized(cache_used) or ""))
  3326. else
  3327. return ""
  3328. end
  3329. end
  3330.  
  3331. -- volume
  3332. ne = new_element("volume", "button")
  3333.  
  3334. ne.content = function()
  3335. local volume = mp.get_property_number("volume", 0)
  3336. local mute = mp.get_property_native("mute")
  3337. local volicon = {"\238\132\139", "\238\132\140",
  3338. "\238\132\141", "\238\132\142"}
  3339. if volume == 0 or mute then
  3340. return "\238\132\138"
  3341. else
  3342. return volicon[math.min(4,math.ceil(volume / (100/3)))]
  3343. end
  3344. end
  3345. ne.eventresponder["mbtn_left_up"] =
  3346. function () mp.commandv("cycle", "mute") end
  3347.  
  3348. ne.eventresponder["wheel_up_press"] =
  3349. function () mp.commandv("osd-auto", "add", "volume", 5) end
  3350. ne.eventresponder["wheel_down_press"] =
  3351. function () mp.commandv("osd-auto", "add", "volume", -5) end
  3352.  
  3353.  
  3354. -- load layout
  3355. layouts[user_opts.layout]()
  3356.  
  3357. --do something with the elements
  3358. prepare_elements()
  3359.  
  3360. end
  3361.  
  3362.  
  3363.  
  3364. --
  3365. -- Other important stuff
  3366. --
  3367.  
  3368.  
  3369. function show_osc()
  3370. -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
  3371. if not state.enabled then return end
  3372.  
  3373. msg.trace("show_osc")
  3374. --remember last time of invocation (mouse move)
  3375. state.showtime = mp.get_time()
  3376.  
  3377. osc_visible(true)
  3378.  
  3379. if (user_opts.fadeduration > 0) then
  3380. state.anitype = nil
  3381. end
  3382. end
  3383.  
  3384. function hide_osc()
  3385. msg.trace("hide_osc")
  3386. if not state.enabled then
  3387. -- typically hide happens at render() from tick(), but now tick() is
  3388. -- no-op and won't render again to remove the osc, so do that manually.
  3389. state.osc_visible = false
  3390. timer_stop()
  3391. render_wipe()
  3392. elseif (user_opts.fadeduration > 0) then
  3393. if not(state.osc_visible == false) then
  3394. state.anitype = "out"
  3395. control_timer()
  3396. end
  3397. else
  3398. osc_visible(false)
  3399. end
  3400. end
  3401.  
  3402. function osc_visible(visible)
  3403. state.osc_visible = visible
  3404. control_timer()
  3405. end
  3406.  
  3407. function pause_state(name, enabled)
  3408. state.paused = enabled
  3409. control_timer()
  3410. end
  3411.  
  3412. function cache_state(name, idle)
  3413. state.cache_idle = idle
  3414. control_timer()
  3415. end
  3416.  
  3417. function control_timer()
  3418. if (state.paused) and (state.osc_visible) and
  3419. ( not(state.cache_idle) or not (state.anitype == nil) ) then
  3420.  
  3421. timer_start()
  3422. else
  3423. timer_stop()
  3424. end
  3425. end
  3426.  
  3427. function timer_start()
  3428. if not (state.timer_active) then
  3429. msg.trace("timer start")
  3430.  
  3431. if (state.timer == nil) then
  3432. -- create new timer
  3433. state.timer = mp.add_periodic_timer(0.03, tick)
  3434. else
  3435. -- resume existing one
  3436. state.timer:resume()
  3437. end
  3438.  
  3439. state.timer_active = true
  3440. end
  3441. end
  3442.  
  3443. function timer_stop()
  3444. if (state.timer_active) then
  3445. msg.trace("timer stop")
  3446.  
  3447. if not (state.timer == nil) then
  3448. -- kill timer
  3449. state.timer:kill()
  3450. end
  3451.  
  3452. state.timer_active = false
  3453. end
  3454. end
  3455.  
  3456.  
  3457.  
  3458. function mouse_leave()
  3459. if user_opts.hidetimeout >= 0 then
  3460. hide_osc()
  3461. end
  3462. -- reset mouse position
  3463. state.last_mouseX, state.last_mouseY = nil, nil
  3464. end
  3465.  
  3466. function request_init()
  3467. state.initREQ = true
  3468. end
  3469.  
  3470. function render_wipe()
  3471. msg.trace("render_wipe()")
  3472. mp.set_osd_ass(0, 0, "{}")
  3473. end
  3474.  
  3475. function render()
  3476. msg.trace("rendering")
  3477. local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
  3478. local mouseX, mouseY = get_virt_mouse_pos()
  3479. local now = mp.get_time()
  3480.  
  3481. -- check if display changed, if so request reinit
  3482. if not (state.mp_screen_sizeX == current_screen_sizeX
  3483. and state.mp_screen_sizeY == current_screen_sizeY) then
  3484.  
  3485. request_init()
  3486.  
  3487. state.mp_screen_sizeX = current_screen_sizeX
  3488. state.mp_screen_sizeY = current_screen_sizeY
  3489. end
  3490.  
  3491. -- init management
  3492. if state.initREQ then
  3493. osc_init()
  3494. state.initREQ = false
  3495.  
  3496. -- store initial mouse position
  3497. if (state.last_mouseX == nil or state.last_mouseY == nil)
  3498. and not (mouseX == nil or mouseY == nil) then
  3499.  
  3500. state.last_mouseX, state.last_mouseY = mouseX, mouseY
  3501. end
  3502. end
  3503.  
  3504.  
  3505. -- fade animation
  3506. if not(state.anitype == nil) then
  3507.  
  3508. if (state.anistart == nil) then
  3509. state.anistart = now
  3510. end
  3511.  
  3512. if (now < state.anistart + (user_opts.fadeduration/1000)) then
  3513.  
  3514. if (state.anitype == "in") then --fade in
  3515. osc_visible(true)
  3516. state.animation = scale_value(state.anistart,
  3517. (state.anistart + (user_opts.fadeduration/1000)),
  3518. 255, 0, now)
  3519. elseif (state.anitype == "out") then --fade out
  3520. state.animation = scale_value(state.anistart,
  3521. (state.anistart + (user_opts.fadeduration/1000)),
  3522. 0, 255, now)
  3523. end
  3524.  
  3525. else
  3526. if (state.anitype == "out") then
  3527. osc_visible(false)
  3528. end
  3529. state.anistart = nil
  3530. state.animation = nil
  3531. state.anitype = nil
  3532. end
  3533. else
  3534. state.anistart = nil
  3535. state.animation = nil
  3536. state.anitype = nil
  3537. end
  3538.  
  3539. --mouse show/hide area
  3540. for k,cords in pairs(osc_param.areas["showhide"]) do
  3541. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
  3542. end
  3543. do_enable_keybindings()
  3544.  
  3545. --mouse input area
  3546. local mouse_over_osc = false
  3547.  
  3548. for _,cords in ipairs(osc_param.areas["input"]) do
  3549. if state.osc_visible then -- activate only when OSC is actually visible
  3550. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
  3551. end
  3552. if state.osc_visible ~= state.input_enabled then
  3553. if state.osc_visible then
  3554. mp.enable_key_bindings("input")
  3555. else
  3556. mp.disable_key_bindings("input")
  3557. end
  3558. state.input_enabled = state.osc_visible
  3559. end
  3560.  
  3561. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  3562. mouse_over_osc = true
  3563. end
  3564. end
  3565.  
  3566. -- autohide
  3567. if not (state.showtime == nil) and (user_opts.hidetimeout >= 0)
  3568. and (state.showtime + (user_opts.hidetimeout/1000) < now)
  3569. and (state.active_element == nil) and not (mouse_over_osc) then
  3570.  
  3571. hide_osc()
  3572. end
  3573.  
  3574.  
  3575. -- actual rendering
  3576. local ass = assdraw.ass_new()
  3577.  
  3578. -- Messages
  3579. render_message(ass)
  3580.  
  3581. -- mpv_thumbnail_script.lua --
  3582. local thumb_was_visible = osc_thumb_state.visible
  3583. osc_thumb_state.visible = false
  3584. -- // mpv_thumbnail_script.lua // --
  3585.  
  3586. -- actual OSC
  3587. if state.osc_visible then
  3588. render_elements(ass)
  3589. end
  3590.  
  3591. -- mpv_thumbnail_script.lua --
  3592. if not osc_thumb_state.visible and thumb_was_visible then
  3593. hide_thumbnail()
  3594. end
  3595. -- // mpv_thumbnail_script.lua // --
  3596.  
  3597. -- submit
  3598. mp.set_osd_ass(osc_param.playresy * osc_param.display_aspect,
  3599. osc_param.playresy, ass.text)
  3600.  
  3601.  
  3602.  
  3603.  
  3604. end
  3605.  
  3606. --
  3607. -- Eventhandling
  3608. --
  3609.  
  3610. local function element_has_action(element, action)
  3611. return element and element.eventresponder and
  3612. element.eventresponder[action]
  3613. end
  3614.  
  3615. function process_event(source, what)
  3616. local action = string.format("%s%s", source,
  3617. what and ("_" .. what) or "")
  3618.  
  3619. if what == "down" or what == "press" then
  3620.  
  3621. for n = 1, #elements do
  3622.  
  3623. if mouse_hit(elements[n]) and
  3624. elements[n].eventresponder and
  3625. (elements[n].eventresponder[source .. "_up"] or
  3626. elements[n].eventresponder[action]) then
  3627.  
  3628. if what == "down" then
  3629. state.active_element = n
  3630. state.active_event_source = source
  3631. end
  3632. -- fire the down or press event if the element has one
  3633. if element_has_action(elements[n], action) then
  3634. elements[n].eventresponder[action](elements[n])
  3635. end
  3636.  
  3637. end
  3638. end
  3639.  
  3640. elseif what == "up" then
  3641.  
  3642. if elements[state.active_element] then
  3643. local n = state.active_element
  3644.  
  3645. if n == 0 then
  3646. --click on background (does not work)
  3647. elseif element_has_action(elements[n], action) and
  3648. mouse_hit(elements[n]) then
  3649.  
  3650. elements[n].eventresponder[action](elements[n])
  3651. end
  3652.  
  3653. --reset active element
  3654. if element_has_action(elements[n], "reset") then
  3655. elements[n].eventresponder["reset"](elements[n])
  3656. end
  3657.  
  3658. end
  3659. state.active_element = nil
  3660. state.mouse_down_counter = 0
  3661.  
  3662. elseif source == "mouse_move" then
  3663.  
  3664. local mouseX, mouseY = get_virt_mouse_pos()
  3665. if (user_opts.minmousemove == 0) or
  3666. (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
  3667. ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
  3668. or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
  3669. )
  3670. ) then
  3671. show_osc()
  3672. end
  3673. state.last_mouseX, state.last_mouseY = mouseX, mouseY
  3674.  
  3675. local n = state.active_element
  3676. if element_has_action(elements[n], action) then
  3677. elements[n].eventresponder[action](elements[n])
  3678. end
  3679. tick()
  3680. end
  3681. end
  3682.  
  3683. -- called by mpv on every frame
  3684. function tick()
  3685. if (not state.enabled) then return end
  3686.  
  3687. if (state.idle) then
  3688.  
  3689. -- render idle message
  3690. msg.trace("idle message")
  3691. local icon_x, icon_y = 320 - 26, 140
  3692.  
  3693. local ass = assdraw.ass_new()
  3694. ass:new_event()
  3695. ass:pos(icon_x, icon_y)
  3696. 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}")
  3697. ass:new_event()
  3698. ass:pos(icon_x, icon_y)
  3699. 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}")
  3700. ass:new_event()
  3701. ass:pos(icon_x, icon_y)
  3702. 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}")
  3703. ass:new_event()
  3704. ass:pos(icon_x, icon_y)
  3705. 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}")
  3706. ass:new_event()
  3707. ass:pos(icon_x, icon_y)
  3708. 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}")
  3709. ass:new_event()
  3710. ass:pos(icon_x, icon_y)
  3711. 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}")
  3712. ass:new_event()
  3713. ass:pos(320, icon_y+65)
  3714. ass:an(8)
  3715. ass:append("Drop files or URLs to play here.")
  3716. mp.set_osd_ass(640, 360, ass.text)
  3717.  
  3718. if state.showhide_enabled then
  3719. mp.disable_key_bindings("showhide")
  3720. state.showhide_enabled = false
  3721. end
  3722.  
  3723.  
  3724. elseif (state.fullscreen and user_opts.showfullscreen)
  3725. or (not state.fullscreen and user_opts.showwindowed) then
  3726.  
  3727. -- render the OSC
  3728. render()
  3729. else
  3730. -- Flush OSD
  3731. mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "")
  3732. end
  3733. end
  3734.  
  3735. function do_enable_keybindings()
  3736. if state.enabled then
  3737. if not state.showhide_enabled then
  3738. mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
  3739. end
  3740. state.showhide_enabled = true
  3741. end
  3742. end
  3743.  
  3744. function enable_osc(enable)
  3745. state.enabled = enable
  3746. if enable then
  3747. do_enable_keybindings()
  3748. else
  3749. hide_osc() -- acts immediately when state.enabled == false
  3750. if state.showhide_enabled then
  3751. mp.disable_key_bindings("showhide")
  3752. end
  3753. state.showhide_enabled = false
  3754. end
  3755. end
  3756.  
  3757. -- mpv_thumbnail_script.lua --
  3758.  
  3759. local builtin_osc_enabled = mp.get_property_native('osc')
  3760. if builtin_osc_enabled then
  3761. local err = "You must disable the built-in OSC with osc=no in your configuration!"
  3762. mp.osd_message(err, 5)
  3763. msg.error(err)
  3764.  
  3765. -- This may break, but since we can, let's try to just disable the builtin OSC.
  3766. mp.set_property_native('osc', false)
  3767. end
  3768.  
  3769. -- // mpv_thumbnail_script.lua // --
  3770.  
  3771.  
  3772. validate_user_opts()
  3773.  
  3774. mp.register_event("start-file", request_init)
  3775. mp.register_event("tracks-changed", request_init)
  3776. mp.observe_property("playlist", nil, request_init)
  3777.  
  3778. mp.register_script_message("osc-message", show_message)
  3779. mp.register_script_message("osc-chapterlist", function(dur)
  3780. show_message(get_chapterlist(), dur)
  3781. end)
  3782. mp.register_script_message("osc-playlist", function(dur)
  3783. show_message(get_playlist(), dur)
  3784. end)
  3785. mp.register_script_message("osc-tracklist", function(dur)
  3786. local msg = {}
  3787. for k,v in pairs(nicetypes) do
  3788. table.insert(msg, get_tracklist(k))
  3789. end
  3790. show_message(table.concat(msg, '\n\n'), dur)
  3791. end)
  3792.  
  3793. mp.observe_property("fullscreen", "bool",
  3794. function(name, val)
  3795. state.fullscreen = val
  3796. request_init()
  3797. end
  3798. )
  3799. mp.observe_property("idle-active", "bool",
  3800. function(name, val)
  3801. state.idle = val
  3802. tick()
  3803. end
  3804. )
  3805. mp.observe_property("pause", "bool", pause_state)
  3806. mp.observe_property("cache-idle", "bool", cache_state)
  3807. mp.observe_property("vo-configured", "bool", function(name, val)
  3808. if val then
  3809. mp.register_event("tick", tick)
  3810. else
  3811. mp.unregister_event(tick)
  3812. end
  3813. end)
  3814.  
  3815. -- mouse show/hide bindings
  3816. mp.set_key_bindings({
  3817. {"mouse_move", function(e) process_event("mouse_move", nil) end},
  3818. {"mouse_leave", mouse_leave},
  3819. }, "showhide", "force")
  3820. do_enable_keybindings()
  3821.  
  3822. --mouse input bindings
  3823. mp.set_key_bindings({
  3824. {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
  3825. function(e) process_event("mbtn_left", "down") end},
  3826. {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
  3827. function(e) process_event("shift+mbtn_left", "down") end},
  3828. {"mbtn_right", function(e) process_event("mbtn_right", "up") end,
  3829. function(e) process_event("mbtn_right", "down") end},
  3830. {"wheel_up", function(e) process_event("wheel_up", "press") end},
  3831. {"wheel_down", function(e) process_event("wheel_down", "press") end},
  3832. {"mbtn_left_dbl", "ignore"},
  3833. {"shift+mbtn_left_dbl", "ignore"},
  3834. {"mbtn_right_dbl", "ignore"},
  3835. }, "input", "force")
  3836. mp.enable_key_bindings("input")
  3837.  
  3838.  
  3839. user_opts.hidetimeout_orig = user_opts.hidetimeout
  3840.  
  3841. function always_on(val)
  3842. if val then
  3843. user_opts.hidetimeout = -1 -- disable autohide
  3844. if state.enabled then show_osc() end
  3845. else
  3846. user_opts.hidetimeout = user_opts.hidetimeout_orig
  3847. if state.enabled then hide_osc() end
  3848. end
  3849. end
  3850.  
  3851. -- mode can be auto/always/never/cycle
  3852. -- the modes only affect internal variables and not stored on its own.
  3853. function visibility_mode(mode, no_osd)
  3854. if mode == "cycle" then
  3855. if not state.enabled then
  3856. mode = "auto"
  3857. elseif user_opts.hidetimeout >= 0 then
  3858. mode = "always"
  3859. else
  3860. mode = "never"
  3861. end
  3862. end
  3863.  
  3864. if mode == "auto" then
  3865. always_on(false)
  3866. enable_osc(true)
  3867. elseif mode == "always" then
  3868. enable_osc(true)
  3869. always_on(true)
  3870. elseif mode == "never" then
  3871. enable_osc(false)
  3872. else
  3873. msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
  3874. return
  3875. end
  3876.  
  3877. if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
  3878. mp.osd_message("OSC visibility: " .. mode)
  3879. end
  3880. end
  3881.  
  3882. visibility_mode(user_opts.visibility, true)
  3883. mp.register_script_message("osc-visibility", visibility_mode)
  3884. mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
  3885.  
  3886. set_virt_mouse_area(0, 0, 0, 0, "input")
  3887.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement