976 lines
31 KiB
Lua
976 lines
31 KiB
Lua
|
-- thumbfast.lua
|
||
|
--
|
||
|
-- High-performance on-the-fly thumbnailer
|
||
|
--
|
||
|
-- Built for easy integration in third-party UIs.
|
||
|
|
||
|
local options = {
|
||
|
-- Socket path (leave empty for auto)
|
||
|
socket = "",
|
||
|
|
||
|
-- Thumbnail path (leave empty for auto)
|
||
|
thumbnail = "",
|
||
|
|
||
|
-- Maximum thumbnail size in pixels (scaled down to fit)
|
||
|
-- Values are scaled when hidpi is enabled
|
||
|
max_height = 200,
|
||
|
max_width = 200,
|
||
|
|
||
|
-- Apply tone-mapping, no to disable
|
||
|
tone_mapping = "auto",
|
||
|
|
||
|
-- Overlay id
|
||
|
overlay_id = 42,
|
||
|
|
||
|
-- Spawn thumbnailer on file load for faster initial thumbnails
|
||
|
spawn_first = false,
|
||
|
|
||
|
-- Close thumbnailer process after an inactivity period in seconds, 0 to disable
|
||
|
quit_after_inactivity = 0,
|
||
|
|
||
|
-- Enable on network playback
|
||
|
network = false,
|
||
|
|
||
|
-- Enable on audio playback
|
||
|
audio = false,
|
||
|
|
||
|
-- Enable hardware decoding
|
||
|
hwdec = false,
|
||
|
|
||
|
-- Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
||
|
direct_io = false,
|
||
|
|
||
|
-- Custom path to the mpv executable
|
||
|
mpv_path = "mpv"
|
||
|
}
|
||
|
|
||
|
local mp = require "mp"
|
||
|
mp.utils = require "mp.utils"
|
||
|
mp.options = require "mp.options"
|
||
|
mp.options.read_options(options, "thumbfast")
|
||
|
|
||
|
local properties = {}
|
||
|
local pre_0_30_0 = mp.command_native_async == nil
|
||
|
local pre_0_33_0 = true
|
||
|
|
||
|
function subprocess(args, async, callback)
|
||
|
callback = callback or function()
|
||
|
end
|
||
|
|
||
|
if not pre_0_30_0 then
|
||
|
if async then
|
||
|
return mp.command_native_async({ name = "subprocess", playback_only = true, args = args }, callback)
|
||
|
else
|
||
|
return mp.command_native({ name = "subprocess", playback_only = false, capture_stdout = true, args = args })
|
||
|
end
|
||
|
else
|
||
|
if async then
|
||
|
return mp.utils.subprocess_detached({ args = args }, callback)
|
||
|
else
|
||
|
return mp.utils.subprocess({ args = args })
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local winapi = {}
|
||
|
if options.direct_io then
|
||
|
local ffi_loaded, ffi = pcall(require, "ffi")
|
||
|
if ffi_loaded then
|
||
|
winapi = {
|
||
|
ffi = ffi,
|
||
|
C = ffi.C,
|
||
|
bit = require("bit"),
|
||
|
socket_wc = "",
|
||
|
|
||
|
-- WinAPI constants
|
||
|
CP_UTF8 = 65001,
|
||
|
GENERIC_WRITE = 0x40000000,
|
||
|
OPEN_EXISTING = 3,
|
||
|
FILE_FLAG_WRITE_THROUGH = 0x80000000,
|
||
|
FILE_FLAG_NO_BUFFERING = 0x20000000,
|
||
|
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
|
||
|
|
||
|
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
|
||
|
|
||
|
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
|
||
|
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
|
||
|
}
|
||
|
-- cache flags used in run() to avoid bor() call
|
||
|
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
|
||
|
|
||
|
ffi.cdef [[
|
||
|
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
|
||
|
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
|
||
|
bool __stdcall CloseHandle(void *hObject);
|
||
|
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
|
||
|
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||
|
]]
|
||
|
|
||
|
winapi.MultiByteToWideChar = function(MultiByteStr)
|
||
|
if MultiByteStr then
|
||
|
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
|
||
|
if utf16_len > 0 then
|
||
|
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||
|
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
|
||
|
return utf16_str
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return ""
|
||
|
end
|
||
|
else
|
||
|
options.direct_io = false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local file = nil
|
||
|
local file_bytes = 0
|
||
|
local spawned = false
|
||
|
local disabled = false
|
||
|
local force_disabled = false
|
||
|
local spawn_waiting = false
|
||
|
local spawn_working = false
|
||
|
local script_written = false
|
||
|
|
||
|
local dirty = false
|
||
|
|
||
|
local x = nil
|
||
|
local y = nil
|
||
|
local last_x = x
|
||
|
local last_y = y
|
||
|
|
||
|
local last_seek_time = nil
|
||
|
|
||
|
local effective_w = options.max_width
|
||
|
local effective_h = options.max_height
|
||
|
local real_w = nil
|
||
|
local real_h = nil
|
||
|
local last_real_w = nil
|
||
|
local last_real_h = nil
|
||
|
|
||
|
local script_name = nil
|
||
|
|
||
|
local show_thumbnail = false
|
||
|
|
||
|
local filters_reset = { ["lavfi-crop"] = true, ["crop"] = true }
|
||
|
local filters_runtime = { ["hflip"] = true, ["vflip"] = true }
|
||
|
local filters_all = { ["hflip"] = true, ["vflip"] = true, ["lavfi-crop"] = true, ["crop"] = true }
|
||
|
|
||
|
local tone_mappings = {
|
||
|
["none"] = true,
|
||
|
["clip"] = true,
|
||
|
["linear"] = true,
|
||
|
["gamma"] = true,
|
||
|
["reinhard"] = true,
|
||
|
["hable"] = true,
|
||
|
["mobius"] = true
|
||
|
}
|
||
|
local last_tone_mapping = nil
|
||
|
|
||
|
local last_vf_reset = ""
|
||
|
local last_vf_runtime = ""
|
||
|
|
||
|
local last_rotate = 0
|
||
|
|
||
|
local par = ""
|
||
|
local last_par = ""
|
||
|
|
||
|
local last_has_vid = 0
|
||
|
local has_vid = 0
|
||
|
|
||
|
local file_timer = nil
|
||
|
local file_check_period = 1 / 60
|
||
|
|
||
|
local allow_fast_seek = true
|
||
|
|
||
|
local client_script = [=[
|
||
|
#!/usr/bin/env bash
|
||
|
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
|
||
|
trap "kill 0" EXIT
|
||
|
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
|
||
|
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
|
||
|
]=]
|
||
|
|
||
|
local function get_os()
|
||
|
local raw_os_name = ""
|
||
|
|
||
|
if jit and jit.os and jit.arch then
|
||
|
raw_os_name = jit.os
|
||
|
else
|
||
|
if package.config:sub(1, 1) == "\\" then
|
||
|
-- Windows
|
||
|
local env_OS = os.getenv("OS")
|
||
|
if env_OS then
|
||
|
raw_os_name = env_OS
|
||
|
end
|
||
|
else
|
||
|
raw_os_name = subprocess({ "uname", "-s" }).stdout
|
||
|
end
|
||
|
end
|
||
|
|
||
|
raw_os_name = (raw_os_name):lower()
|
||
|
|
||
|
local os_patterns = {
|
||
|
["windows"] = "windows",
|
||
|
["linux"] = "linux",
|
||
|
|
||
|
["osx"] = "darwin",
|
||
|
["mac"] = "darwin",
|
||
|
["darwin"] = "darwin",
|
||
|
|
||
|
["^mingw"] = "windows",
|
||
|
["^cygwin"] = "windows",
|
||
|
|
||
|
["bsd$"] = "darwin",
|
||
|
["sunos"] = "darwin"
|
||
|
}
|
||
|
|
||
|
-- Default to linux
|
||
|
local str_os_name = "linux"
|
||
|
|
||
|
for pattern, name in pairs(os_patterns) do
|
||
|
if raw_os_name:match(pattern) then
|
||
|
str_os_name = name
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return str_os_name
|
||
|
end
|
||
|
|
||
|
local os_name = mp.get_property("platform") or get_os()
|
||
|
|
||
|
local path_separator = os_name == "windows" and "\\" or "/"
|
||
|
|
||
|
if options.socket == "" then
|
||
|
if os_name == "windows" then
|
||
|
options.socket = "thumbfast"
|
||
|
else
|
||
|
options.socket = "/tmp/thumbfast"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if options.thumbnail == "" then
|
||
|
if os_name == "windows" then
|
||
|
options.thumbnail = os.getenv("TEMP") .. "\\thumbfast.out"
|
||
|
else
|
||
|
options.thumbnail = "/tmp/thumbfast.out"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local unique = mp.utils.getpid()
|
||
|
|
||
|
options.socket = options.socket .. unique
|
||
|
options.thumbnail = options.thumbnail .. unique
|
||
|
|
||
|
if options.direct_io then
|
||
|
if os_name == "windows" then
|
||
|
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
|
||
|
end
|
||
|
|
||
|
if winapi.socket_wc == "" then
|
||
|
options.direct_io = false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local mpv_path = options.mpv_path
|
||
|
|
||
|
if mpv_path == "mpv" and os_name == "darwin" and unique then
|
||
|
-- TODO: look into ~~osxbundle/
|
||
|
mpv_path = string.gsub(subprocess({ "ps", "-o", "comm=", "-p", tostring(unique) }).stdout, "[\n\r]", "")
|
||
|
if mpv_path ~= "mpv" then
|
||
|
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
||
|
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
|
||
|
if mpv_bin and mpv_bin.is_file then
|
||
|
mpv_path = "/usr/local/mpv"
|
||
|
else
|
||
|
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
|
||
|
if mpv_app and mpv_app.is_file then
|
||
|
mp.msg.warn(
|
||
|
"symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||
|
else
|
||
|
mp.msg.warn(
|
||
|
"drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function vo_tone_mapping()
|
||
|
local passes = mp.get_property_native("vo-passes")
|
||
|
if passes and passes["fresh"] then
|
||
|
for _, v in pairs(passes["fresh"]) do
|
||
|
for k2, v2 in pairs(v) do
|
||
|
if k2 == "desc" and v2 then
|
||
|
local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
|
||
|
if tone_mapping then
|
||
|
return tone_mapping
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function vf_string(filters, full)
|
||
|
local vf = ""
|
||
|
local vf_table = properties["vf"]
|
||
|
|
||
|
if vf_table and #vf_table > 0 then
|
||
|
for i = #vf_table, 1, -1 do
|
||
|
if filters[vf_table[i].name] then
|
||
|
local args = ""
|
||
|
for key, value in pairs(vf_table[i].params) do
|
||
|
if args ~= "" then
|
||
|
args = args .. ":"
|
||
|
end
|
||
|
args = args .. key .. "=" .. value
|
||
|
end
|
||
|
vf = vf .. vf_table[i].name .. "=" .. args .. ","
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
|
||
|
if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
|
||
|
local tone_mapping = options.tone_mapping
|
||
|
if tone_mapping == "auto" then
|
||
|
tone_mapping = last_tone_mapping or properties["tone-mapping"]
|
||
|
if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
|
||
|
tone_mapping = vo_tone_mapping()
|
||
|
end
|
||
|
end
|
||
|
if not tone_mappings[tone_mapping] then
|
||
|
tone_mapping = "hable"
|
||
|
end
|
||
|
last_tone_mapping = tone_mapping
|
||
|
vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap=" .. tone_mapping .. ",zscale=transfer=bt709,"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if full then
|
||
|
vf = vf ..
|
||
|
"scale=w=" ..
|
||
|
effective_w .. ":h=" .. effective_h .. par ..
|
||
|
",pad=w=" .. effective_w .. ":h=" .. effective_h .. ":x=-1:y=-1,format=bgra"
|
||
|
end
|
||
|
|
||
|
return vf
|
||
|
end
|
||
|
|
||
|
local function calc_dimensions()
|
||
|
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
|
||
|
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
|
||
|
if not width or not height then return end
|
||
|
|
||
|
local scale = properties["display-hidpi-scale"] or 1
|
||
|
|
||
|
if width / height > options.max_width / options.max_height then
|
||
|
effective_w = math.floor(options.max_width * scale + 0.5)
|
||
|
effective_h = math.floor(height / width * effective_w + 0.5)
|
||
|
else
|
||
|
effective_h = math.floor(options.max_height * scale + 0.5)
|
||
|
effective_w = math.floor(width / height * effective_h + 0.5)
|
||
|
end
|
||
|
|
||
|
local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
|
||
|
if v_par == 1 then
|
||
|
par = ":force_original_aspect_ratio=decrease"
|
||
|
else
|
||
|
par = ""
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local info_timer = nil
|
||
|
|
||
|
local function info(w, h)
|
||
|
local rotate = properties["video-params"] and properties["video-params"]["rotate"]
|
||
|
local image = properties["current-tracks"] and properties["current-tracks"]["video"] and
|
||
|
properties["current-tracks"]["video"]["image"]
|
||
|
local albumart = image and properties["current-tracks"]["video"]["albumart"]
|
||
|
|
||
|
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
||
|
has_vid == 0 or
|
||
|
(properties["demuxer-via-network"] and not options.network) or
|
||
|
(albumart and not options.audio) or
|
||
|
(image and not albumart) or
|
||
|
force_disabled
|
||
|
|
||
|
if info_timer then
|
||
|
info_timer:kill()
|
||
|
info_timer = nil
|
||
|
elseif has_vid == 0 or (rotate == nil and not disabled) then
|
||
|
info_timer = mp.add_timeout(0.05, function() info(w, h) end)
|
||
|
end
|
||
|
|
||
|
local json, _ = mp.utils.format_json({
|
||
|
width = w,
|
||
|
height = h,
|
||
|
disabled = disabled,
|
||
|
available = true,
|
||
|
socket = options.socket,
|
||
|
thumbnail = options.thumbnail,
|
||
|
overlay_id = options.overlay_id
|
||
|
})
|
||
|
if pre_0_30_0 then
|
||
|
mp.command_native({ "script-message", "thumbfast-info", json })
|
||
|
else
|
||
|
mp.command_native_async({ "script-message", "thumbfast-info", json }, function()
|
||
|
end)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function remove_thumbnail_files()
|
||
|
if file then
|
||
|
file:close()
|
||
|
file = nil
|
||
|
file_bytes = 0
|
||
|
end
|
||
|
os.remove(options.thumbnail)
|
||
|
os.remove(options.thumbnail .. ".bgra")
|
||
|
end
|
||
|
|
||
|
local activity_timer
|
||
|
|
||
|
local function spawn(time)
|
||
|
if disabled then return end
|
||
|
|
||
|
local path = properties["path"]
|
||
|
if path == nil then return end
|
||
|
|
||
|
if options.quit_after_inactivity > 0 then
|
||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||
|
activity_timer:kill()
|
||
|
end
|
||
|
activity_timer:resume()
|
||
|
end
|
||
|
|
||
|
local open_filename = properties["stream-open-filename"]
|
||
|
local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
|
||
|
if ytdl then
|
||
|
path = open_filename
|
||
|
end
|
||
|
|
||
|
remove_thumbnail_files()
|
||
|
|
||
|
local vid = properties["vid"]
|
||
|
has_vid = vid or 0
|
||
|
|
||
|
local args = {
|
||
|
mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet",
|
||
|
"--no-terminal",
|
||
|
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no",
|
||
|
"--load-auto-profiles=no",
|
||
|
"--edition=" .. (properties["edition"] or "auto"), "--vid=" .. (vid or "auto"), "--no-sub", "--no-audio",
|
||
|
"--start=" .. time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
|
||
|
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
||
|
"--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2",
|
||
|
"--hwdec=" .. (options.hwdec and "auto" or "no"),
|
||
|
"--vf=" .. vf_string(filters_all, true),
|
||
|
"--sws-scaler=fast-bilinear",
|
||
|
"--video-rotate=" .. last_rotate,
|
||
|
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o=" .. options.thumbnail
|
||
|
}
|
||
|
|
||
|
if not pre_0_30_0 then
|
||
|
table.insert(args, "--sws-allow-zimg=no")
|
||
|
end
|
||
|
|
||
|
if os_name == "darwin" and properties["macos-app-activation-policy"] then
|
||
|
table.insert(args, "--macos-app-activation-policy=accessory")
|
||
|
end
|
||
|
|
||
|
if os_name == "windows" or pre_0_33_0 then
|
||
|
table.insert(args, "--input-ipc-server=" .. options.socket)
|
||
|
elseif not script_written then
|
||
|
local client_script_path = options.socket .. ".run"
|
||
|
local script = io.open(client_script_path, "w+")
|
||
|
if script == nil then
|
||
|
mp.msg.error("client script write failed")
|
||
|
return
|
||
|
else
|
||
|
script_written = true
|
||
|
script:write(string.format(client_script, options.socket))
|
||
|
script:close()
|
||
|
subprocess({ "chmod", "+x", client_script_path }, true)
|
||
|
table.insert(args, "--scripts=" .. client_script_path)
|
||
|
end
|
||
|
else
|
||
|
local client_script_path = options.socket .. ".run"
|
||
|
table.insert(args, "--scripts=" .. client_script_path)
|
||
|
end
|
||
|
|
||
|
table.insert(args, "--")
|
||
|
table.insert(args, path)
|
||
|
|
||
|
spawned = true
|
||
|
spawn_waiting = true
|
||
|
|
||
|
subprocess(args, true,
|
||
|
function(success, result)
|
||
|
if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
|
||
|
spawned = false
|
||
|
spawn_waiting = false
|
||
|
options.tone_mapping = "no"
|
||
|
mp.msg.error("mpv subprocess create failed")
|
||
|
if not spawn_working then -- notify users of required configuration
|
||
|
if options.mpv_path == "mpv" then
|
||
|
if properties["current-vo"] == "libmpv" then
|
||
|
if options.mpv_path == mpv_path then -- attempt to locate ImPlay
|
||
|
mpv_path = "ImPlay"
|
||
|
spawn(time)
|
||
|
else -- ImPlay not in path
|
||
|
if os_name ~= "darwin" then
|
||
|
force_disabled = true
|
||
|
info(real_w or effective_w, real_h or effective_h)
|
||
|
end
|
||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup",
|
||
|
"Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" ..
|
||
|
string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }),
|
||
|
"[/\\]", path_separator) .. "\nand restart ImPlay")
|
||
|
end
|
||
|
else
|
||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||
|
if os_name == "windows" then
|
||
|
mp.commandv("script-message-to", "mpvnet", "show-text",
|
||
|
"thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||
|
mp.commandv("script-message", "mpv.net", "show-text",
|
||
|
"thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||
|
-- found ImPlay but not defined in config
|
||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast",
|
||
|
"Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" ..
|
||
|
string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), "[/\\]",
|
||
|
path_separator) .. "\nand restart ImPlay")
|
||
|
end
|
||
|
end
|
||
|
elseif success == true and (result.status == 0 or result.status == -2) then
|
||
|
if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
|
||
|
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup",
|
||
|
"Set mpv_path=ImPlay in thumbfast config:\n" ..
|
||
|
string.gsub(mp.command_native({ "expand-path", "~~/script-opts/thumbfast.conf" }), "[/\\]",
|
||
|
path_separator) .. "\nand restart ImPlay")
|
||
|
end
|
||
|
spawn_working = true
|
||
|
spawn_waiting = false
|
||
|
end
|
||
|
end
|
||
|
)
|
||
|
end
|
||
|
|
||
|
local function run(command)
|
||
|
if not spawned then return end
|
||
|
|
||
|
if options.direct_io then
|
||
|
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING,
|
||
|
winapi._createfile_pipe_flags, nil)
|
||
|
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
|
||
|
local buf = command .. "\n"
|
||
|
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
|
||
|
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
|
||
|
winapi.C.CloseHandle(hPipe)
|
||
|
end
|
||
|
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local command_n = command .. "\n"
|
||
|
|
||
|
if os_name == "windows" then
|
||
|
if file and file_bytes + #command_n >= 4096 then
|
||
|
file:close()
|
||
|
file = nil
|
||
|
file_bytes = 0
|
||
|
end
|
||
|
if not file then
|
||
|
file = io.open("\\\\.\\pipe\\" .. options.socket, "r+b")
|
||
|
end
|
||
|
elseif pre_0_33_0 then
|
||
|
subprocess({ "/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket })
|
||
|
return
|
||
|
elseif not file then
|
||
|
file = io.open(options.socket, "r+")
|
||
|
end
|
||
|
if file then
|
||
|
file_bytes = file:seek("end")
|
||
|
file:write(command_n)
|
||
|
file:flush()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function draw(w, h, script)
|
||
|
if not w or not show_thumbnail then return end
|
||
|
if x ~= nil then
|
||
|
if pre_0_30_0 then
|
||
|
mp.command_native({ "overlay-add", options.overlay_id, x, y, options.thumbnail .. ".bgra", 0, "bgra", w, h,
|
||
|
(4 * w) })
|
||
|
else
|
||
|
mp.command_native_async(
|
||
|
{ "overlay-add", options.overlay_id, x, y, options.thumbnail .. ".bgra", 0, "bgra", w, h, (4 * w) },
|
||
|
function()
|
||
|
end)
|
||
|
end
|
||
|
elseif script then
|
||
|
local json, _ = mp.utils.format_json({
|
||
|
width = w,
|
||
|
height = h,
|
||
|
x = x,
|
||
|
y = y,
|
||
|
socket = options.socket,
|
||
|
thumbnail = options.thumbnail,
|
||
|
overlay_id = options.overlay_id
|
||
|
})
|
||
|
mp.commandv("script-message-to", script, "thumbfast-render", json)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function real_res(req_w, req_h, filesize)
|
||
|
local count = filesize / 4
|
||
|
local diff = (req_w * req_h) - count
|
||
|
|
||
|
if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
|
||
|
req_w, req_h = req_h, req_w
|
||
|
end
|
||
|
|
||
|
if diff == 0 then
|
||
|
return req_w, req_h
|
||
|
else
|
||
|
local threshold = 5 -- throw out results that change too much
|
||
|
local long_side, short_side = req_w, req_h
|
||
|
if req_h > req_w then
|
||
|
long_side, short_side = req_h, req_w
|
||
|
end
|
||
|
for a = short_side, short_side - threshold, -1 do
|
||
|
if count % a == 0 then
|
||
|
local b = count / a
|
||
|
if long_side - b < threshold then
|
||
|
if req_h < req_w then return b, a else return a, b end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function move_file(from, to)
|
||
|
if os_name == "windows" then
|
||
|
os.remove(to)
|
||
|
end
|
||
|
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
|
||
|
os.rename(from, to)
|
||
|
end
|
||
|
|
||
|
local function seek(fast)
|
||
|
if last_seek_time then
|
||
|
run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local seek_period = 3 / 60
|
||
|
local seek_period_counter = 0
|
||
|
local seek_timer
|
||
|
seek_timer = mp.add_periodic_timer(seek_period, function()
|
||
|
if seek_period_counter == 0 then
|
||
|
seek(allow_fast_seek)
|
||
|
seek_period_counter = 1
|
||
|
else
|
||
|
if seek_period_counter == 2 then
|
||
|
if allow_fast_seek then
|
||
|
seek_timer:kill()
|
||
|
seek()
|
||
|
end
|
||
|
else
|
||
|
seek_period_counter = seek_period_counter + 1
|
||
|
end
|
||
|
end
|
||
|
end)
|
||
|
seek_timer:kill()
|
||
|
|
||
|
local function request_seek()
|
||
|
if seek_timer:is_enabled() then
|
||
|
seek_period_counter = 0
|
||
|
else
|
||
|
seek_timer:resume()
|
||
|
seek(allow_fast_seek)
|
||
|
seek_period_counter = 1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function check_new_thumb()
|
||
|
-- the slave might start writing to the file after checking existance and
|
||
|
-- validity but before actually moving the file, so move to a temporary
|
||
|
-- location before validity check to make sure everything stays consistant
|
||
|
-- and valid thumbnails don't get overwritten by invalid ones
|
||
|
local tmp = options.thumbnail .. ".tmp"
|
||
|
move_file(options.thumbnail, tmp)
|
||
|
local finfo = mp.utils.file_info(tmp)
|
||
|
if not finfo then return false end
|
||
|
spawn_waiting = false
|
||
|
local w, h = real_res(effective_w, effective_h, finfo.size)
|
||
|
if w then -- only accept valid thumbnails
|
||
|
move_file(tmp, options.thumbnail .. ".bgra")
|
||
|
|
||
|
real_w, real_h = w, h
|
||
|
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
|
||
|
last_real_w, last_real_h = real_w, real_h
|
||
|
info(real_w, real_h)
|
||
|
end
|
||
|
if not show_thumbnail then
|
||
|
file_timer:kill()
|
||
|
end
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
file_timer = mp.add_periodic_timer(file_check_period, function()
|
||
|
if check_new_thumb() then
|
||
|
draw(real_w, real_h, script_name)
|
||
|
end
|
||
|
end)
|
||
|
file_timer:kill()
|
||
|
|
||
|
local function clear()
|
||
|
file_timer:kill()
|
||
|
seek_timer:kill()
|
||
|
if options.quit_after_inactivity > 0 then
|
||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||
|
activity_timer:kill()
|
||
|
end
|
||
|
activity_timer:resume()
|
||
|
end
|
||
|
last_seek_time = nil
|
||
|
show_thumbnail = false
|
||
|
last_x = nil
|
||
|
last_y = nil
|
||
|
if script_name then return end
|
||
|
if pre_0_30_0 then
|
||
|
mp.command_native({ "overlay-remove", options.overlay_id })
|
||
|
else
|
||
|
mp.command_native_async({ "overlay-remove", options.overlay_id }, function()
|
||
|
end)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function quit()
|
||
|
activity_timer:kill()
|
||
|
if show_thumbnail then
|
||
|
activity_timer:resume()
|
||
|
return
|
||
|
end
|
||
|
run("quit")
|
||
|
spawned = false
|
||
|
real_w, real_h = nil, nil
|
||
|
clear()
|
||
|
end
|
||
|
|
||
|
activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
|
||
|
activity_timer:kill()
|
||
|
|
||
|
local function thumb(time, r_x, r_y, script)
|
||
|
if disabled then return end
|
||
|
|
||
|
time = tonumber(time)
|
||
|
if time == nil then return end
|
||
|
|
||
|
if r_x == "" or r_y == "" then
|
||
|
x, y = nil, nil
|
||
|
else
|
||
|
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
|
||
|
end
|
||
|
|
||
|
script_name = script
|
||
|
if last_x ~= x or last_y ~= y or not show_thumbnail then
|
||
|
show_thumbnail = true
|
||
|
last_x = x
|
||
|
last_y = y
|
||
|
draw(real_w, real_h, script)
|
||
|
end
|
||
|
|
||
|
if options.quit_after_inactivity > 0 then
|
||
|
if show_thumbnail or activity_timer:is_enabled() then
|
||
|
activity_timer:kill()
|
||
|
end
|
||
|
activity_timer:resume()
|
||
|
end
|
||
|
|
||
|
if time == last_seek_time then return end
|
||
|
last_seek_time = time
|
||
|
if not spawned then spawn(time) end
|
||
|
request_seek()
|
||
|
if not file_timer:is_enabled() then file_timer:resume() end
|
||
|
end
|
||
|
|
||
|
local function watch_changes()
|
||
|
if not dirty or not properties["video-out-params"] then return end
|
||
|
dirty = false
|
||
|
|
||
|
local old_w = effective_w
|
||
|
local old_h = effective_h
|
||
|
|
||
|
calc_dimensions()
|
||
|
|
||
|
local vf_reset = vf_string(filters_reset)
|
||
|
local rotate = properties["video-rotate"] or 0
|
||
|
|
||
|
local resized = old_w ~= effective_w or
|
||
|
old_h ~= effective_h or
|
||
|
last_vf_reset ~= vf_reset or
|
||
|
(last_rotate % 180) ~= (rotate % 180) or
|
||
|
par ~= last_par
|
||
|
|
||
|
if resized then
|
||
|
last_rotate = rotate
|
||
|
info(effective_w, effective_h)
|
||
|
elseif last_has_vid ~= has_vid and has_vid ~= 0 then
|
||
|
info(effective_w, effective_h)
|
||
|
end
|
||
|
|
||
|
if spawned then
|
||
|
if resized then
|
||
|
-- mpv doesn't allow us to change output size
|
||
|
local seek_time = last_seek_time
|
||
|
run("quit")
|
||
|
clear()
|
||
|
spawned = false
|
||
|
spawn(seek_time or mp.get_property_number("time-pos", 0))
|
||
|
file_timer:resume()
|
||
|
else
|
||
|
if rotate ~= last_rotate then
|
||
|
run("set video-rotate " .. rotate)
|
||
|
end
|
||
|
local vf_runtime = vf_string(filters_runtime)
|
||
|
if vf_runtime ~= last_vf_runtime then
|
||
|
run("vf set " .. vf_string(filters_all, true))
|
||
|
last_vf_runtime = vf_runtime
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
last_vf_runtime = vf_string(filters_runtime)
|
||
|
end
|
||
|
|
||
|
last_vf_reset = vf_reset
|
||
|
last_rotate = rotate
|
||
|
last_par = par
|
||
|
last_has_vid = has_vid
|
||
|
|
||
|
if not spawned and not disabled and options.spawn_first and resized then
|
||
|
spawn(mp.get_property_number("time-pos", 0))
|
||
|
file_timer:resume()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function update_property(name, value)
|
||
|
properties[name] = value
|
||
|
end
|
||
|
|
||
|
local function update_property_dirty(name, value)
|
||
|
properties[name] = value
|
||
|
dirty = true
|
||
|
if name == "tone-mapping" then
|
||
|
last_tone_mapping = nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function update_tracklist(_, value)
|
||
|
-- current-tracks shim
|
||
|
for _, track in ipairs(value) do
|
||
|
if track.type == "video" and track.selected then
|
||
|
properties["current-tracks/video/image"] = track.image
|
||
|
properties["current-tracks/video/albumart"] = track.albumart
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function sync_changes(prop, val)
|
||
|
update_property(prop, val)
|
||
|
if val == nil then return end
|
||
|
|
||
|
if type(val) == "boolean" then
|
||
|
if prop == "vid" then
|
||
|
has_vid = 0
|
||
|
last_has_vid = 0
|
||
|
info(effective_w, effective_h)
|
||
|
clear()
|
||
|
return
|
||
|
end
|
||
|
val = val and "yes" or "no"
|
||
|
end
|
||
|
|
||
|
if prop == "vid" then
|
||
|
has_vid = 1
|
||
|
end
|
||
|
|
||
|
if not spawned then return end
|
||
|
|
||
|
run("set " .. prop .. " " .. val)
|
||
|
dirty = true
|
||
|
end
|
||
|
|
||
|
local function file_load()
|
||
|
clear()
|
||
|
spawned = false
|
||
|
real_w, real_h = nil, nil
|
||
|
last_real_w, last_real_h = nil, nil
|
||
|
last_tone_mapping = nil
|
||
|
last_seek_time = nil
|
||
|
if info_timer then
|
||
|
info_timer:kill()
|
||
|
info_timer = nil
|
||
|
end
|
||
|
|
||
|
calc_dimensions()
|
||
|
info(effective_w, effective_h)
|
||
|
end
|
||
|
|
||
|
local function shutdown()
|
||
|
run("quit")
|
||
|
remove_thumbnail_files()
|
||
|
if os_name ~= "windows" then
|
||
|
os.remove(options.socket)
|
||
|
os.remove(options.socket .. ".run")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function on_duration(_, val)
|
||
|
allow_fast_seek = (val or 30) >= 30
|
||
|
end
|
||
|
|
||
|
mp.observe_property("current-tracks", "native", function(name, value)
|
||
|
if pre_0_33_0 then
|
||
|
mp.unobserve_property(update_tracklist)
|
||
|
pre_0_33_0 = false
|
||
|
end
|
||
|
update_property(name, value)
|
||
|
end)
|
||
|
|
||
|
mp.observe_property("track-list", "native", update_tracklist)
|
||
|
mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
|
||
|
mp.observe_property("video-out-params", "native", update_property_dirty)
|
||
|
mp.observe_property("video-params", "native", update_property_dirty)
|
||
|
mp.observe_property("vf", "native", update_property_dirty)
|
||
|
mp.observe_property("tone-mapping", "native", update_property_dirty)
|
||
|
mp.observe_property("demuxer-via-network", "native", update_property)
|
||
|
mp.observe_property("stream-open-filename", "native", update_property)
|
||
|
mp.observe_property("macos-app-activation-policy", "native", update_property)
|
||
|
mp.observe_property("current-vo", "native", update_property)
|
||
|
mp.observe_property("video-rotate", "native", update_property)
|
||
|
mp.observe_property("path", "native", update_property)
|
||
|
mp.observe_property("vid", "native", sync_changes)
|
||
|
mp.observe_property("edition", "native", sync_changes)
|
||
|
mp.observe_property("duration", "native", on_duration)
|
||
|
|
||
|
mp.register_script_message("thumb", thumb)
|
||
|
mp.register_script_message("clear", clear)
|
||
|
|
||
|
mp.register_event("file-loaded", file_load)
|
||
|
mp.register_event("shutdown", shutdown)
|
||
|
|
||
|
mp.register_idle(watch_changes)
|