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