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