add thumbfast script
This commit is contained in:
parent
522c4cae6f
commit
6c8c5ed661
7 changed files with 937 additions and 2441 deletions
|
@ -1,42 +0,0 @@
|
||||||
###不支持参数后注释,须另起一行
|
|
||||||
|
|
||||||
#layout=bottombox
|
|
||||||
# -- "bottombox"是osc_lazy新增的专属布局,基于box布局改进而来并兼容缩略图脚本
|
|
||||||
# -- 该布局不支持valign,halign,boxalpha这些原本影响box布局的选项
|
|
||||||
# -- 该布局设计时不考虑长宽比小于1的视频可用性(例如手机拍摄的9:16的视频),因为元素将被覆盖或不显示,如需在此情况下使用,请启用mpv.conf中的 "keepaspect-window=no" 参数(允许在不改变视频尺寸的情况下手动拉伸补充黑边)
|
|
||||||
# -- 如果使用下方示例的title长度,需要视频长宽比至少大于1.3才能避免标题栏目不被覆盖,自行选择删减监视的参数数量
|
|
||||||
# -- 该布局性能要求比其它布局都高,此外也可能产生非性能原因导致的不明卡顿甚至窗口冻结(解决方案:排除影响参数,或手动快捷键改变一次播放进度)
|
|
||||||
##基于以上多个原因,不把bottombox作为懒人包的默认布局
|
|
||||||
|
|
||||||
deadzonesize=1
|
|
||||||
|
|
||||||
seekbarstyle=knob
|
|
||||||
|
|
||||||
title=音量[${volume}] 速度[${speed}] 列表[${playlist-pos-1}/${playlist-count}] ${!chapters==0:章节[${chapter}/${chapters}]} 解码[${?hwdec-current==no:SW}${?=hwdec-current==:SW}${!hwdec-current==no:${hwdec-current}}]${!fullscreen==yes: 缩放[${current-window-scale}]}
|
|
||||||
# -- 所有布局的主标题显示内容,兼容属性 https://mpv.io/manual/master/#property-list 扩展字符串 https://mpv.io/manual/master/#property-expansion
|
|
||||||
|
|
||||||
#boxmaxchars=150
|
|
||||||
# -- 所有box布局的标题长度限制,osc_lazy版的默认值即150。在示例title的所需长度下,此时低于140将压缩文字并变形
|
|
||||||
|
|
||||||
#timetotal=yes
|
|
||||||
# -- 显示总时间而不是剩余时间,osc_lazy版的默认值即yes
|
|
||||||
|
|
||||||
playlist_osd=no
|
|
||||||
# -- 禁用它因为和懒人包的设置冲突(播发新文件时OSD会重叠显示)
|
|
||||||
|
|
||||||
##其它可用选项及注释见 osc.conf ,这里不全部列出
|
|
||||||
##...
|
|
||||||
##以下参数不存在于原版OSC中
|
|
||||||
|
|
||||||
|
|
||||||
#wctitle=${media-title}
|
|
||||||
# -- osc_lazy版无边框模式的上方标题与OSC标题的显示内容相互独立。示例即默认值
|
|
||||||
#sub_title=
|
|
||||||
# -- bottombox布局的右侧子标题(可选,默认不显示),兼容属性和扩展字符串
|
|
||||||
#sub_title2=
|
|
||||||
# -- bottombox布局的临时右侧子标题(在光标移动到时间轴时强制显示,默认为监视调色板属性),兼容属性和扩展字符串
|
|
||||||
|
|
||||||
#font=
|
|
||||||
#font_mono=
|
|
||||||
#font_bold=
|
|
||||||
# -- 以上三项为OSC的全局字体显示,默认值分别为 sans sans 500
|
|
28
script-opts/thumbfast.conf
Normal file
28
script-opts/thumbfast.conf
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Overlay id
|
||||||
|
overlay_id=42
|
||||||
|
|
||||||
|
# Spawn thumbnailer on file load for faster initial thumbnails
|
||||||
|
spawn_first=no
|
||||||
|
|
||||||
|
# Enable on network playback
|
||||||
|
network=no
|
||||||
|
|
||||||
|
# Enable on audio playback
|
||||||
|
audio=no
|
||||||
|
|
||||||
|
# Enable hardware decoding
|
||||||
|
hwdec=no
|
||||||
|
|
||||||
|
# Windows only: use native Windows API to write to pipe (requires LuaJIT)
|
||||||
|
direct_io=no
|
|
@ -1,77 +0,0 @@
|
||||||
###不支持参数后注释,须另起一行
|
|
||||||
###在缩略图生成过程中关闭MPV窗口,需等待1s左右完全结束进程
|
|
||||||
|
|
||||||
auto_gen=no
|
|
||||||
##是否生成缩略图,默认:yes
|
|
||||||
auto_show=no
|
|
||||||
##是否显示缩略图,默认:yes
|
|
||||||
auto_delete=2
|
|
||||||
##退出MPV后清理本次使用时产生的临时文件,0不清理,1关闭文件时清理,2退出程序时清理。默认0
|
|
||||||
#start_delay=2
|
|
||||||
##延后启动缩略图进程(秒)
|
|
||||||
|
|
||||||
#cache_dir=default_cache_dir
|
|
||||||
##缩略图缓存目录,使用绝对路径,win10默认为 C:/Users/你的用户名/AppData/Local/Temp/Thumbnailer/
|
|
||||||
#worker_script_path=
|
|
||||||
##搜索该目录下的worker脚本,仅当找不到worker时使用(默认为空)
|
|
||||||
#exec_path=C:/xxxx/mpv-lazy/
|
|
||||||
##搜索该目录下的"ffmpeg.exe"(无需配置环境变量),使用绝对路径(默认为空)
|
|
||||||
|
|
||||||
dimension=480
|
|
||||||
##缩略图最大高度/宽度,默认320
|
|
||||||
thumbnail_count=90
|
|
||||||
##周期内限制数量(默认120)。24分钟的视频作为参考,以下两个参数下生成的数量正好为90
|
|
||||||
min_delta=10
|
|
||||||
##最小值默认5秒
|
|
||||||
max_delta=30
|
|
||||||
##最大值默认60秒
|
|
||||||
##最小和最大时间轴间隔
|
|
||||||
#remote_delta_factor=2
|
|
||||||
##远程源因数
|
|
||||||
#stream_delta_factor=2
|
|
||||||
##网络流因数
|
|
||||||
#bitrate_delta_factor=2
|
|
||||||
##比特流因数
|
|
||||||
#bitrate_threshold=8
|
|
||||||
##高码率源的阈值
|
|
||||||
|
|
||||||
#spacer=2
|
|
||||||
##缩略图边框
|
|
||||||
#show_progress=1
|
|
||||||
##显示缩略图进度,0不显示,1仅生成中显示,2始终显示
|
|
||||||
#centered=no
|
|
||||||
##缩略图居中显示
|
|
||||||
#update_time=0.5
|
|
||||||
##缩略图刷新间隔(秒)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#max_workers=3
|
|
||||||
##并行的worker数量
|
|
||||||
#worker_remote_factor=0.5
|
|
||||||
##远程源因数
|
|
||||||
#worker_bitrate_factor=0.5
|
|
||||||
##比特流因数
|
|
||||||
#worker_delay=0.5
|
|
||||||
##延后启动worker(秒)
|
|
||||||
worker_timeout=1
|
|
||||||
##结束编码阶段时间(秒),默认4
|
|
||||||
#accurate_seek=yes
|
|
||||||
##使用精确帧,默认no
|
|
||||||
use_ffmpeg=yes
|
|
||||||
##是否使用ffmpeg,默认no
|
|
||||||
prefer_ffmpeg=yes
|
|
||||||
##优先使用ffmpeg,默认no
|
|
||||||
ffmpeg_threads=1
|
|
||||||
##并行的ffmpeg线程数,默认8
|
|
||||||
ffmpeg_scaler=neighbor
|
|
||||||
##ffmpeg编码时的软件缩放算法 https://ffmpeg.org/ffmpeg-scaler.html#toc-Scaler-Options ,默认bilinear
|
|
||||||
mpv_scaler=point
|
|
||||||
##mpv编码时的软件缩放算法 https://mpv.io/manual/master/#options-sws-scaler ,默认bilinear
|
|
||||||
|
|
||||||
#mpv_hwdec=auto
|
|
||||||
##mpv使用的解码模式,默认no
|
|
||||||
#ffmpeg_hwaccel=d3d11va
|
|
||||||
##ffmpeg使用的解码模式,默认none
|
|
||||||
#ffmpeg_hwaccel_device=0
|
|
||||||
##ffmpeg硬解设备选择(双显卡设备注意切换)
|
|
File diff suppressed because it is too large
Load diff
712
scripts/thumbfast.lua
Normal file
712
scripts/thumbfast.lua
Normal file
|
@ -0,0 +1,712 @@
|
||||||
|
-- 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,
|
||||||
|
|
||||||
|
-- Overlay id
|
||||||
|
overlay_id = 42,
|
||||||
|
|
||||||
|
-- Spawn thumbnailer on file load for faster initial thumbnails
|
||||||
|
spawn_first = false,
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.utils = require "mp.utils"
|
||||||
|
mp.options = require "mp.options"
|
||||||
|
mp.options.read_options(options, "thumbfast")
|
||||||
|
|
||||||
|
local pre_0_30_0 = mp.command_native_async == nil
|
||||||
|
|
||||||
|
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 spawned = false
|
||||||
|
local network = false
|
||||||
|
local disabled = false
|
||||||
|
local spawn_waiting = 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 = filters_runtime
|
||||||
|
for k,v in pairs(filters_reset) do filters_all[k] = v end
|
||||||
|
|
||||||
|
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 first_file = false
|
||||||
|
|
||||||
|
local function debounce(func, wait)
|
||||||
|
func = type(func) == "function" and func or function() end
|
||||||
|
wait = type(wait) == "number" and wait / 1000 or 0
|
||||||
|
|
||||||
|
local timer = nil
|
||||||
|
local timer_end = function ()
|
||||||
|
timer:kill()
|
||||||
|
timer = nil
|
||||||
|
func()
|
||||||
|
end
|
||||||
|
|
||||||
|
return function ()
|
||||||
|
if timer then
|
||||||
|
timer:kill()
|
||||||
|
end
|
||||||
|
timer = mp.add_timeout(wait, timer_end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local client_script = [=[
|
||||||
|
#!/bin/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"] = "Mac",
|
||||||
|
["mac"] = "Mac",
|
||||||
|
["darwin"] = "Mac",
|
||||||
|
|
||||||
|
["^mingw"] = "Windows",
|
||||||
|
["^cygwin"] = "Windows",
|
||||||
|
|
||||||
|
["bsd$"] = "Mac",
|
||||||
|
["sunos"] = "Mac"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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 = get_os()
|
||||||
|
|
||||||
|
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 = "mpv"
|
||||||
|
|
||||||
|
if os_name == "Mac" and unique then
|
||||||
|
mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
|
||||||
|
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vf_string(filters, full)
|
||||||
|
local vf = ""
|
||||||
|
local vf_table = mp.get_property_native("vf")
|
||||||
|
|
||||||
|
if #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 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 = mp.get_property_number("video-out-params/dw")
|
||||||
|
local height = mp.get_property_number("video-out-params/dh")
|
||||||
|
if not width or not height then return end
|
||||||
|
|
||||||
|
local scale = mp.get_property_number("display-hidpi-scale", 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 = mp.get_property_number("video-out-params/par", 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 display_w, display_h = w, h
|
||||||
|
local rotate = mp.get_property_number("video-params/rotate")
|
||||||
|
|
||||||
|
network = mp.get_property_bool("demuxer-via-network", false)
|
||||||
|
local image = mp.get_property_native("current-tracks/video/image", false)
|
||||||
|
local albumart = image and mp.get_property_native("current-tracks/video/albumart", false)
|
||||||
|
disabled = (w or 0) == 0 or (h or 0) == 0 or
|
||||||
|
has_vid == 0 or
|
||||||
|
(network and not options.network) or
|
||||||
|
(albumart and not options.audio) or
|
||||||
|
(image and not albumart)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if rotate ~= nil and rotate % 180 == 90 then
|
||||||
|
display_w, display_h = h, w
|
||||||
|
end
|
||||||
|
|
||||||
|
local json, err = mp.utils.format_json({width=display_w, height=display_h, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||||
|
mp.commandv("script-message", "thumbfast-info", json)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_thumbnail_files()
|
||||||
|
os.remove(options.thumbnail)
|
||||||
|
os.remove(options.thumbnail..".bgra")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function spawn(time)
|
||||||
|
if disabled then return end
|
||||||
|
|
||||||
|
local path = mp.get_property("path")
|
||||||
|
if path == nil then return end
|
||||||
|
|
||||||
|
local open_filename = mp.get_property("stream-open-filename")
|
||||||
|
local ytdl = open_filename and network and path ~= open_filename
|
||||||
|
if ytdl then
|
||||||
|
path = open_filename
|
||||||
|
end
|
||||||
|
|
||||||
|
remove_thumbnail_files()
|
||||||
|
|
||||||
|
local vid = mp.get_property_number("vid")
|
||||||
|
has_vid = vid or 0
|
||||||
|
|
||||||
|
local args = {
|
||||||
|
mpv_path, path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
|
||||||
|
"--edition="..(mp.get_property_number("edition") or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio",
|
||||||
|
"--start="..time, "--hr-seek=no",
|
||||||
|
"--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 == "Windows" then
|
||||||
|
table.insert(args, "--input-ipc-server="..options.socket)
|
||||||
|
else
|
||||||
|
local client_script_path = options.socket..".run"
|
||||||
|
local file = io.open(client_script_path, "w+")
|
||||||
|
if file == nil then
|
||||||
|
mp.msg.error("client script write failed")
|
||||||
|
return
|
||||||
|
else
|
||||||
|
file:write(string.format(client_script, options.socket))
|
||||||
|
file:close()
|
||||||
|
subprocess({"chmod", "+x", client_script_path}, true)
|
||||||
|
table.insert(args, "--script="..client_script_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
spawned = true
|
||||||
|
spawn_waiting = true
|
||||||
|
|
||||||
|
subprocess(args, true,
|
||||||
|
function(success, result)
|
||||||
|
if spawn_waiting and (success == false or result.status ~= 0) then
|
||||||
|
mp.msg.error("mpv subprocess create failed")
|
||||||
|
end
|
||||||
|
spawned = false
|
||||||
|
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 file = nil
|
||||||
|
if os_name == "Windows" then
|
||||||
|
file = io.open("\\\\.\\pipe\\"..options.socket, "r+")
|
||||||
|
else
|
||||||
|
file = io.open(options.socket, "r+")
|
||||||
|
end
|
||||||
|
if file ~= nil then
|
||||||
|
file:seek("end")
|
||||||
|
file:write(command.."\n")
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw(w, h, script)
|
||||||
|
if not w or not show_thumbnail then return end
|
||||||
|
local display_w, display_h = w, h
|
||||||
|
if mp.get_property_number("video-params/rotate", 0) % 180 == 90 then
|
||||||
|
display_w, display_h = h, w
|
||||||
|
end
|
||||||
|
|
||||||
|
if x ~= nil then
|
||||||
|
mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", display_w, display_h, (4*display_w)})
|
||||||
|
elseif script then
|
||||||
|
local json, err = mp.utils.format_json({width=display_w, height=display_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 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(true)
|
||||||
|
seek_period_counter = 1
|
||||||
|
else
|
||||||
|
if seek_period_counter == 2 then
|
||||||
|
seek_timer:kill()
|
||||||
|
seek()
|
||||||
|
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(true)
|
||||||
|
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
|
||||||
|
if first_file then
|
||||||
|
request_seek()
|
||||||
|
first_file = false
|
||||||
|
end
|
||||||
|
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
|
||||||
|
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 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 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 clear()
|
||||||
|
file_timer:kill()
|
||||||
|
seek_timer:kill()
|
||||||
|
last_seek = 0
|
||||||
|
show_thumbnail = false
|
||||||
|
last_x = nil
|
||||||
|
last_y = nil
|
||||||
|
if script_name then return end
|
||||||
|
mp.command_native({"overlay-remove", options.overlay_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function watch_changes()
|
||||||
|
local old_w = effective_w
|
||||||
|
local old_h = effective_h
|
||||||
|
|
||||||
|
calc_dimensions()
|
||||||
|
|
||||||
|
local vf_reset = vf_string(filters_reset)
|
||||||
|
local rotate = mp.get_property_number("video-rotate", 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
|
||||||
|
run("quit")
|
||||||
|
clear()
|
||||||
|
spawned = false
|
||||||
|
spawn(last_seek_time or mp.get_property_number("time-pos", 0))
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
local watch_changes_debounce = debounce(watch_changes, 500)
|
||||||
|
|
||||||
|
local function sync_changes(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)
|
||||||
|
watch_changes_debounce()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function file_load()
|
||||||
|
clear()
|
||||||
|
real_w, real_h = nil, nil
|
||||||
|
last_real_w, last_real_h = nil, nil
|
||||||
|
last_seek_time = nil
|
||||||
|
if info_timer then
|
||||||
|
info_timer:kill()
|
||||||
|
info_timer = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
calc_dimensions()
|
||||||
|
info(effective_w, effective_h)
|
||||||
|
if disabled then return end
|
||||||
|
|
||||||
|
spawned = false
|
||||||
|
if options.spawn_first then
|
||||||
|
spawn(mp.get_property_number("time-pos", 0))
|
||||||
|
first_file = true
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
mp.observe_property("display-hidpi-scale", "native", watch_changes)
|
||||||
|
mp.observe_property("video-out-params", "native", watch_changes)
|
||||||
|
mp.observe_property("vf", "native", watch_changes_debounce)
|
||||||
|
mp.observe_property("vid", "native", sync_changes)
|
||||||
|
mp.observe_property("edition", "native", sync_changes)
|
||||||
|
|
||||||
|
mp.register_script_message("thumb", thumb)
|
||||||
|
mp.register_script_message("clear", clear)
|
||||||
|
|
||||||
|
mp.register_event("file-loaded", file_load)
|
||||||
|
mp.register_event("shutdown", shutdown)
|
|
@ -1,910 +0,0 @@
|
||||||
--[[
|
|
||||||
SOURCE_ https://github.com/deus0ww/mpv-conf/blob/master/scripts/Thumbnailer.lua
|
|
||||||
COMMIT_ 20211005 62fa158
|
|
||||||
|
|
||||||
搭配osc_lazy的缩略图脚本(1)/(2)
|
|
||||||
]]--
|
|
||||||
|
|
||||||
local ipairs,loadfile,pairs,pcall,tonumber,tostring = ipairs,loadfile,pairs,pcall,tonumber,tostring
|
|
||||||
local debug,io,math,os,string,table,utf8 = debug,io,math,os,string,table,utf8
|
|
||||||
local min,max,floor,ceil,huge = math.min,math.max,math.floor,math.ceil,math.huge
|
|
||||||
local mp = require 'mp'
|
|
||||||
local msg = require 'mp.msg'
|
|
||||||
local opt = require 'mp.options'
|
|
||||||
local utils = require 'mp.utils'
|
|
||||||
|
|
||||||
local script_name = mp.get_script_name()
|
|
||||||
|
|
||||||
local message = {
|
|
||||||
worker = {
|
|
||||||
registration = 'tn_worker_registration',
|
|
||||||
reset = 'tn_worker_reset',
|
|
||||||
queue = 'tn_worker_queue',
|
|
||||||
start = 'tn_worker_start',
|
|
||||||
progress = 'tn_worker_progress',
|
|
||||||
finish = 'tn_worker_finish',
|
|
||||||
},
|
|
||||||
osc = {
|
|
||||||
registration = 'tn_osc_registration',
|
|
||||||
reset = 'tn_osc_reset',
|
|
||||||
update = 'tn_osc_update',
|
|
||||||
finish = 'tn_osc_finish',
|
|
||||||
},
|
|
||||||
debug = 'Thumbnailer-debug',
|
|
||||||
|
|
||||||
manual_start = script_name .. '-start',
|
|
||||||
manual_stop = script_name .. '-stop',
|
|
||||||
manual_show = script_name .. '-show',
|
|
||||||
manual_hide = script_name .. '-hide',
|
|
||||||
toggle_gen = script_name .. '-toggle-gen',
|
|
||||||
toggle_osc = script_name .. '-toggle-osc',
|
|
||||||
double = script_name .. '-double',
|
|
||||||
shrink = script_name .. '-shrink',
|
|
||||||
enlarge = script_name .. '-enlarge',
|
|
||||||
auto_delete = script_name .. '-toggle-auto-delete',
|
|
||||||
|
|
||||||
queued = 1,
|
|
||||||
processing = 2,
|
|
||||||
ready = 3,
|
|
||||||
failed = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
-----------
|
|
||||||
-- Utils --
|
|
||||||
-----------
|
|
||||||
local OS_MAC, OS_WIN, OS_NIX = 'MAC', 'WIN', 'NIX'
|
|
||||||
local function get_os()
|
|
||||||
if jit and jit.os then
|
|
||||||
if jit.os == 'Windows' then return OS_WIN
|
|
||||||
elseif jit.os == 'OSX' then return OS_MAC
|
|
||||||
else return OS_NIX end
|
|
||||||
end
|
|
||||||
if (package.config:sub(1,1) ~= '/') then return OS_WIN end
|
|
||||||
local res = mp.command_native({ name = 'subprocess', args = {'uname', '-s'}, playback_only = false, capture_stdout = true, capture_stderr = true, })
|
|
||||||
return (res and res.stdout and res.stdout:lower():find('darwin') ~= nil) and OS_MAC or OS_NIX
|
|
||||||
end
|
|
||||||
local OPERATING_SYSTEM = get_os()
|
|
||||||
|
|
||||||
local function format_json(tab)
|
|
||||||
local json, err = utils.format_json(tab)
|
|
||||||
if err then msg.error('Formatting JSON failed:', err) end
|
|
||||||
if json then return json else return '' end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parse_json(json)
|
|
||||||
local tab, err = utils.parse_json(json, true)
|
|
||||||
if err then msg.error('Parsing JSON failed:', err) end
|
|
||||||
if tab then return tab else return {} end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_empty(...) -- Not for tables
|
|
||||||
if ... == nil then return true end
|
|
||||||
for _, v in ipairs({...}) do
|
|
||||||
if (v == nil) or (v == '') or (v == 0) then return true end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------
|
|
||||||
-- External Process and Filesystem --
|
|
||||||
-------------------------------------
|
|
||||||
local function subprocess_result(sub_success, result, mpv_error, subprocess_name, start_time)
|
|
||||||
local cmd_status, cmd_stdout, cmd_stderr, cmd_error, cmd_killed
|
|
||||||
if result then cmd_status, cmd_stdout, cmd_stderr, cmd_error, cmd_killed = result.status, result.stdout, result.stderr, result.error_string, result.killed_by_us end
|
|
||||||
local cmd_status_success, cmd_status_string, cmd_err_success, cmd_err_string, success
|
|
||||||
|
|
||||||
if cmd_status == 0 then cmd_status_success, cmd_status_string = true, 'ok'
|
|
||||||
elseif is_empty(cmd_status) then cmd_status_success, cmd_status_string = true, '_'
|
|
||||||
elseif cmd_status == 124 or cmd_status == 137 or cmd_status == 143 then -- timer: timed-out(124), killed(128+9), or terminated(128+15)
|
|
||||||
cmd_status_success, cmd_status_string = false, 'timed out'
|
|
||||||
else cmd_status_success, cmd_status_string = false, ('%d'):format(cmd_status) end
|
|
||||||
|
|
||||||
if is_empty(cmd_error) then cmd_err_success, cmd_err_string = true, '_'
|
|
||||||
elseif cmd_error == 'init' then cmd_err_success, cmd_err_string = false, 'failed to initialize'
|
|
||||||
elseif cmd_error == 'killed' then cmd_err_success, cmd_err_string = false, cmd_killed and 'killed by us' or 'killed, but not by us'
|
|
||||||
else cmd_err_success, cmd_err_string = false, cmd_error end
|
|
||||||
|
|
||||||
if is_empty(cmd_stdout) then cmd_stdout = '_' end
|
|
||||||
if is_empty(cmd_stderr) then cmd_stderr = '_' end
|
|
||||||
subprocess_name = subprocess_name or '_'
|
|
||||||
start_time = start_time or os.time()
|
|
||||||
success = (sub_success == nil or sub_success) and is_empty(mpv_error) and cmd_status_success and cmd_err_success
|
|
||||||
|
|
||||||
if success then msg.debug('Subprocess', subprocess_name, 'succeeded. | Status:', cmd_status_string, '| Time:', ('%ds'):format(os.difftime(os.time(), start_time)))
|
|
||||||
else msg.error('Subprocess', subprocess_name, 'failed. | Status:', cmd_status_string, '| MPV Error:', mpv_error or 'n/a',
|
|
||||||
'| Subprocess Error:', cmd_err_string, '| Stdout:', cmd_stdout, '| Stderr:', cmd_stderr) end
|
|
||||||
return success, cmd_status_string, cmd_err_string, cmd_stdout, cmd_stderr
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run_subprocess(command, name)
|
|
||||||
if not command then return false end
|
|
||||||
local subprocess_name, start_time = name or command[1], os.time()
|
|
||||||
-- msg.debug('Subprocess', subprocess_name, 'Starting...')
|
|
||||||
local result, mpv_error = mp.command_native( {name='subprocess', args=command, playback_only = false, capture_stdout = true, capture_stderr = true} )
|
|
||||||
local success, _, _, _ = subprocess_result(nil, result, mpv_error, subprocess_name, start_time)
|
|
||||||
return success
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run_subprocess_async(command, name)
|
|
||||||
if not command then return false end
|
|
||||||
local subprocess_name, start_time = name or command[1], os.time()
|
|
||||||
-- msg.debug('Subprocess', subprocess_name, 'Starting (async)...')
|
|
||||||
mp.command_native_async( {name='subprocess', args=command, playback_only = false, capture_stdout = true, capture_stderr = true}, function(s, r, e) subprocess_result(s, r, e, subprocess_name, start_time) end )
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function join_paths(...)
|
|
||||||
local sep = OPERATING_SYSTEM == OS_WIN and '\\' or '/'
|
|
||||||
local result = ''
|
|
||||||
for _, p in ipairs({...}) do
|
|
||||||
result = (result == '') and p or result .. sep .. p
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local function file_exists(path)
|
|
||||||
local file = io.open(path, 'rb')
|
|
||||||
if not file then return false end
|
|
||||||
local _, _, code = file:read(1)
|
|
||||||
file:close()
|
|
||||||
return code == nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function exec_exist(name, exec_path)
|
|
||||||
local delim = ':'
|
|
||||||
if OPERATING_SYSTEM == OS_WIN then delim, name = ';', name .. '.exe' end
|
|
||||||
local env_path = exec_path ~= '' and exec_path or ((os.getenv('PWD') or mp.get_property('working-directory')) .. delim .. os.getenv('PATH'))
|
|
||||||
msg.debug('PATH: ' .. env_path)
|
|
||||||
for path_dir in env_path:gmatch('[^'..delim..']+') do
|
|
||||||
if file_exists(join_paths(path_dir, name)) then return true end
|
|
||||||
end
|
|
||||||
msg.debug(name .. 'not found.')
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function dir_exist(path)
|
|
||||||
local ok, _, _ = os.rename(path .. '/', path .. '/')
|
|
||||||
if not ok then return false end
|
|
||||||
local file = io.open(join_paths(path, 'test'), 'w')
|
|
||||||
if file then
|
|
||||||
file:close()
|
|
||||||
return os.remove(join_paths(path, 'test'))
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function create_dir(path)
|
|
||||||
return dir_exist(path) or run_subprocess( OPERATING_SYSTEM == OS_WIN and {'cmd', '/e:on', '/c', 'mkdir', path} or {'mkdir', '-p', path} )
|
|
||||||
end
|
|
||||||
|
|
||||||
local function delete_dir(path)
|
|
||||||
if is_empty(path) then return end
|
|
||||||
msg.warn('Deleting Dir:', path)
|
|
||||||
return run_subprocess( OPERATING_SYSTEM == OS_WIN and {'cmd', '/e:on', '/c', 'rd', '/s', '/q', path} or {'rm', '-r', path} )
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
--------------------
|
|
||||||
-- Data Structure --
|
|
||||||
--------------------
|
|
||||||
local initialized = false
|
|
||||||
local default_cache_dir = join_paths(OPERATING_SYSTEM == OS_WIN and os.getenv('TEMP') or '/tmp/', script_name)
|
|
||||||
local saved_state, state
|
|
||||||
|
|
||||||
local user_opts = {
|
|
||||||
-- General
|
|
||||||
auto_gen = true, -- Auto generate thumbnails
|
|
||||||
auto_show = true, -- Show thumbnails by default
|
|
||||||
auto_delete = 0, -- Delete the thumbnail cache. Use at your own risk. 0=No, 1=On file close, 2=When quiting
|
|
||||||
start_delay = 2, -- Delay the start of the thumbnailer (seconds)
|
|
||||||
|
|
||||||
-- Paths
|
|
||||||
cache_dir = default_cache_dir, -- Note: Files are not cleaned afterward, by default
|
|
||||||
worker_script_path = '', -- Only needed if the script can't auto-locate the file to load more workers
|
|
||||||
exec_path = '', -- This is appended to PATH to search for mpv, ffmpeg, and other executables.
|
|
||||||
|
|
||||||
-- Thumbnail
|
|
||||||
dimension = 320, -- Max width and height before scaling
|
|
||||||
thumbnail_count = 120, -- Try to create this many thumbnails within the delta limits below
|
|
||||||
min_delta = 5, -- Minimum time between thumbnails (seconds)
|
|
||||||
max_delta = 60, -- Maximum time between thumbnails (seconds)
|
|
||||||
remote_delta_factor = 2, -- Multiply delta by this for remote sources
|
|
||||||
stream_delta_factor = 2, -- Multiply delta by this for streams (youtube, etc)
|
|
||||||
bitrate_delta_factor = 2, -- Multiply delta by this for high bitrate sources
|
|
||||||
bitrate_threshold = 8, -- The threshold to consider a source to be high bitrate (Mbps)
|
|
||||||
|
|
||||||
-- OSC
|
|
||||||
spacer = 2, -- Size of borders and spacings
|
|
||||||
show_progress = 1, -- Display the thumbnail-ing progress. (0=never, 1=while generating, 2=always)
|
|
||||||
centered = false, -- Center the thumbnail on screen
|
|
||||||
update_time = 0.5, -- Fastest time interval between updating the OSC with new thumbnails
|
|
||||||
|
|
||||||
-- Worker
|
|
||||||
max_workers = 3, -- Number of active workers. Must have at least one copy of the worker script alongside this script.
|
|
||||||
worker_remote_factor = 0.5, -- Multiply max_workers by this for remote streams or when MPV enables cache
|
|
||||||
worker_bitrate_factor = 0.5, -- Multiply max_workers by this for high bitrate sources. Set threshold with bitrate_threshold
|
|
||||||
worker_delay = 0.5, -- Delay between starting workers (seconds)
|
|
||||||
worker_timeout = 4, -- Timeout before killing encoder. 0=No Timeout (Linux or Mac w/ coreutils installed only). Standardized at 720p and linearly scaled with resolution.
|
|
||||||
accurate_seek = false, -- Use accurate timing instead of closest keyframe for thumbnails. (Slower)
|
|
||||||
use_ffmpeg = false, -- Use FFMPEG when appropriate. FFMPEG must be in PATH or in the MPV directory
|
|
||||||
prefer_ffmpeg = false, -- Use FFMPEG when available
|
|
||||||
ffmpeg_threads = 8, -- Limit FFMPEG/MPV LAVC threads per worker. Also limits filter and output threads for FFMPEG.
|
|
||||||
ffmpeg_scaler = 'bilinear', -- ffmpeg软件缩放算法 https://ffmpeg.org/ffmpeg-scaler.html
|
|
||||||
mpv_scaler = 'bilinear', -- mpv软件缩放算法
|
|
||||||
mpv_hwdec = 'no', -- mpv硬解码
|
|
||||||
ffmpeg_hwaccel = 'none', -- ffmpeg硬解码
|
|
||||||
ffmpeg_hwaccel_device = '0', -- ffmpeg硬解码设备
|
|
||||||
}
|
|
||||||
|
|
||||||
local thumbnails, thumbnails_new,thumbnails_new_count
|
|
||||||
|
|
||||||
local function reset_thumbnails()
|
|
||||||
thumbnails = {}
|
|
||||||
thumbnails_new = {}
|
|
||||||
thumbnails_new_count = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
------------
|
|
||||||
-- Worker --
|
|
||||||
------------
|
|
||||||
local workers, workers_indexed, workers_timers = {}, {}, {}
|
|
||||||
local workers_started, workers_finished, workers_finished_indexed, timer_start, timer_total
|
|
||||||
|
|
||||||
local function workers_reset()
|
|
||||||
for _, timer in ipairs(workers_timers) do timer:kill() end
|
|
||||||
workers_timers = {}
|
|
||||||
workers_started = false
|
|
||||||
workers_finished = {}
|
|
||||||
workers_finished_indexed = {}
|
|
||||||
timer_start = 0
|
|
||||||
timer_total = 0
|
|
||||||
for _, worker in ipairs(workers_indexed) do
|
|
||||||
mp.command_native({'script-message-to', worker, message.worker.reset})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function worker_set_options()
|
|
||||||
return {
|
|
||||||
encoder = (not state.is_remote and user_opts.use_ffmpeg and exec_exist('ffmpeg', user_opts.exec_path)) and 'ffmpeg' or 'mpv',
|
|
||||||
exec_path = user_opts.exec_path,
|
|
||||||
worker_timeout = state.worker_timeout,
|
|
||||||
accurate_seek = user_opts.accurate_seek,
|
|
||||||
use_ffmpeg = user_opts.use_ffmpeg,
|
|
||||||
ffmpeg_threads = user_opts.ffmpeg_threads,
|
|
||||||
ffmpeg_scaler = user_opts.ffmpeg_scaler,
|
|
||||||
mpv_scaler = user_opts.mpv_scaler,
|
|
||||||
mpv_hwdec = user_opts.mpv_hwdec,
|
|
||||||
ffmpeg_hwaccel = user_opts.ffmpeg_hwaccel,
|
|
||||||
ffmpeg_hwaccel_device = user_opts.ffmpeg_hwaccel_device,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function workers_queue()
|
|
||||||
local worker_data = {
|
|
||||||
state = state,
|
|
||||||
worker_options = worker_set_options(),
|
|
||||||
}
|
|
||||||
local start_time_index = 0
|
|
||||||
for i, worker in ipairs(workers_indexed) do
|
|
||||||
if i > state.max_workers then break end
|
|
||||||
worker_data.start_time_index = start_time_index
|
|
||||||
mp.command_native_async({'script-message-to', worker, message.worker.queue, format_json(worker_data)}, function() end)
|
|
||||||
start_time_index = ceil(i * state.tn_per_worker)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function workers_start()
|
|
||||||
timer_start = os.time()
|
|
||||||
if state.cache_dir and state.cache_dir ~= '' then os.remove(join_paths(state.cache_dir, 'stop')) end
|
|
||||||
for i, worker in ipairs(workers_indexed) do
|
|
||||||
if i > state.max_workers then break end
|
|
||||||
table.insert(workers_timers, mp.add_timeout( user_opts.worker_delay * i^0.8, function() mp.command_native({'script-message-to', worker, message.worker.start}) end))
|
|
||||||
end
|
|
||||||
workers_started = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function workers_stop()
|
|
||||||
if state and state.cache_dir and state.cache_dir ~= '' then
|
|
||||||
local file = io.open(join_paths(state.cache_dir, 'stop'), 'w')
|
|
||||||
if file then file:close() end
|
|
||||||
end
|
|
||||||
if timer_total and timer_start then timer_total = timer_total + os.difftime(os.time(), timer_start) end
|
|
||||||
timer_start = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
local function workers_are_stopped()
|
|
||||||
if not initialized or not workers_started then return true end
|
|
||||||
local file = io.open(join_paths(state.cache_dir, 'stop'), 'r')
|
|
||||||
if not file then return false end
|
|
||||||
file:close()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
---------
|
|
||||||
-- OSC --
|
|
||||||
---------
|
|
||||||
local osc_name, osc_opts, osc_stats, osc_visible, osc_last_update
|
|
||||||
|
|
||||||
local function osc_reset_stats()
|
|
||||||
osc_stats = {
|
|
||||||
queued = 0,
|
|
||||||
processing = 0,
|
|
||||||
ready = 0,
|
|
||||||
failed = 0,
|
|
||||||
total = 0,
|
|
||||||
total_expected = 0,
|
|
||||||
percent = 0,
|
|
||||||
timer = 0,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function osc_reset()
|
|
||||||
osc_reset_stats()
|
|
||||||
osc_last_update = 0
|
|
||||||
osc_visible = nil
|
|
||||||
if osc_name then mp.command_native({'script-message-to', osc_name, message.osc.reset}) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function osc_set_options(is_visible)
|
|
||||||
osc_visible = (is_visible == nil) and user_opts.auto_show or is_visible
|
|
||||||
return {
|
|
||||||
spacer = user_opts.spacer,
|
|
||||||
show_progress = user_opts.show_progress,
|
|
||||||
scale = state.scale,
|
|
||||||
centered = user_opts.centered,
|
|
||||||
visible = osc_visible,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function osc_update(ustate, uoptions, uthumbnails)
|
|
||||||
if is_empty(osc_name) then return end
|
|
||||||
local osc_data = {
|
|
||||||
state = ustate,
|
|
||||||
osc_options = uoptions,
|
|
||||||
thumbnails = uthumbnails,
|
|
||||||
}
|
|
||||||
if osc_data.thumbnails then
|
|
||||||
osc_stats.timer = timer_start == 0 and timer_total or (timer_total + os.difftime(os.time(), timer_start))
|
|
||||||
osc_stats.total_expected = floor(state.duration / state.delta) + 1
|
|
||||||
osc_data.osc_stats = osc_stats
|
|
||||||
else
|
|
||||||
osc_data.osc_stats = nil
|
|
||||||
end
|
|
||||||
mp.command_native_async({'script-message-to', osc_name, message.osc.update, format_json(osc_data)}, function() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function osc_delta_update(flush)
|
|
||||||
local time_since_last_update = os.clock() - osc_last_update
|
|
||||||
if thumbnails_new_count <= 0 then return end
|
|
||||||
if (time_since_last_update >= (4.00 * user_opts.update_time)) or
|
|
||||||
(time_since_last_update >= (2.00 * user_opts.update_time) and thumbnails_new_count >= state.worker_buffer) or
|
|
||||||
(time_since_last_update >= (1.00 * user_opts.update_time) and thumbnails_new_count >= state.osc_buffer) or
|
|
||||||
thumbnails_new_count >= floor(state.tn_per_worker - 1) or
|
|
||||||
flush
|
|
||||||
then
|
|
||||||
osc_update(nil, nil, thumbnails_new)
|
|
||||||
thumbnails_new = {}
|
|
||||||
thumbnails_new_count = 0
|
|
||||||
osc_last_update = os.clock()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local osc_full_update_timer = mp.add_periodic_timer((4.00 * user_opts.update_time), function() osc_update(nil, nil, thumbnails) end)
|
|
||||||
osc_full_update_timer:kill()
|
|
||||||
local osc_delta_update_timer = mp.add_periodic_timer((0.25 * user_opts.update_time), function() osc_delta_update() end)
|
|
||||||
osc_delta_update_timer:kill()
|
|
||||||
|
|
||||||
local count_existing = {
|
|
||||||
[message.queued] = function() osc_stats.queued = osc_stats.queued - 1 end,
|
|
||||||
[message.processing] = function() osc_stats.processing = osc_stats.processing - 1 end,
|
|
||||||
[message.failed] = function() osc_stats.failed = osc_stats.failed - 1 end,
|
|
||||||
[message.ready] = function() osc_stats.ready = osc_stats.ready - 1 end,
|
|
||||||
}
|
|
||||||
local count_new = {
|
|
||||||
[message.queued] = function() osc_stats.queued = osc_stats.queued + 1 end,
|
|
||||||
[message.processing] = function() osc_stats.processing = osc_stats.processing + 1 end,
|
|
||||||
[message.failed] = function() osc_stats.failed = osc_stats.failed + 1 end,
|
|
||||||
[message.ready] = function() osc_stats.ready = osc_stats.ready + 1 end,
|
|
||||||
}
|
|
||||||
|
|
||||||
local function osc_update_count(time_string, status)
|
|
||||||
local osc_stats, existing = osc_stats, thumbnails[time_string]
|
|
||||||
if existing then count_existing[existing]() else osc_stats.total = osc_stats.total + 1 end
|
|
||||||
if status then count_new[status]() else osc_stats.total = osc_stats.total - 1 end
|
|
||||||
osc_stats.percent = osc_stats.total > 0 and (osc_stats.failed + osc_stats.ready) / osc_stats.total or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
----------------
|
|
||||||
-- Core Logic --
|
|
||||||
----------------
|
|
||||||
local stop_conditions
|
|
||||||
|
|
||||||
local worker_script_path
|
|
||||||
|
|
||||||
local function create_workers()
|
|
||||||
local workers_requested = (state and state.max_workers) and state.max_workers or user_opts.max_workers
|
|
||||||
msg.debug('Workers Available:', #workers_indexed)
|
|
||||||
msg.debug('Workers Requested:', workers_requested)
|
|
||||||
msg.debug('worker_script_path:', worker_script_path)
|
|
||||||
local missing_workers = workers_requested - #workers_indexed
|
|
||||||
if missing_workers > 0 and worker_script_path ~= nil and worker_script_path ~= '' then
|
|
||||||
for _ = 1, missing_workers do
|
|
||||||
-- msg.debug('Recruiting Worker...')
|
|
||||||
mp.command_native({'load-script', worker_script_path})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hash_string(filepath, filename)
|
|
||||||
if OPERATING_SYSTEM == OS_WIN then return filename end
|
|
||||||
local command
|
|
||||||
if exec_exist('shasum', user_opts.exec_path) then command = {user_opts.exec_path .. 'shasum', '-a', '256', filepath}
|
|
||||||
elseif exec_exist('gsha256sum', user_opts.exec_path) then command = {user_opts.exec_path .. 'gsha256sum', filepath}
|
|
||||||
elseif exec_exist('sha256sum', user_opts.exec_path) then command = {user_opts.exec_path .. 'sha256sum', filepath} end
|
|
||||||
if not command then return filename end -- checksum command unavailable
|
|
||||||
local res = mp.command_native({name = 'subprocess', args = command, playback_only = false, capture_stdout = true, capture_stderr = true,})
|
|
||||||
return (res and res.stdout) and res.stdout or filename
|
|
||||||
end
|
|
||||||
|
|
||||||
local function create_ouput_dir(filepath, filename, dimension, rotate)
|
|
||||||
local name, basepath, success, max_char = '', '', false, 64
|
|
||||||
|
|
||||||
-- Try path without two-bytes UTF-8 char and only alphanumerics
|
|
||||||
name = filename:gsub('[\192-\255][\128-\191]*', ''):gsub('[^%w]+', ''):sub(1, max_char)
|
|
||||||
msg.debug('Creating Output Dir: Trying', name)
|
|
||||||
if not is_empty(name) then
|
|
||||||
basepath = join_paths(user_opts.cache_dir, name)
|
|
||||||
success = create_dir(basepath)
|
|
||||||
end
|
|
||||||
|
|
||||||
if not success then -- Try hashed path
|
|
||||||
name = hash_string(filepath, filename):sub(1, max_char)
|
|
||||||
msg.debug('Creating Output Dir: Trying', name)
|
|
||||||
basepath = join_paths(user_opts.cache_dir, name)
|
|
||||||
success = create_dir(basepath)
|
|
||||||
end
|
|
||||||
|
|
||||||
if not success then -- Failed
|
|
||||||
msg.error('Creating Output Dir: Failed', name)
|
|
||||||
return {basepath = nil, fullpath = nil}
|
|
||||||
end
|
|
||||||
msg.debug('Creating Output Dir: Using ', name)
|
|
||||||
|
|
||||||
local fullpath = join_paths(basepath, dimension, rotate)
|
|
||||||
if not create_dir(fullpath) then return { basepath = nil, fullpath = nil } end
|
|
||||||
return {basepath = basepath, fullpath = fullpath}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function calculate_timing(is_remote)
|
|
||||||
local duration, file_size = mp.get_property_native('duration', 0), mp.get_property_native('file-size', 0)
|
|
||||||
if duration == 0 then return { duration = 0, delta = huge, high_bitrate = false } end
|
|
||||||
local delta_target = duration / (user_opts.thumbnail_count - 1)
|
|
||||||
local saved_factor = saved_state.delta_factor or 1
|
|
||||||
local remote_factor = is_remote and user_opts.remote_delta_factor or 1
|
|
||||||
local stream_factor = file_size == 0 and user_opts.stream_delta_factor or 1
|
|
||||||
local high_bitrate = (file_size / duration) >= (user_opts.bitrate_threshold * 131072)
|
|
||||||
local bitrate_factor = high_bitrate and user_opts.bitrate_delta_factor or 1
|
|
||||||
local delta = max(user_opts.min_delta, min(user_opts.max_delta, delta_target)) * saved_factor * remote_factor * stream_factor * bitrate_factor
|
|
||||||
return { duration = duration, delta = delta, high_bitrate = high_bitrate }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function calculate_scale()
|
|
||||||
local hidpi_scale = mp.get_property_native('display-hidpi-scale', 1.0)
|
|
||||||
if osc_opts then
|
|
||||||
local scale = (saved_state.fullscreen ~= nil and saved_state.fullscreen) and osc_opts.scalefullscreen or osc_opts.scalewindowed
|
|
||||||
return scale * hidpi_scale
|
|
||||||
else
|
|
||||||
return hidpi_scale
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function calculate_geometry(scale)
|
|
||||||
local geometry = { dimension = 0, width = 0, height = 0, scale = 0, rotate = 0, is_rotated = false }
|
|
||||||
local video_params = saved_state.video_params
|
|
||||||
local dimension = floor(saved_state.size_factor * user_opts.dimension * scale + 0.5)
|
|
||||||
if not video_params or is_empty(video_params.dw, video_params.dh) or dimension <= 0 then return geometry end
|
|
||||||
local width, height = dimension, dimension
|
|
||||||
if video_params.dw > video_params.dh then
|
|
||||||
height = floor(width * video_params.dh / video_params.dw + 0.5)
|
|
||||||
else
|
|
||||||
width = floor(height * video_params.dw / video_params.dh + 0.5)
|
|
||||||
end
|
|
||||||
geometry.dimension, geometry.width, geometry.height, geometry.dw, geometry.dh = dimension, width, height, video_params.dw, video_params.dh
|
|
||||||
if not video_params.rotate then return geometry end
|
|
||||||
geometry.rotate = (video_params.rotate - saved_state.initial_rotate) % 360
|
|
||||||
geometry.is_rotated = not ((((video_params.rotate - saved_state.initial_rotate) % 180) ~= 0) == saved_state.meta_rotated) --xor
|
|
||||||
return geometry
|
|
||||||
end
|
|
||||||
|
|
||||||
local function calculate_worker_limit(duration, delta, is_remote, is_high_bitrate)
|
|
||||||
local remote_factor = is_remote and user_opts.worker_remote_factor or 1
|
|
||||||
local bitrate_factor = is_high_bitrate and user_opts.worker_bitrate_factor or 1
|
|
||||||
return max(floor(min(user_opts.max_workers, duration / delta) * remote_factor * bitrate_factor), 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function calculate_worker_timeout(width, height, is_remote, is_high_bitrate)
|
|
||||||
if user_opts.worker_timeout == 0 then return 0 end
|
|
||||||
local worker_timeout = ((width * height) / 921600) * user_opts.worker_timeout
|
|
||||||
if is_remote then worker_timeout = worker_timeout * 2 end
|
|
||||||
if is_high_bitrate then worker_timeout = worker_timeout * 2 end
|
|
||||||
return ceil(worker_timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function has_video()
|
|
||||||
local track_list = mp.get_property_native('track-list', {})
|
|
||||||
if is_empty(track_list) then return false end
|
|
||||||
for _, track in ipairs(track_list) do
|
|
||||||
if track.type == 'video' and not track.external and not track.albumart then return true end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function state_init()
|
|
||||||
local input_fullpath = saved_state.input_fullpath
|
|
||||||
local input_filename = saved_state.input_filename
|
|
||||||
local cache_format = '%.5d'
|
|
||||||
local cache_extension = '.bgra'
|
|
||||||
local is_remote = (input_fullpath:find('://') ~= nil) and mp.get_property_native('demuxer-via-network', false)
|
|
||||||
local timing = calculate_timing(is_remote)
|
|
||||||
local scale = calculate_scale()
|
|
||||||
local geometry = calculate_geometry(scale)
|
|
||||||
local meta_rotated = saved_state.meta_rotated
|
|
||||||
local cache_dir = create_ouput_dir(input_fullpath, input_filename, geometry.dimension, geometry.rotate)
|
|
||||||
local worker_timeout = calculate_worker_timeout(geometry.width, geometry.height, is_remote, timing.is_high_bitrate)
|
|
||||||
local max_workers = calculate_worker_limit(timing.duration, timing.delta, is_remote, timing.is_high_bitrate)
|
|
||||||
local tn_max = floor(timing.duration / timing.delta) + 1
|
|
||||||
local tn_per_worker = tn_max / max_workers
|
|
||||||
local worker_buffer = 2
|
|
||||||
local osc_buffer = worker_buffer * max_workers
|
|
||||||
|
|
||||||
-- Global State
|
|
||||||
state = {
|
|
||||||
cache_dir = cache_dir.fullpath,
|
|
||||||
cache_dir_base = cache_dir.basepath,
|
|
||||||
cache_format = cache_format,
|
|
||||||
cache_extension = cache_extension,
|
|
||||||
input_fullpath = input_fullpath,
|
|
||||||
input_filename = input_filename,
|
|
||||||
duration = timing.duration,
|
|
||||||
delta = timing.delta,
|
|
||||||
width = geometry.width,
|
|
||||||
height = geometry.height,
|
|
||||||
dw = geometry.dw,
|
|
||||||
dh = geometry.dh,
|
|
||||||
scale = scale,
|
|
||||||
rotate = geometry.rotate,
|
|
||||||
meta_rotated = meta_rotated,
|
|
||||||
is_rotated = geometry.is_rotated,
|
|
||||||
is_remote = is_remote,
|
|
||||||
tn_max = tn_max,
|
|
||||||
tn_per_worker = tn_per_worker,
|
|
||||||
max_workers = max_workers,
|
|
||||||
worker_timeout = worker_timeout,
|
|
||||||
worker_buffer = worker_buffer,
|
|
||||||
osc_buffer = osc_buffer,
|
|
||||||
}
|
|
||||||
stop_conditions = {
|
|
||||||
is_seekable = mp.get_property_native('seekable', true),
|
|
||||||
has_video = has_video(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_empty(worker_script_path) then worker_script_path = user_opts.worker_script_path end
|
|
||||||
create_workers()
|
|
||||||
initialized = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function saved_state_init()
|
|
||||||
local rotate = mp.get_property_native('video-params/rotate', 0)
|
|
||||||
saved_state = {
|
|
||||||
input_fullpath = mp.get_property_native('path', ''),
|
|
||||||
input_filename = mp.get_property_native('filename/no-ext', ''):gsub('watch%?v=', ''),
|
|
||||||
meta_rotated = ((rotate % 180) ~= 0),
|
|
||||||
initial_rotate = rotate % 360,
|
|
||||||
delta_factor = 1.0,
|
|
||||||
size_factor = 1.0,
|
|
||||||
fullscreen = mp.get_property_native("fullscreen", false)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_thumbnailable()
|
|
||||||
-- Must catch all cases that's not thumbnail-able and anything else that may crash the OSC.
|
|
||||||
if not (state and stop_conditions) then return false end
|
|
||||||
for key, value in pairs(state) do
|
|
||||||
if key == 'rotate' and value then goto continue end
|
|
||||||
if key == 'worker_buffer' and value then goto continue end
|
|
||||||
if key == 'osc_buffer' and value then goto continue end
|
|
||||||
if key == 'worker_timeout' and value then goto continue end
|
|
||||||
if is_empty(value) then
|
|
||||||
msg.warn('Stopping - State Incomplete:', key, value)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
::continue::
|
|
||||||
end
|
|
||||||
for condition, value in pairs(stop_conditions) do
|
|
||||||
if not value then
|
|
||||||
msg.warn('Stopping:', condition, value)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local auto_delete = nil
|
|
||||||
|
|
||||||
local function delete_cache_dir()
|
|
||||||
if auto_delete == nil then auto_delete = user_opts.auto_delete end
|
|
||||||
if auto_delete > 0 then
|
|
||||||
local path = user_opts.cache_dir
|
|
||||||
msg.debug('Clearing Cache on Shutdown:', path)
|
|
||||||
if path:len() < 16 then return end
|
|
||||||
delete_dir(path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function delete_cache_subdir()
|
|
||||||
if not state then return end
|
|
||||||
if auto_delete == nil then auto_delete = user_opts.auto_delete end
|
|
||||||
if auto_delete == 1 then
|
|
||||||
local path = state.cache_dir_base
|
|
||||||
msg.debug('Clearing Cache for File:', path)
|
|
||||||
if path:len() < 16 then return end
|
|
||||||
delete_dir(path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reset_all(keep_saved, keep_osc_data)
|
|
||||||
initialized = false
|
|
||||||
osc_full_update_timer:kill()
|
|
||||||
osc_delta_update_timer:kill()
|
|
||||||
workers_stop()
|
|
||||||
workers_reset()
|
|
||||||
reset_thumbnails()
|
|
||||||
opt.read_options(user_opts, script_name)
|
|
||||||
if not keep_saved or not saved_state then saved_state_init() end
|
|
||||||
if not keep_osc_data then osc_reset() else osc_reset_stats() end
|
|
||||||
msg.debug('Reset (' .. (keep_saved and 'Soft' or 'Hard') .. ', ' .. (keep_osc_data and 'OSC-Partial' or 'OSC-All') .. ')')
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run_generation(paused)
|
|
||||||
if not initialized or not is_thumbnailable() then return end
|
|
||||||
if #workers_indexed < state.max_workers or not osc_name or not osc_opts then
|
|
||||||
mp.add_timeout(0.1, function() run_generation(paused) end)
|
|
||||||
else
|
|
||||||
workers_queue()
|
|
||||||
if not paused then
|
|
||||||
workers_start()
|
|
||||||
osc_delta_update_timer:resume()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function stop()
|
|
||||||
workers_stop()
|
|
||||||
osc_delta_update_timer:kill()
|
|
||||||
osc_delta_update(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function start(paused)
|
|
||||||
if not initialized then mp.add_timeout(user_opts.start_delay, function()
|
|
||||||
state_init()
|
|
||||||
start(paused)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
if is_thumbnailable() then
|
|
||||||
osc_update(state, osc_set_options(osc_visible), nil)
|
|
||||||
run_generation(paused)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function osc_set_visibility(is_visible)
|
|
||||||
if is_visible and not initialized then start(true) end
|
|
||||||
if osc_name then osc_update(nil, osc_set_options(is_visible), nil) end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
--------------
|
|
||||||
-- Bindings --
|
|
||||||
--------------
|
|
||||||
-- Binding - Manual Start
|
|
||||||
mp.register_script_message(message.manual_start, start)
|
|
||||||
|
|
||||||
-- Binding - Manual Stop
|
|
||||||
mp.register_script_message(message.manual_stop, stop)
|
|
||||||
|
|
||||||
-- Binding - Toggle Generation
|
|
||||||
mp.register_script_message(message.toggle_gen, function() if workers_are_stopped() then start() else stop() end end)
|
|
||||||
|
|
||||||
-- Binding - Manual Show OSC
|
|
||||||
mp.register_script_message(message.manual_show, function() osc_set_visibility(true) end)
|
|
||||||
|
|
||||||
-- Binding - Manual Hide OSC
|
|
||||||
mp.register_script_message(message.manual_hide, function() osc_set_visibility(false) end)
|
|
||||||
|
|
||||||
-- Binding - Toggle Visibility
|
|
||||||
mp.register_script_message(message.toggle_osc, function() osc_set_visibility(not osc_visible) end)
|
|
||||||
|
|
||||||
-- Binding - Double Frequency
|
|
||||||
mp.register_script_message(message.double, function()
|
|
||||||
if not initialized or not saved_state or not saved_state.delta_factor then return end
|
|
||||||
local target = max(0.25, saved_state.delta_factor * 0.5)
|
|
||||||
if tostring(saved_state.delta_factor) ~= tostring(target) then
|
|
||||||
saved_state.delta_factor = target
|
|
||||||
reset_all(true, true)
|
|
||||||
start()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local function resize(target)
|
|
||||||
if tostring(saved_state.size_factor) ~= tostring(target) then
|
|
||||||
saved_state.size_factor = target
|
|
||||||
reset_all(true)
|
|
||||||
start()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Binding - Shrink
|
|
||||||
mp.register_script_message(message.shrink, function()
|
|
||||||
if initialized and saved_state and saved_state.size_factor then resize(max(0.2, saved_state.size_factor - 0.2)) end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Binding - Enlarge
|
|
||||||
mp.register_script_message(message.enlarge, function()
|
|
||||||
if initialized and saved_state and saved_state.size_factor then resize(min(2.0, saved_state.size_factor + 0.2)) end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Binding - Toggle Auto Delete
|
|
||||||
local auto_delete_message = { [0] = '', [1] = ' (on file close)', [2] = ' (on quit)' }
|
|
||||||
mp.register_script_message(message.auto_delete, function()
|
|
||||||
if auto_delete == nil then auto_delete = user_opts.auto_delete end
|
|
||||||
auto_delete = (auto_delete + 1) % 3
|
|
||||||
mp.osd_message( (auto_delete > 0 and '■' or '□') .. ' Thumbnail Auto Delete' .. auto_delete_message[auto_delete])
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
------------
|
|
||||||
-- Events --
|
|
||||||
------------
|
|
||||||
-- On Video Params Change
|
|
||||||
mp.observe_property('video-params', 'native', function(_, video_params)
|
|
||||||
if not video_params or is_empty(video_params.dw, video_params.dh) then return end
|
|
||||||
if not saved_state or (saved_state.input_fullpath ~= mp.get_property_native('path', '')) then
|
|
||||||
delete_cache_subdir()
|
|
||||||
reset_all()
|
|
||||||
saved_state.video_params = video_params
|
|
||||||
start(not user_opts.auto_gen)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if initialized and saved_state and saved_state.video_params and saved_state.video_params.rotate and video_params.rotate and tostring(saved_state.video_params.rotate) ~= tostring(video_params.rotate) then
|
|
||||||
reset_all(true)
|
|
||||||
saved_state.video_params = video_params
|
|
||||||
start()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- On Fullscreen Change
|
|
||||||
mp.observe_property('fullscreen', 'native', function(_, fullscreen)
|
|
||||||
if (fullscreen == nil) or (not osc_opts or osc_opts.scalewindowed == osc_opts.scalefullscreen) then return end
|
|
||||||
if initialized and saved_state then
|
|
||||||
reset_all(true)
|
|
||||||
saved_state.fullscreen = fullscreen
|
|
||||||
start()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- On file close
|
|
||||||
mp.register_event('end-file', delete_cache_subdir)
|
|
||||||
|
|
||||||
-- On Shutdown
|
|
||||||
mp.register_event('shutdown', delete_cache_dir)
|
|
||||||
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
-- Workers & OSC --
|
|
||||||
-------------------
|
|
||||||
-- Listen for OSC Registration
|
|
||||||
mp.register_script_message(message.osc.registration, function(json)
|
|
||||||
local osc_reg = parse_json(json)
|
|
||||||
if osc_reg and osc_reg.script_name and osc_reg.osc_opts and not (osc_name and osc_opts) then
|
|
||||||
osc_name = osc_reg.script_name
|
|
||||||
osc_opts = osc_reg.osc_opts
|
|
||||||
msg.debug('OSC Registered:', utils.to_string(osc_reg))
|
|
||||||
else
|
|
||||||
msg.warn('OSC Not Registered:', utils.to_string(osc_reg))
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Listen for OSC Finish
|
|
||||||
mp.register_script_message(message.osc.finish, function()
|
|
||||||
msg.debug('OSC: Finished.')
|
|
||||||
osc_delta_update_timer:kill()
|
|
||||||
osc_full_update_timer:kill()
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Listen for Worker Registration
|
|
||||||
mp.register_script_message(message.worker.registration, function(new_reg)
|
|
||||||
local worker_reg = parse_json(new_reg)
|
|
||||||
if worker_reg.name and not workers[worker_reg.name] then
|
|
||||||
workers[worker_reg.name] = true
|
|
||||||
workers_indexed[#workers_indexed + 1] = worker_reg.name
|
|
||||||
if (is_empty(worker_script_path)) and not is_empty(worker_reg.script_path) then
|
|
||||||
worker_script_path = worker_reg.script_path
|
|
||||||
create_workers()
|
|
||||||
msg.debug('Worker Script Path Recieved:', worker_script_path)
|
|
||||||
end
|
|
||||||
msg.debug('Worker Registered:', worker_reg.name)
|
|
||||||
else
|
|
||||||
msg.warn('Worker Not Registered:', worker_reg.name)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Listen for Worker Progress Report
|
|
||||||
mp.register_script_message(message.worker.progress, function(json)
|
|
||||||
local new_progress = parse_json(json)
|
|
||||||
if new_progress.input_filename ~= state.input_filename then return end
|
|
||||||
for time_string, new_status in pairs(new_progress.thumbnail_map) do
|
|
||||||
if thumbnails_new[time_string] ~= new_status then
|
|
||||||
thumbnails_new[time_string] = new_status
|
|
||||||
thumbnails_new_count = thumbnails_new_count + 1
|
|
||||||
end
|
|
||||||
osc_update_count(time_string, new_status)
|
|
||||||
thumbnails[time_string] = new_status
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Listen for Worker Finish
|
|
||||||
mp.register_script_message(message.worker.finish, function(json)
|
|
||||||
local worker_stats = parse_json(json)
|
|
||||||
if worker_stats.name and worker_stats.queued == 0 and not workers_finished[worker_stats.name] then
|
|
||||||
workers_finished[worker_stats.name] = true
|
|
||||||
workers_finished_indexed[#workers_finished_indexed + 1] = worker_stats.name
|
|
||||||
msg.debug('Worker Finished:', worker_stats.name, json)
|
|
||||||
else
|
|
||||||
msg.warn('Worker Finished (uncounted):', worker_stats.name, json)
|
|
||||||
end
|
|
||||||
if #workers_finished_indexed >= state.max_workers then
|
|
||||||
msg.debug('All Workers: Finished.')
|
|
||||||
osc_delta_update_timer:kill()
|
|
||||||
osc_delta_update(true)
|
|
||||||
osc_full_update_timer:resume()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
|
||||||
-----------
|
|
||||||
-- Debug --
|
|
||||||
-----------
|
|
||||||
mp.register_script_message(message.debug, function()
|
|
||||||
msg.info('============')
|
|
||||||
msg.info('Video Stats:')
|
|
||||||
msg.info('============')
|
|
||||||
msg.info('video-params', utils.to_string(mp.get_property_native('video-params', {})))
|
|
||||||
msg.info('video-dec-params', utils.to_string(mp.get_property_native('video-dec-params', {})))
|
|
||||||
msg.info('video-out-params', utils.to_string(mp.get_property_native('video-out-params', {})))
|
|
||||||
msg.info('track-list', utils.to_string(mp.get_property_native('track-list', {})))
|
|
||||||
msg.info('duration', mp.get_property_native('duration', 0))
|
|
||||||
msg.info('file-size', mp.get_property_native('file-size', 0))
|
|
||||||
msg.info('auto_delete', auto_delete)
|
|
||||||
|
|
||||||
msg.info('============================')
|
|
||||||
msg.info('Thumbnailer Internal States:')
|
|
||||||
msg.info('============================')
|
|
||||||
msg.info('saved_state:', state and utils.to_string(saved_state) or 'nil')
|
|
||||||
msg.info('state:', state and utils.to_string(state) or 'nil')
|
|
||||||
msg.info('stop_conditions:', stop_conditions and utils.to_string(stop_conditions) or 'nil')
|
|
||||||
msg.info('user_opts:', user_opts and utils.to_string(user_opts) or 'nil')
|
|
||||||
msg.info('worker_script_path:', worker_script_path and worker_script_path or 'nil')
|
|
||||||
msg.info('osc_name:', osc_name and osc_name or 'nil')
|
|
||||||
msg.info('osc_stats:', osc_stats and utils.to_string(osc_stats) or 'nil')
|
|
||||||
msg.info('thumbnails:', thumbnails and utils.to_string(thumbnails) or 'nil')
|
|
||||||
msg.info('thumbnails_new:', thumbnails_new and utils.to_string(thumbnails_new) or 'nil')
|
|
||||||
msg.info('workers:', workers and utils.to_string(workers) or 'nil')
|
|
||||||
msg.info('workers_indexed:', workers_indexed and utils.to_string(workers_indexed) or 'nil')
|
|
||||||
msg.info('workers_finished:', workers_finished and utils.to_string(workers_finished) or 'nil')
|
|
||||||
msg.info('workers_finished_indexed:', workers_finished_indexed and utils.to_string(workers_finished_indexed) or 'nil')
|
|
||||||
end)
|
|
|
@ -1,576 +0,0 @@
|
||||||
--[[
|
|
||||||
SOURCE_ https://github.com/deus0ww/mpv-conf/blob/master/scripts/Thumbnailer_Worker.lua
|
|
||||||
COMMIT_ 20210716 91ae987
|
|
||||||
|
|
||||||
搭配osc_lazy的缩略图脚本(2)/(2)
|
|
||||||
]]--
|
|
||||||
|
|
||||||
local ipairs,loadfile,pairs,pcall,tonumber,tostring = ipairs,loadfile,pairs,pcall,tonumber,tostring
|
|
||||||
local debug,io,math,os,string,table,utf8 = debug,io,math,os,string,table,utf8
|
|
||||||
local min,max,floor,ceil,huge,sqrt = math.min,math.max,math.floor,math.ceil,math.huge,math.sqrt
|
|
||||||
local mp = require 'mp'
|
|
||||||
local msg = require 'mp.msg'
|
|
||||||
local opt = require 'mp.options'
|
|
||||||
local utils = require 'mp.utils'
|
|
||||||
|
|
||||||
local script_name = mp.get_script_name()
|
|
||||||
|
|
||||||
local message = {
|
|
||||||
worker = {
|
|
||||||
registration = 'tn_worker_registration',
|
|
||||||
reset = 'tn_worker_reset',
|
|
||||||
queue = 'tn_worker_queue',
|
|
||||||
start = 'tn_worker_start',
|
|
||||||
progress = 'tn_worker_progress',
|
|
||||||
finish = 'tn_worker_finish',
|
|
||||||
},
|
|
||||||
debug = 'Thumbnailer-debug',
|
|
||||||
|
|
||||||
queued = 1,
|
|
||||||
processing = 2,
|
|
||||||
ready = 3,
|
|
||||||
failed = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
--------------------
|
|
||||||
-- Data Structure --
|
|
||||||
--------------------
|
|
||||||
local state
|
|
||||||
|
|
||||||
local worker_data
|
|
||||||
|
|
||||||
local worker_options
|
|
||||||
|
|
||||||
local worker_extra
|
|
||||||
|
|
||||||
local worker_stats
|
|
||||||
|
|
||||||
local work_queue
|
|
||||||
|
|
||||||
local thumbnail_map_buffer = {}
|
|
||||||
local thumbnail_map_buffer_size = 0
|
|
||||||
|
|
||||||
local function reset_all()
|
|
||||||
state = nil
|
|
||||||
worker_data = {}
|
|
||||||
worker_options = {}
|
|
||||||
worker_extra = {}
|
|
||||||
worker_stats = {}
|
|
||||||
worker_stats.name = script_name
|
|
||||||
worker_stats.queued = 0
|
|
||||||
worker_stats.existing = 0
|
|
||||||
worker_stats.failed = 0
|
|
||||||
worker_stats.success = 0
|
|
||||||
work_queue = {}
|
|
||||||
thumbnail_map_buffer = {}
|
|
||||||
thumbnail_map_buffer_size = 0
|
|
||||||
end
|
|
||||||
reset_all()
|
|
||||||
|
|
||||||
-----------
|
|
||||||
-- Utils --
|
|
||||||
-----------
|
|
||||||
local OS_MAC, OS_WIN, OS_NIX = 'MAC', 'WIN', 'NIX'
|
|
||||||
local function get_os()
|
|
||||||
if jit and jit.os then
|
|
||||||
if jit.os == 'Windows' then return OS_WIN
|
|
||||||
elseif jit.os == 'OSX' then return OS_MAC
|
|
||||||
else return OS_NIX end
|
|
||||||
end
|
|
||||||
if (package.config:sub(1,1) ~= '/') then return OS_WIN end
|
|
||||||
local res = mp.command_native({ name = 'subprocess', args = {'uname', '-s'}, playback_only = false, capture_stdout = true, capture_stderr = true, })
|
|
||||||
return (res and res.stdout and res.stdout:lower():find('darwin') ~= nil) and OS_MAC or OS_NIX
|
|
||||||
end
|
|
||||||
local OPERATING_SYSTEM = get_os()
|
|
||||||
|
|
||||||
local function format_json(tab)
|
|
||||||
local json, err = utils.format_json(tab)
|
|
||||||
if err then msg.error('Formatting JSON failed:', err) end
|
|
||||||
if json then return json else return '' end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parse_json(json)
|
|
||||||
local tab, err = utils.parse_json(json, true)
|
|
||||||
if err then msg.error('Parsing JSON failed:', err) end
|
|
||||||
if tab then return tab else return {} end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_empty(...) -- Not for tables
|
|
||||||
if ... == nil then return true end
|
|
||||||
for _, v in ipairs({...}) do
|
|
||||||
if (v == nil) or (v == '') or (v == 0) then return true end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-------------------------------------
|
|
||||||
-- External Process and Filesystem --
|
|
||||||
-------------------------------------
|
|
||||||
local function subprocess_result(sub_success, result, mpv_error, subprocess_name, start_time)
|
|
||||||
local cmd_status, cmd_stdout, cmd_stderr, cmd_error, cmd_killed
|
|
||||||
if result then cmd_status, cmd_stdout, cmd_stderr, cmd_error, cmd_killed = result.status, result.stdout, result.stderr, result.error_string, result.killed_by_us end
|
|
||||||
local cmd_status_success, cmd_status_string, cmd_err_success, cmd_err_string, success
|
|
||||||
|
|
||||||
if cmd_status == 0 then cmd_status_success, cmd_status_string = true, 'ok'
|
|
||||||
elseif is_empty(cmd_status) then cmd_status_success, cmd_status_string = true, '_'
|
|
||||||
elseif cmd_status == 124 or cmd_status == 137 or cmd_status == 143 then -- timer: timed-out(124), killed(128+9), or terminated(128+15)
|
|
||||||
cmd_status_success, cmd_status_string = false, 'timed out'
|
|
||||||
else cmd_status_success, cmd_status_string = false, ('%d'):format(cmd_status) end
|
|
||||||
|
|
||||||
if is_empty(cmd_error) then cmd_err_success, cmd_err_string = true, '_'
|
|
||||||
elseif cmd_error == 'init' then cmd_err_success, cmd_err_string = false, 'failed to initialize'
|
|
||||||
elseif cmd_error == 'killed' then cmd_err_success, cmd_err_string = false, cmd_killed and 'killed by us' or 'killed, but not by us'
|
|
||||||
else cmd_err_success, cmd_err_string = false, cmd_error end
|
|
||||||
|
|
||||||
if is_empty(cmd_stdout) then cmd_stdout = '_' end
|
|
||||||
if is_empty(cmd_stderr) then cmd_stderr = '_' end
|
|
||||||
subprocess_name = subprocess_name or '_'
|
|
||||||
start_time = start_time or os.time()
|
|
||||||
success = (sub_success == nil or sub_success) and is_empty(mpv_error) and cmd_status_success and cmd_err_success
|
|
||||||
|
|
||||||
if success then msg.debug('Subprocess', subprocess_name, 'succeeded. | Status:', cmd_status_string, '| Time:', ('%ds'):format(os.difftime(os.time(), start_time)))
|
|
||||||
else msg.error('Subprocess', subprocess_name, 'failed. | Status:', cmd_status_string, '| MPV Error:', mpv_error or 'n/a',
|
|
||||||
'| Subprocess Error:', cmd_err_string, '| Stdout:', cmd_stdout, '| Stderr:', cmd_stderr, '| Time:', ('%ds'):format(os.difftime(os.time(), start_time))) end
|
|
||||||
return success, cmd_status_string, cmd_err_string, cmd_stdout, cmd_stderr
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run_subprocess(command, name)
|
|
||||||
if not command then return false end
|
|
||||||
local subprocess_name, start_time = name or command[1], os.time()
|
|
||||||
msg.debug('Subprocess', subprocess_name, 'Starting...', utils.to_string(command))
|
|
||||||
local result, mpv_error = mp.command_native( {name='subprocess', args=command, playback_only=false} )
|
|
||||||
local success, _, _, _ = subprocess_result(nil, result, mpv_error, subprocess_name, start_time)
|
|
||||||
return success
|
|
||||||
end
|
|
||||||
|
|
||||||
local function run_subprocess_async(command, name)
|
|
||||||
if not command then return false end
|
|
||||||
local subprocess_name, start_time = name or command[1], os.time()
|
|
||||||
msg.debug('Subprocess', subprocess_name, 'Starting (async)...')
|
|
||||||
mp.command_native_async( {name='subprocess', args=command, playback_only=false}, function(s, r, e) subprocess_result(s, r, e, subprocess_name, start_time) end )
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function join_paths(...)
|
|
||||||
local sep = OPERATING_SYSTEM == OS_WIN and '\\' or '/'
|
|
||||||
local result = ''
|
|
||||||
for _, p in ipairs({...}) do
|
|
||||||
result = (result == '') and p or result .. sep .. p
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clean_path(path)
|
|
||||||
if OPERATING_SYSTEM == OS_WIN and utf8 ~= nil then
|
|
||||||
for uchar in string.gmatch(path, '\\u([0-9a-f][0-9a-f][0-9a-f][0-9a-f])') do
|
|
||||||
path = path:gsub('\\u' .. uchar, utf8.char('0x' .. uchar))
|
|
||||||
end
|
|
||||||
path = path:gsub('[\\/]', '\\\\')
|
|
||||||
end
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
------------
|
|
||||||
-- Worker --
|
|
||||||
------------
|
|
||||||
mp.command_native({'script-message', message.worker.registration, format_json({name = script_name, script_path = debug.getinfo(1).source})})
|
|
||||||
|
|
||||||
local function stop_file_exist()
|
|
||||||
local file = io.open(join_paths(state.cache_dir, 'stop'), 'r')
|
|
||||||
if not file then return false end
|
|
||||||
file:close()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function check_existing(thumbnail_path)
|
|
||||||
local thumbnail_file = io.open(thumbnail_path, 'rb')
|
|
||||||
if thumbnail_file and thumbnail_file:seek('end') >= worker_extra.filesize then
|
|
||||||
thumbnail_file:close()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function pad_file(thumbnail_path)
|
|
||||||
local thumbnail_file = io.open(thumbnail_path, 'rb')
|
|
||||||
if thumbnail_file then
|
|
||||||
-- Check the size of the generated file
|
|
||||||
local thumbnail_file_size = thumbnail_file:seek('end')
|
|
||||||
thumbnail_file:close()
|
|
||||||
|
|
||||||
-- Check if the file is big enough
|
|
||||||
local missing_bytes = max(0, worker_extra.filesize - thumbnail_file_size)
|
|
||||||
if thumbnail_file_size ~= 0 and missing_bytes > 0 then
|
|
||||||
msg.warn(('Thumbnail missing %d bytes (expected %d, had %d), padding %s'):format(missing_bytes, worker_extra.filesize, thumbnail_file_size, thumbnail_path))
|
|
||||||
thumbnail_file = io.open(thumbnail_path, 'ab')
|
|
||||||
thumbnail_file:write(string.rep(string.char(0), missing_bytes))
|
|
||||||
thumbnail_file:close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function concat_args(args, ...)
|
|
||||||
local arg = ''
|
|
||||||
for _, option in ipairs({...}) do
|
|
||||||
if is_empty(option) then return #args end
|
|
||||||
arg = arg .. tostring(option)
|
|
||||||
end
|
|
||||||
if arg ~= '' then args[#args+1] = arg end
|
|
||||||
return #args
|
|
||||||
end
|
|
||||||
|
|
||||||
local function add_args(args, ...)
|
|
||||||
for _, option in ipairs({...}) do
|
|
||||||
if is_empty(option) then return #args end
|
|
||||||
end
|
|
||||||
for _, option in ipairs({...}) do
|
|
||||||
args[#args+1] = tostring(option)
|
|
||||||
end
|
|
||||||
return #args
|
|
||||||
end
|
|
||||||
|
|
||||||
local function add_timeout(args)
|
|
||||||
local timeout = worker_options.worker_timeout and worker_options.worker_timeout or 0
|
|
||||||
if timeout == 0 then return #args end
|
|
||||||
if OPERATING_SYSTEM == OS_MAC then
|
|
||||||
add_args(args, worker_options.exec_path .. 'gtimeout', ('--kill-after=%d'):format(timeout + 1), ('%d'):format(timeout + 3))
|
|
||||||
elseif OPERATING_SYSTEM == OS_NIX then
|
|
||||||
add_args(args, worker_options.exec_path .. 'timeout', ('--kill-after=%d'):format(timeout + 1), ('%d'):format(timeout + 3))
|
|
||||||
elseif OPERATING_SYSTEM == OS_WIN then
|
|
||||||
-- unimplemented
|
|
||||||
end
|
|
||||||
return #args
|
|
||||||
end
|
|
||||||
|
|
||||||
local function add_nice(args)
|
|
||||||
if OPERATING_SYSTEM == OS_MAC then
|
|
||||||
add_args(args, worker_options.exec_path .. 'gnice', '-19')
|
|
||||||
elseif OPERATING_SYSTEM == OS_NIX then
|
|
||||||
add_args(args, worker_options.exec_path .. 'nice', '-n', '19')
|
|
||||||
elseif OPERATING_SYSTEM == OS_WIN then
|
|
||||||
-- unimplemented
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local pix_fmt = 'bgra'
|
|
||||||
local scale_ff = 'scale=w=%d:h=%d:sws_flags=%s:dst_format=' .. pix_fmt
|
|
||||||
local scale_mpv = 'scale=w=%d:h=%d:flags=%s'
|
|
||||||
local vf_format = ',format=fmt=' .. pix_fmt
|
|
||||||
local transpose = { [-360] = '',
|
|
||||||
[-270] = ',transpose=1',
|
|
||||||
[-180] = ',transpose=2,transpose=2',
|
|
||||||
[ -90] = ',transpose=2',
|
|
||||||
[ 0] = '',
|
|
||||||
[ 90] = ',transpose=1',
|
|
||||||
[ 180] = ',transpose=1,transpose=1',
|
|
||||||
[ 270] = ',transpose=2',
|
|
||||||
[ 360] = '',
|
|
||||||
}
|
|
||||||
|
|
||||||
local function create_mpv_command(time, output, force_accurate_seek)
|
|
||||||
local state, worker_extra, args = state, worker_extra, worker_extra.args
|
|
||||||
local is_last_thumbnail = (state.duration - time) < state.delta
|
|
||||||
local accurate_seek = force_accurate_seek or worker_options.accurate_seek or is_last_thumbnail or state.delta < 3
|
|
||||||
if args then
|
|
||||||
args[worker_extra.index_log] = '--log-file=' .. output .. '.log'
|
|
||||||
args[worker_extra.index_fastseek] = '--demuxer-lavf-o-set=fflags=' .. (accurate_seek and '+discardcorrupt+nobuffer' or '+fastseek+discardcorrupt+nobuffer')
|
|
||||||
args[worker_extra.index_accurate] = '--hr-seek=' .. (accurate_seek and 'yes' or 'no')
|
|
||||||
args[worker_extra.index_skip_loop] = '--vd-lavc-skiploopfilter=' .. (accurate_seek and 'nonref' or 'nonkey')
|
|
||||||
args[worker_extra.index_skip_idct] = '--vd-lavc-skipidct=' .. (accurate_seek and 'nonref' or 'nonkey')
|
|
||||||
args[worker_extra.index_skip_frame] = '--vd-lavc-skipframe=' .. (accurate_seek and 'nonref' or 'nonkey')
|
|
||||||
args[worker_extra.index_time] = '--start=' .. tostring(is_last_thumbnail and floor(time) or time)
|
|
||||||
args[worker_extra.index_output] = '--o=' .. output
|
|
||||||
else
|
|
||||||
local width, height = state.width, state.height
|
|
||||||
local vf_scale = (scale_mpv):format(width, height, worker_options.ffmpeg_scaler)
|
|
||||||
local vf_transpose = state.rotate and transpose[tonumber(state.rotate % 360)] or ''
|
|
||||||
local filter_threads = (':o="threads=%d"'):format(worker_options.ffmpeg_threads)
|
|
||||||
local video_filters = '--vf=lavfi="' .. vf_scale .. vf_transpose .. '"' .. filter_threads .. vf_format
|
|
||||||
|
|
||||||
local worker_options = worker_options
|
|
||||||
local header_fields_arg = nil
|
|
||||||
local header_fields = mp.get_property_native('http-header-fields', {})
|
|
||||||
if #header_fields > 0 then
|
|
||||||
header_fields_arg = '--http-header-fields=' .. table.concat(header_fields, ',')
|
|
||||||
end
|
|
||||||
worker_extra.args = {}
|
|
||||||
args = worker_extra.args -- https://mpv.io/manual/master/
|
|
||||||
add_timeout(args)
|
|
||||||
add_nice(args)
|
|
||||||
|
|
||||||
worker_extra.index_name = concat_args(args, worker_options.exec_path .. 'mpv')
|
|
||||||
-- General
|
|
||||||
concat_args(args, '--no-config')
|
|
||||||
concat_args(args, '--msg-level=all=no')
|
|
||||||
worker_extra.index_log = concat_args(args, '--log-file=', output .. '.log')
|
|
||||||
concat_args(args, '--osc=no')
|
|
||||||
concat_args(args, '--load-stats-overlay=no')
|
|
||||||
-- Remote
|
|
||||||
concat_args(args, (worker_extra.ytdl and '--ytdl' or '--no-ytdl'))
|
|
||||||
concat_args(args, header_fields_arg)
|
|
||||||
concat_args(args, '--user-agent=', mp.get_property_native('user-agent'))
|
|
||||||
concat_args(args, '--referrer=', mp.get_property_native('referrer'))
|
|
||||||
-- Input
|
|
||||||
concat_args(args, '--vd-lavc-fast')
|
|
||||||
concat_args(args, '--vd-lavc-threads=', worker_options.ffmpeg_threads)
|
|
||||||
concat_args(args, '--demuxer-lavf-analyzeduration=0.1')
|
|
||||||
concat_args(args, '--demuxer-lavf-probesize=500000')
|
|
||||||
concat_args(args, '--demuxer-lavf-probe-info=nostreams')
|
|
||||||
worker_extra.index_fastseek = concat_args(args, '--demuxer-lavf-o-set=fflags=', accurate_seek and '+discardcorrupt+nobuffer' or '+fastseek+discardcorrupt+nobuffer')
|
|
||||||
worker_extra.index_accurate = concat_args(args, '--hr-seek=', accurate_seek and 'yes' or 'no')
|
|
||||||
worker_extra.index_skip_loop = concat_args(args, '--vd-lavc-skiploopfilter=', accurate_seek and 'nonref' or 'nonkey')
|
|
||||||
worker_extra.index_skip_idct = concat_args(args, '--vd-lavc-skipidct=', accurate_seek and 'nonref' or 'nonkey')
|
|
||||||
worker_extra.index_skip_frame = concat_args(args, '--vd-lavc-skipframe=', accurate_seek and 'nonref' or 'nonkey')
|
|
||||||
concat_args(args, '--hwdec=', worker_options.mpv_hwdec)
|
|
||||||
concat_args(args, '--hdr-compute-peak=no')
|
|
||||||
concat_args(args, '--vd-lavc-dr=no')
|
|
||||||
concat_args(args, '--aid=no')
|
|
||||||
concat_args(args, '--sid=no')
|
|
||||||
concat_args(args, '--sub-auto=no')
|
|
||||||
worker_extra.index_time = concat_args(args, '--start=', tostring(is_last_thumbnail and floor(time) or time))
|
|
||||||
concat_args(args, '--frames=1')
|
|
||||||
concat_args(args, state.input_fullpath)
|
|
||||||
-- Filters
|
|
||||||
concat_args(args, '--sws-scaler=', worker_options.mpv_scaler)
|
|
||||||
concat_args(args, video_filters)
|
|
||||||
-- Output
|
|
||||||
concat_args(args, '--of=rawvideo')
|
|
||||||
concat_args(args, '--ovcopts=pixel_format=', pix_fmt)
|
|
||||||
concat_args(args, '--ocopy-metadata=no')
|
|
||||||
worker_extra.index_output = concat_args(args, '--o=' .. output)
|
|
||||||
end
|
|
||||||
return args, args[worker_extra.index_name]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function create_ffmpeg_command(time, output, force_accurate_seek)
|
|
||||||
local state, worker_extra, args = state, worker_extra, worker_extra.args
|
|
||||||
local is_last_thumbnail = (state.duration - time) < state.delta
|
|
||||||
local accurate_seek = force_accurate_seek or worker_options.accurate_seek or is_last_thumbnail or state.delta < 3
|
|
||||||
if args then
|
|
||||||
args[worker_extra.index_fastseek] = accurate_seek and '+discardcorrupt+nobuffer' or '+fastseek+discardcorrupt+nobuffer'
|
|
||||||
args[worker_extra.index_accurate] = accurate_seek and '-accurate_seek' or '-noaccurate_seek'
|
|
||||||
args[worker_extra.index_skip_loop] = accurate_seek and 'noref' or 'nokey'
|
|
||||||
args[worker_extra.index_skip_idct] = accurate_seek and 'noref' or 'nokey'
|
|
||||||
args[worker_extra.index_skip_frame] = accurate_seek and 'noref' or 'nokey'
|
|
||||||
args[worker_extra.index_time] = tostring(is_last_thumbnail and floor(time) or time)
|
|
||||||
args[worker_extra.index_output] = output
|
|
||||||
else
|
|
||||||
local width, height = state.width, state.height
|
|
||||||
if state.meta_rotated then width, height = height, width end
|
|
||||||
local vf_scale = (scale_ff):format(width, height, worker_options.ffmpeg_scaler)
|
|
||||||
local vf_transpose = state.rotate and transpose[tonumber(state.rotate % 360)] or ''
|
|
||||||
local video_filters = vf_scale .. vf_transpose
|
|
||||||
|
|
||||||
local worker_options = worker_options
|
|
||||||
worker_extra.args = {}
|
|
||||||
args = worker_extra.args -- https://ffmpeg.org/ffmpeg.html#Main-options
|
|
||||||
-- General
|
|
||||||
add_timeout(args)
|
|
||||||
add_nice(args)
|
|
||||||
worker_extra.index_name = add_args(args, worker_options.exec_path .. 'ffmpeg')
|
|
||||||
add_args(args, '-hide_banner')
|
|
||||||
add_args(args, '-nostats')
|
|
||||||
add_args(args, '-loglevel', 'warning')
|
|
||||||
if not (worker_options.ffmpeg_hwaccel == 'none') then
|
|
||||||
add_args(args, '-hwaccel', worker_options.ffmpeg_hwaccel)
|
|
||||||
add_args(args, '-hwaccel_device', worker_options.ffmpeg_hwaccel_device)
|
|
||||||
end
|
|
||||||
-- Input
|
|
||||||
add_args(args, '-threads', worker_options.ffmpeg_threads)
|
|
||||||
add_args(args, '-fflags', 'fastseek')
|
|
||||||
add_args(args, '-flags2', 'fast')
|
|
||||||
if OPERATING_SYSTEM ~= OS_WIN and worker_options.worker_timeout > 0 then add_args(args, '-timelimit', ceil(worker_options.worker_timeout)) end
|
|
||||||
add_args(args, '-analyzeduration', '500000') -- Default: 5000000
|
|
||||||
add_args(args, '-probesize', '500000') -- Default: 5000000
|
|
||||||
worker_extra.index_fastseek = add_args(args, '-fflags', accurate_seek and '+discardcorrupt+nobuffer' or '+fastseek+discardcorrupt+nobuffer')
|
|
||||||
worker_extra.index_accurate = add_args(args, accurate_seek and '-accurate_seek' or '-noaccurate_seek')
|
|
||||||
worker_extra.index_skip_loop = add_args(args, '-skip_loop_filter', accurate_seek and 'noref' or 'nokey')
|
|
||||||
worker_extra.index_skip_idct = add_args(args, '-skip_idct', accurate_seek and 'noref' or 'nokey')
|
|
||||||
worker_extra.index_skip_frame = add_args(args, '-skip_frame', accurate_seek and 'noref' or 'nokey')
|
|
||||||
worker_extra.index_time = add_args(args, '-ss', tostring(is_last_thumbnail and floor(time) or time))
|
|
||||||
add_args(args, '-guess_layout_max', '0')
|
|
||||||
add_args(args, '-an', '-sn')
|
|
||||||
add_args(args, '-i', state.input_fullpath)
|
|
||||||
add_args(args, '-map_metadata', '-1')
|
|
||||||
add_args(args, '-map_chapters', '-1')
|
|
||||||
add_args(args, '-frames:v', '1')
|
|
||||||
-- Filters
|
|
||||||
add_args(args, '-filter_threads', worker_options.ffmpeg_threads)
|
|
||||||
add_args(args, '-vf', video_filters)
|
|
||||||
add_args(args, '-sws_flags', worker_options.ffmpeg_scaler)
|
|
||||||
add_args(args, '-pix_fmt', pix_fmt)
|
|
||||||
-- Output
|
|
||||||
add_args(args, '-f', 'rawvideo')
|
|
||||||
add_args(args, '-threads', worker_options.ffmpeg_threads)
|
|
||||||
add_args(args, '-y')
|
|
||||||
worker_extra.index_output = add_args(args, output)
|
|
||||||
end
|
|
||||||
return args, args[worker_extra.index_name]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- From https://github.com/TheAMM/mpv_thumbnail_script
|
|
||||||
local function hack_input()
|
|
||||||
msg.debug('Hacking Input...')
|
|
||||||
local file_path = mp.get_property_native('stream-path')
|
|
||||||
local playlist_filename = join_paths(state.cache_dir, 'playlist.txt')
|
|
||||||
worker_extra.ytdl = false
|
|
||||||
if #file_path > 8000 then -- Path is too long for a playlist - just pass the original URL to workers and allow ytdl
|
|
||||||
worker_extra.ytdl = true
|
|
||||||
file_path = state.input_fullpath
|
|
||||||
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 could not!'):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
|
|
||||||
state.input_fullpath = file_path
|
|
||||||
end
|
|
||||||
|
|
||||||
local function report_progress_table(thumbnail_map)
|
|
||||||
local progress_report = { name = script_name, input_filename = state.input_filename, thumbnail_map = thumbnail_map }
|
|
||||||
mp.command_native_async({'script-message', message.worker.progress, format_json(progress_report)}, function() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function report_progress(index, new_status)
|
|
||||||
local index_string = index and (state.cache_format):format(index) or ''
|
|
||||||
if index ~= nil and thumbnail_map_buffer[index_string] == nil then thumbnail_map_buffer_size = thumbnail_map_buffer_size + 1 end
|
|
||||||
thumbnail_map_buffer[index_string] = new_status
|
|
||||||
if index == nil or thumbnail_map_buffer_size >= state.worker_buffer then
|
|
||||||
report_progress_table(thumbnail_map_buffer)
|
|
||||||
thumbnail_map_buffer = {}
|
|
||||||
thumbnail_map_buffer_size = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function set_encoder(encoder)
|
|
||||||
if encoder == 'ffmpeg' then
|
|
||||||
worker_extra.create_command = create_ffmpeg_command
|
|
||||||
else
|
|
||||||
worker_extra.create_command = create_mpv_command
|
|
||||||
if state.is_remote then hack_input() end
|
|
||||||
end
|
|
||||||
worker_extra.args = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
local function create_thumbnail(time, fullpath)
|
|
||||||
return (run_subprocess(worker_extra.create_command(time, fullpath, false)) and check_existing(fullpath)) or
|
|
||||||
(run_subprocess(worker_extra.create_command(time, fullpath, true)) and check_existing(fullpath))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function process_thumbnail()
|
|
||||||
if #work_queue == 0 then return end
|
|
||||||
local worker_stats = worker_stats
|
|
||||||
local status = message.processing
|
|
||||||
local time = table.remove(work_queue, 1)
|
|
||||||
local output = (state.cache_format):format(time)
|
|
||||||
local fullpath = join_paths(state.cache_dir, output) .. state.cache_extension
|
|
||||||
report_progress (time, status)
|
|
||||||
|
|
||||||
-- Check for existing thumbnail to avoid generation
|
|
||||||
if check_existing(fullpath) then
|
|
||||||
worker_stats.existing = worker_stats.existing + 1
|
|
||||||
worker_stats.queued = worker_stats.queued - 1
|
|
||||||
report_progress (time, message.ready)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- Generate the thumbnail
|
|
||||||
if create_thumbnail(time, fullpath) then
|
|
||||||
worker_stats.success = worker_stats.success + 1
|
|
||||||
worker_stats.queued = worker_stats.queued - 1
|
|
||||||
report_progress (time, message.ready)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- Switch to MPV when FFMPEG fails
|
|
||||||
-- if worker_options.encoder == 'ffmpeg' then
|
|
||||||
-- set_encoder('mpv')
|
|
||||||
-- if create_thumbnail(time, fullpath) then
|
|
||||||
-- worker_stats.success = worker_stats.success + 1
|
|
||||||
-- worker_stats.queued = worker_stats.queued - 1
|
|
||||||
-- report_progress (time, message.ready)
|
|
||||||
-- return
|
|
||||||
-- end
|
|
||||||
-- end
|
|
||||||
-- If the thumbnail is incomplete, pad it
|
|
||||||
if not check_existing(fullpath) then pad_file(fullpath) end
|
|
||||||
-- Final check
|
|
||||||
if check_existing(fullpath) then
|
|
||||||
worker_stats.success = worker_stats.success + 1
|
|
||||||
worker_stats.queued = worker_stats.queued - 1
|
|
||||||
report_progress (time, message.ready)
|
|
||||||
else
|
|
||||||
worker_stats.failed = worker_stats.failed + 1
|
|
||||||
worker_stats.queued = worker_stats.queued - 1
|
|
||||||
report_progress (time, message.failed)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function process_queue()
|
|
||||||
if not work_queue then return end
|
|
||||||
for _ = 1, #work_queue do
|
|
||||||
if stop_file_exist() then report_progress() break end
|
|
||||||
process_thumbnail()
|
|
||||||
end
|
|
||||||
report_progress()
|
|
||||||
if #work_queue == 0 then mp.command_native({'script-message', message.worker.finish, format_json(worker_stats)}) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function create_queue()
|
|
||||||
set_encoder(worker_options.encoder)
|
|
||||||
work_queue = {}
|
|
||||||
local worker_data, work_queue, worker_stats = worker_data, work_queue, worker_stats
|
|
||||||
local time, output, report_queue, used_frames = 0, '', {}, {}
|
|
||||||
for x = 8, 0, -1 do
|
|
||||||
local nth = (2^x)
|
|
||||||
for y = 0, (ceil(state.tn_per_worker) - 1), nth do
|
|
||||||
if not used_frames[y + 1] then
|
|
||||||
time = (worker_data.start_time_index + y) * state.delta
|
|
||||||
output = (state.cache_format):format(time)
|
|
||||||
if check_existing(join_paths(state.cache_dir, output)) then
|
|
||||||
worker_stats.existing = worker_stats.existing + 1
|
|
||||||
report_queue[(state.cache_format):format(time)] = message.ready
|
|
||||||
elseif time <= state.duration then
|
|
||||||
work_queue[#work_queue + 1] = time
|
|
||||||
worker_stats.queued = worker_stats.queued + 1
|
|
||||||
report_queue[(state.cache_format):format(time)] = message.queued
|
|
||||||
end
|
|
||||||
used_frames[y + 1] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
report_progress_table(report_queue)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
---------------
|
|
||||||
-- Listeners --
|
|
||||||
---------------
|
|
||||||
mp.register_script_message(message.worker.reset, reset_all)
|
|
||||||
|
|
||||||
mp.register_script_message(message.worker.queue, function(json)
|
|
||||||
local new_data = parse_json(json)
|
|
||||||
if new_data.state then state = new_data.state end
|
|
||||||
if new_data.worker_options then worker_options = new_data.worker_options end
|
|
||||||
if new_data.start_time_index then worker_data.start_time_index = new_data.start_time_index end
|
|
||||||
if not worker_extra.filesize then worker_extra.filesize = (state.width * state.height * 4) end
|
|
||||||
create_queue()
|
|
||||||
end)
|
|
||||||
|
|
||||||
mp.register_script_message(message.worker.start, process_queue)
|
|
||||||
|
|
||||||
mp.register_script_message(message.debug, function()
|
|
||||||
msg.info('Thumbnailer Worker Internal States:')
|
|
||||||
msg.info('state:', state and utils.to_string(state) or 'nil')
|
|
||||||
msg.info('worker_data:', worker_data and utils.to_string(worker_data) or 'nil')
|
|
||||||
msg.info('worker_options:', worker_options and utils.to_string(worker_options) or 'nil')
|
|
||||||
msg.info('worker_extra:', worker_extra and utils.to_string(worker_extra) or 'nil')
|
|
||||||
msg.info('worker_stats:', worker_stats and utils.to_string(worker_stats) or 'nil')
|
|
||||||
end)
|
|
Loading…
Reference in a new issue