diff --git a/script-opts/osc_lazy.conf b/script-opts/osc_lazy.conf deleted file mode 100644 index 388f424..0000000 --- a/script-opts/osc_lazy.conf +++ /dev/null @@ -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 diff --git a/script-opts/thumbfast.conf b/script-opts/thumbfast.conf new file mode 100644 index 0000000..1a02a5d --- /dev/null +++ b/script-opts/thumbfast.conf @@ -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 diff --git a/script-opts/thumbnailer.conf b/script-opts/thumbnailer.conf deleted file mode 100644 index f5f2eec..0000000 --- a/script-opts/thumbnailer.conf +++ /dev/null @@ -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硬解设备选择(双显卡设备注意切换) diff --git a/scripts/osc_lazy.lua b/scripts/osc.lua similarity index 71% rename from scripts/osc_lazy.lua rename to scripts/osc.lua index f30087f..87be79a 100644 --- a/scripts/osc_lazy.lua +++ b/scripts/osc.lua @@ -1,33 +1,7 @@ ---[[ -SOURCE_ https://github.com/mpv-player/mpv/blob/master/player/lua/osc.lua -COMMIT_ 20211127 448fe02 -SOURCE_ https://github.com/deus0ww/mpv-conf/blob/master/scripts/Thumbnailer_OSC.lua -COMMIT_ 20211004 b448073 - -改进版本的OSC,须禁用原始mpv的内置OSC,且不兼容其它OSC类脚本,实现全部功能需搭配额外两个缩略图引擎脚本(Thumbnailer)。 -示例在 input.conf 中写入: -SHIFT+DEL script-binding osc_lazy/visibility # 切换osc_lazy的可见性 ---]] - -local orig_osc = mp.get_property('osc') -if orig_osc == 'yes' then - local err = "_____\n{\\1c&H0000FF&}注意:\n必须设置 {\\1c&H0000FF&}osc=no\n打开控制台查看更多信息" - mp.set_osd_ass(1280, 720, err) - mp.set_property('osc', 'no') - mp.msg.warn("脚本已自动执行 osc=no 以临时兼容") - mp.msg.warn("正确编辑 mpv.conf 重启程序即可") - mp.msg.warn("不要在运行中更改参数 --osc 的状态") - mp.msg.warn("注意其它osc类脚本亦不应共存") -end - -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 assdraw = require 'mp.assdraw' -local msg = require 'mp.msg' -local opt = require 'mp.options' -local utils = require 'mp.utils' +local msg = require 'mp.msg' +local opt = require 'mp.options' +local utils = require 'mp.utils' -- -- Parameters @@ -35,520 +9,57 @@ local utils = require 'mp.utils' -- default user option values -- do not touch, change them in osc.conf local user_opts = { - showwindowed = true, -- show OSC when windowed? - showfullscreen = true, -- show OSC when fullscreen? - scalewindowed = 1, -- scaling of the controller when windowed - scalefullscreen = 1, -- scaling of the controller when fullscreen - scaleforcedwindow = 2, -- scaling when rendered on a forced window - vidscale = true, -- scale the controller with the video? - valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom) - halign = 0, -- horizontal alignment, -1 (left) to 1 (right) - barmargin = 0, -- vertical margin of top/bottombar - boxalpha = 80, -- alpha of the background box, - -- 0 (opaque) to 255 (fully transparent) - hidetimeout = 500, -- duration in ms until the OSC hides if no - -- mouse movement. enforced non-negative for the - -- user, but internally negative is "always-on". - fadeduration = 200, -- duration of fade out in ms, 0 = no fade - deadzonesize = 0.5, -- size of deadzone - minmousemove = 0, -- minimum amount of pixels the mouse has to - -- move between ticks to make the OSC show up - iamaprogrammer = false, -- use native mpv values and disable OSC - -- internal track list management (and some - -- functions that depend on it) - layout = "bottombar", -- 原可选为 "bottombar" "topbar" "box" "slimbox" ;在osc_lazy中新增 "bottombox" - seekbarstyle = "bar", -- bar, diamond or knob - seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle - seekrangestyle = "inverted", -- bar, line, slider, inverted or none - seekrangeseparate = true, -- wether the seekranges overlay on the bar-style seekbar - seekrangealpha = 200, -- transparency of seekranges - seekbarkeyframes = true, -- use keyframes when dragging the seekbar - title = "${media-title}", -- string compatible with property-expansion - -- to be shown as OSC title - tooltipborder = 1, -- border of tooltip in bottom/topbar - timetotal = true, -- display total time instead of remaining time? -- 原为false - timems = false, -- display timecodes with milliseconds? - visibility = "auto", -- only used at init to set visibility_mode(...) - boxmaxchars = 150, -- title crop threshold for box layout -- 原为80 - boxvideo = false, -- apply osc_param.video_margins to video - windowcontrols = "auto", -- whether to show window controls + showwindowed = true, -- show OSC when windowed? + showfullscreen = true, -- show OSC when fullscreen? + idlescreen = true, -- show mpv logo on idle + scalewindowed = 1, -- scaling of the controller when windowed + scalefullscreen = 1, -- scaling of the controller when fullscreen + scaleforcedwindow = 2, -- scaling when rendered on a forced window + vidscale = true, -- scale the controller with the video? + valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom) + halign = 0, -- horizontal alignment, -1 (left) to 1 (right) + barmargin = 0, -- vertical margin of top/bottombar + boxalpha = 80, -- alpha of the background box, + -- 0 (opaque) to 255 (fully transparent) + hidetimeout = 500, -- duration in ms until the OSC hides if no + -- mouse movement. enforced non-negative for the + -- user, but internally negative is "always-on". + fadeduration = 200, -- duration of fade out in ms, 0 = no fade + deadzonesize = 0.5, -- size of deadzone + minmousemove = 0, -- minimum amount of pixels the mouse has to + -- move between ticks to make the OSC show up + iamaprogrammer = false, -- use native mpv values and disable OSC + -- internal track list management (and some + -- functions that depend on it) + layout = "bottombar", + seekbarstyle = "bar", -- bar, diamond or knob + seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle + seekrangestyle = "inverted",-- bar, line, slider, inverted or none + seekrangeseparate = true, -- whether the seekranges overlay on the bar-style seekbar + seekrangealpha = 200, -- transparency of seekranges + seekbarkeyframes = true, -- use keyframes when dragging the seekbar + title = "${media-title}", -- string compatible with property-expansion + -- to be shown as OSC title + tooltipborder = 1, -- border of tooltip in bottom/topbar + timetotal = false, -- display total time instead of remaining time? + timems = false, -- display timecodes with milliseconds? + tcspace = 100, -- timecode spacing (compensate font size estimation) + visibility = "auto", -- only used at init to set visibility_mode(...) + boxmaxchars = 80, -- title crop threshold for box layout + boxvideo = false, -- apply osc_param.video_margins to video + windowcontrols = "auto", -- whether to show window controls windowcontrols_alignment = "right", -- which side to show window controls on - greenandgrumpy = false, -- disable santa hat - livemarkers = true, -- update seekbar chapter markers on duration change - chapters_osd = true, -- whether to show chapters OSD on next/prev - playlist_osd = true, -- whether to show playlist OSD on next/prev - chapter_fmt = "章节:%s", -- chapter print format for seekbar-hover. "no" to disable - - -- 以下为osc_lazy的独占选项 - - wctitle = "${media-title}", -- 无边框的上方标题 - sub_title = " ", -- bottombox布局的右侧子标题 - sub_title2 = "对比[${contrast}] 亮度[${brightness}] 伽马[${gamma}] 饱和[${saturation}] 色相[${hue}]", - -- bottombox布局的临时右侧子标题 - font = "sans", -- OSC的全局字体显示 - font_mono = "sans", - font_bold = 500, + greenandgrumpy = false, -- disable santa hat + livemarkers = true, -- update seekbar chapter markers on duration change + chapters_osd = true, -- whether to show chapters OSD on next/prev + playlist_osd = true, -- whether to show playlist OSD on next/prev + chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable + unicodeminus = false, -- whether to use the Unicode minus sign character } -- read options from config and command-line -opt.read_options(user_opts, "osc_lazy", function(list) update_options(list) end) +opt.read_options(user_opts, "osc", function(list) update_options(list) end) - - - - - - - ------------- --- tn_osc -- ------------- -local message = { - osc = { - registration = 'tn_osc_registration', - reset = 'tn_osc_reset', - update = 'tn_osc_update', - finish = 'tn_osc_finish', - }, - debug = 'Thumbnailer-debug', - - 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 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 - - --------------------- --- Data Structure -- --------------------- -local tn_state, tn_osc, tn_osc_options, tn_osc_stats -local tn_thumbnails_indexed, tn_thumbnails_ready -local tn_gen_time_start, tn_gen_duration - -local function reset_all() - tn_state = nil - tn_osc = { - cursor = {}, - position = {}, - scale = {}, - osc_scale = {}, - spacer = {}, - osd = {}, - background = {}, - font_scale = {}, - display_progress = {}, - progress = {}, - mini = {}, - thumbnail = { - visible = false, - path_last = nil, - x_last = nil, - y_last = nil, - }, - } - tn_osc_options = nil - tn_osc_stats = { - queued = 0, - processing = 0, - ready = 0, - failed = 0, - total = 0, - total_expected = 0, - percent = 0, - timer = 0, - } - tn_thumbnails_indexed = {} - tn_thumbnails_ready = {} - tn_gen_time_start = nil - tn_gen_duration = nil -end - ------------- --- TN OSC -- ------------- -local osc_reg = { - script_name = mp.get_script_name(), - osc_opts = { - scalewindowed = user_opts.scalewindowed, - scalefullscreen = user_opts.scalefullscreen, - }, -} -mp.command_native({'script-message', message.osc.registration, format_json(osc_reg)}) - -local tn_palette = { - black = '000000', - white = 'FFFFFF', - alpha_opaque = 0, - alpha_clear = 255, - alpha_black = min(255, user_opts.boxalpha), - alpha_white = min(255, user_opts.boxalpha + (255 - user_opts.boxalpha) * 0.8), -} - -local tn_style_format = { - background = '{\\bord0\\1c&H%s&\\1a&H%X&}', - subbackground = '{\\bord0\\1c&H%s&\\1a&H%X&}', - spinner = '{\\bord0\\fs%d\\fscx%f\\fscy%f', - spinner2 = '\\1c&%s&\\1a&H%X&\\frz%d}', - closest_index = '{\\1c&H%s&\\1a&H%X&\\3c&H%s&\\3a&H%X&\\xbord%d\\ybord%d}', - progress_mini = '{\\bord0\\1c&%s&\\1a&H%X&\\fs18\\fscx%f\\fscy%f', - progress_mini2 = '\\frz%d}', - progress_block = '{\\bord0\\1c&H%s&\\1a&H%X&}', - progress_text = '{\\1c&%s&\\3c&H%s&\\1a&H%X&\\3a&H%X&\\blur0.25\\fs18\\fscx%f\\fscy%f\\xbord%f\\ybord%f\\fn' .. user_opts.font_mono .. '}', - text_timer = '%.2ds', - text_progress = '%.3d/%.3d', - text_progress2 = '[%d]', - text_percent = '%d%%', -} - -local tn_style = { - background = (tn_style_format.background):format(tn_palette.black, tn_palette.alpha_black), - subbackground = (tn_style_format.subbackground):format(tn_palette.white, tn_palette.alpha_white), - spinner = (tn_style_format.spinner):format(0, 1, 1), - closest_index = (tn_style_format.closest_index):format(tn_palette.white, tn_palette.alpha_black, tn_palette.black, tn_palette.alpha_black, -1, -1), - progress_mini = (tn_style_format.progress_mini):format(tn_palette.white, tn_palette.alpha_opaque, 1, 1), - progress_block = (tn_style_format.progress_block):format(tn_palette.white, tn_palette.alpha_white), - progress_text = (tn_style_format.progress_text):format(tn_palette.white, tn_palette.black, tn_palette.alpha_opaque, tn_palette.alpha_black, 1, 1, 2, 2), -} - -local function set_thumbnail_above(offset) - local tn_osc = tn_osc - tn_osc.background.bottom = tn_osc.position.y - offset - tn_osc.spacer.bottom - tn_osc.background.top = tn_osc.background.bottom - tn_osc.background.h - tn_osc.thumbnail.top = tn_osc.background.bottom - tn_osc.thumbnail.h - tn_osc.progress.top = tn_osc.background.bottom - tn_osc.background.h - tn_osc.progress.mid = tn_osc.progress.top + tn_osc.progress.h * 0.5 - tn_osc.background.rotation = -1 -end - -local function set_thumbnail_below(offset) - local tn_osc = tn_osc - tn_osc.background.top = tn_osc.position.y + offset + tn_osc.spacer.top - tn_osc.thumbnail.top = tn_osc.background.top - tn_osc.progress.top = tn_osc.background.top + tn_osc.thumbnail.h + tn_osc.spacer.y - tn_osc.progress.mid = tn_osc.progress.top + tn_osc.progress.h * 0.5 - tn_osc.background.rotation = 1 -end - -local function set_mini_above() tn_osc.mini.y = (tn_osc.background.top - 12 * tn_osc.osc_scale.y) end -local function set_mini_below() tn_osc.mini.y = (tn_osc.background.bottom + 12 * tn_osc.osc_scale.y) end - -local set_thumbnail_layout = { - topbar = function() tn_osc.spacer.top = 0.25 - set_thumbnail_below(38.75) - set_mini_above() end, - bottombar = function() tn_osc.spacer.bottom = 0.25 - set_thumbnail_above(38.75) - set_mini_below() end, - box = function() set_thumbnail_above(15) - set_mini_above() end, - bottombox = function() set_thumbnail_above(14) -- 缩略图适配bottombox布局 - set_mini_above() end, - slimbox = function() set_thumbnail_above(12) - set_mini_above() end, -} - -local function update_tn_osc_params(seek_y) - local tn_state, tn_osc_stats, tn_osc, tn_style, tn_style_format = tn_state, tn_osc_stats, tn_osc, tn_style, tn_style_format - tn_osc.scale.x, tn_osc.scale.y = get_virt_scale_factor() - tn_osc.osd.w, tn_osc.osd.h = mp.get_osd_size() - tn_osc.cursor.x, tn_osc.cursor.y = get_virt_mouse_pos() - tn_osc.position.y = seek_y - - local osc_changed = false - if tn_osc.scale.x_last ~= tn_osc.scale.x or tn_osc.scale.y_last ~= tn_osc.scale.y - or tn_osc.w_last ~= tn_state.width or tn_osc.h_last ~= tn_state.height - or tn_osc.osd.w_last ~= tn_osc.osd.w or tn_osc.osd.h_last ~= tn_osc.osd.h - then - tn_osc.scale.x_last, tn_osc.scale.y_last = tn_osc.scale.x, tn_osc.scale.y - tn_osc.w_last, tn_osc.h_last = tn_state.width, tn_state.height - tn_osc.osd.w_last, tn_osc.osd.h_last = tn_osc.osd.w, tn_osc.osd.h - osc_changed = true - end - - if osc_changed then - tn_osc.osc_scale.x, tn_osc.osc_scale.y = 1, 1 - tn_osc.spacer.x, tn_osc.spacer.y = tn_osc_options.spacer, tn_osc_options.spacer - tn_osc.font_scale.x, tn_osc.font_scale.y = 100, 100 - tn_osc.progress.h = (16 + tn_osc_options.spacer) - if not user_opts.vidscale then - tn_osc.osc_scale.x = tn_osc.scale.x * tn_osc_options.scale - tn_osc.osc_scale.y = tn_osc.scale.y * tn_osc_options.scale - tn_osc.spacer.x = tn_osc.osc_scale.x * tn_osc.spacer.x - tn_osc.spacer.y = tn_osc.osc_scale.y * tn_osc.spacer.y - tn_osc.font_scale.x = tn_osc.osc_scale.x * tn_osc.font_scale.x - tn_osc.font_scale.y = tn_osc.osc_scale.y * tn_osc.font_scale.y - tn_osc.progress.h = tn_osc.osc_scale.y * tn_osc.progress.h - end - tn_osc.spacer.top, tn_osc.spacer.bottom = tn_osc.spacer.y, tn_osc.spacer.y - tn_osc.thumbnail.w, tn_osc.thumbnail.h = tn_state.width * tn_osc.scale.x, tn_state.height * tn_osc.scale.y - tn_osc.osd.w_scaled, tn_osc.osd.h_scaled = tn_osc.osd.w * tn_osc.scale.x, tn_osc.osd.h * tn_osc.scale.y - tn_style.spinner = (tn_style_format.spinner):format(min(tn_osc.thumbnail.w, tn_osc.thumbnail.h) * 0.6667, tn_osc.font_scale.x, tn_osc.font_scale.y) - tn_style.closest_index = (tn_style_format.closest_index):format(tn_palette.white, tn_palette.alpha_black, tn_palette.black, tn_palette.alpha_black, -1 * tn_osc.scale.x, -1 * tn_osc.scale.y) - if tn_osc_stats.percent < 1 then - tn_style.progress_text = (tn_style_format.progress_text):format(tn_palette.white, tn_palette.black, tn_palette.alpha_opaque, tn_palette.alpha_black, tn_osc.font_scale.x, tn_osc.font_scale.y, 2 * tn_osc.scale.x, 2 * tn_osc.scale.y) - tn_style.progress_mini = (tn_style_format.progress_mini):format(tn_palette.white, tn_palette.alpha_opaque, tn_osc.font_scale.x, tn_osc.font_scale.y) - end - end - - if not tn_osc.position.y then return end - if (osc_changed or tn_osc.cursor.x_last ~= tn_osc.cursor.x) and tn_osc.osd.w_scaled >= (tn_osc.thumbnail.w + 2 * tn_osc.spacer.x) then - tn_osc.cursor.x_last = tn_osc.cursor.x - if tn_osc_options.centered then - tn_osc.position.x = tn_osc.osd.w_scaled * 0.5 - else - local limit_left = tn_osc.spacer.x + tn_osc.thumbnail.w * 0.5 - local limit_right = tn_osc.osd.w_scaled - limit_left - tn_osc.position.x = min(max(tn_osc.cursor.x, limit_left), limit_right) - end - tn_osc.thumbnail.left, tn_osc.thumbnail.right = tn_osc.position.x - tn_osc.thumbnail.w * 0.5, tn_osc.position.x + tn_osc.thumbnail.w * 0.5 - tn_osc.mini.x = tn_osc.thumbnail.right - 6 * tn_osc.osc_scale.x - end - - if (osc_changed or tn_osc.display_progress.last ~= tn_osc.display_progress.current) then - tn_osc.display_progress.last = tn_osc.display_progress.current - tn_osc.background.h = tn_osc.thumbnail.h + (tn_osc.display_progress.current and (tn_osc.progress.h + tn_osc.spacer.y) or 0) - set_thumbnail_layout[user_opts.layout]() - end -end - -local function find_closest(seek_index, round_up) - local tn_state, tn_thumbnails_indexed, tn_thumbnails_ready = tn_state, tn_thumbnails_indexed, tn_thumbnails_ready - if not (tn_thumbnails_indexed and tn_thumbnails_ready) then return nil, nil end - local time_index = floor(seek_index * tn_state.delta) - if tn_thumbnails_ready[time_index] then return seek_index + 1, tn_thumbnails_indexed[time_index] end - local direction, index = round_up and 1 or -1 - for i = 1, tn_osc_stats.total_expected do - index = seek_index + (i * direction) - time_index = floor(index * tn_state.delta) - if tn_thumbnails_ready[time_index] then return index + 1, tn_thumbnails_indexed[time_index] end - index = seek_index + (i * -direction) - time_index = floor(index * tn_state.delta) - if tn_thumbnails_ready[time_index] then return index + 1, tn_thumbnails_indexed[time_index] end - end - return nil, nil -end - -local draw_cmd = { name = 'overlay-add', id = 9, offset = 0, fmt = 'bgra' } -local hide_cmd = { name = 'overlay-remove', id = 9} - -local function draw_thumbnail(x, y, path) - draw_cmd.x = x - draw_cmd.y = y - draw_cmd.file = path - mp.command_native(draw_cmd) - tn_osc.thumbnail.visible = true -end - -local function hide_thumbnail() - if tn_osc and tn_osc.thumbnail and tn_osc.thumbnail.visible then - mp.command_native(hide_cmd) - tn_osc.thumbnail.visible = false - end -end - -local function show_thumbnail(seek_percent) - if not seek_percent then return nil, nil end - local scale, thumbnail, total_expected, ready = tn_osc.scale, tn_osc.thumbnail, tn_osc_stats.total_expected, tn_osc_stats.ready - local seek = seek_percent * (total_expected - 1) - local seek_index = floor(seek + 0.5) - local closest_index, path = thumbnail.closest_index_last, thumbnail.path_last - if thumbnail.seek_index_last ~= seek_index - or thumbnail.ready_last ~= ready - or thumbnail.total_expected_last ~= tn_osc_stats.total_expected - then - closest_index, path = find_closest(seek_index, seek_index < seek) - thumbnail.closest_index_last, thumbnail.total_expected_last, thumbnail.ready_last, thumbnail.seek_index_last = closest_index, total_expected, ready, seek_index - end - local x, y = floor((thumbnail.left or 0) / scale.x + 0.5), floor((thumbnail.top or 0) / scale.y + 0.5) - if path and not (thumbnail.visible and thumbnail.x_last == x and thumbnail.y_last == y and thumbnail.path_last == path) then - thumbnail.x_last, thumbnail.y_last, thumbnail.path_last = x, y, path - draw_thumbnail(x, y, path) - end - return closest_index, path -end - -local function ass_new(ass, x, y, align, style, text) - ass:new_event() - ass:pos(x, y) - if align then ass:an(align) end - if style then ass:append(style) end - if text then ass:append(text) end -end - -local function ass_rect(ass, x1, y1, x2, y2) - ass:draw_start() - ass:rect_cw(x1, y1, x2, y2) - ass:draw_stop() -end - -local draw_progress = { - [message.queued] = function(ass, index, block_w, block_h) ass:rect_cw((index - 1) * block_w, 0, index * block_w, block_h) end, - [message.processing] = function(ass, index, block_w, block_h) ass:rect_cw((index - 1) * block_w, block_h * 0.2, index * block_w, block_h * 0.8) end, - [message.failed] = function(ass, index, block_w, block_h) ass:rect_cw((index - 1) * block_w, block_h * 0.4, index * block_w, block_h * 0.6) end, -} - -local function display_tn_osc(seek_y, seek_percent, ass) - if not (seek_y and seek_percent and ass and tn_state and tn_osc_stats and tn_osc_options and tn_state.width and tn_state.height and tn_state.duration and tn_state.cache_dir) or not tn_osc_options.visible then hide_thumbnail() return end - - update_tn_osc_params(seek_y) - local tn_osc_stats, tn_osc, tn_style, tn_style_format, ass_new, ass_rect, seek_percent = tn_osc_stats, tn_osc, tn_style, tn_style_format, ass_new, ass_rect, seek_percent * 0.01 - local closest_index, path = show_thumbnail(seek_percent) - - -- Background - ass_new(ass, tn_osc.thumbnail.left, tn_osc.background.top, 7, tn_style.background) - ass_rect(ass, -tn_osc.spacer.x, -tn_osc.spacer.top, tn_osc.thumbnail.w + tn_osc.spacer.x, tn_osc.background.h + tn_osc.spacer.bottom) - - local spinner_color, spinner_alpha = tn_palette.white, tn_palette.alpha_white - if not path then - ass_new(ass, tn_osc.thumbnail.left, tn_osc.thumbnail.top, 7, tn_style.subbackground) - ass_rect(ass, 0, 0, tn_osc.thumbnail.w, tn_osc.thumbnail.h) - spinner_color, spinner_alpha = tn_palette.black, tn_palette.alpha_black - end - ass_new(ass, tn_osc.position.x, tn_osc.thumbnail.top + tn_osc.thumbnail.h * 0.5, 5, tn_style.spinner .. (tn_style_format.spinner2):format(spinner_color, spinner_alpha, tn_osc.background.rotation * seek_percent * 1080), tn_osc.background.text) - - -- Mini Progress Spinner - if tn_osc.display_progress.current ~= nil and not tn_osc.display_progress.current and tn_osc_stats.percent < 1 then - ass_new(ass, tn_osc.mini.x, tn_osc.mini.y, 5, tn_style.progress_mini .. (tn_style_format.progress_mini2):format(tn_osc_stats.percent * -360 + 90), tn_osc.mini.text) - end - - -- Progress Bar - if tn_osc.display_progress.current then - local block_w, index = tn_osc_stats.total_expected > 0 and tn_state.width * tn_osc.scale.y / tn_osc_stats.total_expected or 0, 0 - if tn_thumbnails_indexed and block_w > 0 then - -- Loading bar - ass_new(ass, tn_osc.thumbnail.left, tn_osc.progress.top, 7, tn_style.progress_block) - ass:draw_start() - for time_index, status in pairs(tn_thumbnails_indexed) do - index = floor(time_index / tn_state.delta) + 1 - if index ~= closest_index and not tn_thumbnails_ready[time_index] and index <= tn_osc_stats.total_expected and draw_progress[status] ~= nil then - draw_progress[status](ass, index, block_w, tn_osc.progress.h) - end - end - ass:draw_stop() - - if closest_index and closest_index <= tn_osc_stats.total_expected then - ass_new(ass, tn_osc.thumbnail.left, tn_osc.progress.top, 7, tn_style.closest_index) - ass_rect(ass, (closest_index - 1) * block_w, 0, closest_index * block_w, tn_osc.progress.h) - end - end - - -- Text: Timer - ass_new(ass, tn_osc.thumbnail.left + 3 * tn_osc.osc_scale.y, tn_osc.progress.mid, 4, tn_style.progress_text, (tn_style_format.text_timer):format(tn_osc_stats.timer)) - - -- Text: Number or Index of Thumbnail - local temp = tn_osc_stats.percent < 1 and tn_osc_stats.ready or closest_index - local processing = tn_osc_stats.processing > 0 and (tn_style_format.text_progress2):format(tn_osc_stats.processing) or '' - ass_new(ass, tn_osc.position.x, tn_osc.progress.mid, 5, tn_style.progress_text, (tn_style_format.text_progress):format(temp and temp or 0, tn_osc_stats.total_expected) .. processing) - - -- Text: Percentage - ass_new(ass, tn_osc.thumbnail.right - 3 * tn_osc.osc_scale.y, tn_osc.progress.mid, 6, tn_style.progress_text, (tn_style_format.text_percent):format(min(100, tn_osc_stats.percent * 100))) - end -end - - ---------------- --- Listeners -- ---------------- -mp.register_script_message(message.osc.reset, function() - hide_thumbnail() - reset_all() -end) - -local text_progress_format = { two_digits = '%.2d/%.2d', three_digits = '%.3d/%.3d' } - -mp.register_script_message(message.osc.update, function(json) - local new_data = parse_json(json) - if not new_data then return end - if new_data.state then - tn_state = new_data.state - if tn_state.is_rotated then tn_state.width, tn_state.height = tn_state.height, tn_state.width end - draw_cmd.w = tn_state.width - draw_cmd.h = tn_state.height - draw_cmd.stride = tn_state.width * 4 - end - if new_data.osc_options then tn_osc_options = new_data.osc_options end - if new_data.osc_stats then - tn_osc_stats = new_data.osc_stats - if tn_osc_options and tn_osc_options.show_progress then - if tn_osc_options.show_progress == 0 then tn_osc.display_progress.current = false - elseif tn_osc_options.show_progress == 1 then tn_osc.display_progress.current = tn_osc_stats.percent < 1 - else tn_osc.display_progress.current = true end - end - tn_style_format.text_progress = tn_osc_stats.total > 99 and text_progress_format.three_digits or text_progress_format.two_digits - if tn_osc_stats.percent >= 1 then mp.command_native({'script-message', message.osc.finish}) end - end - if new_data.thumbnails and tn_state then - local index, ready - for time_string, status in pairs(new_data.thumbnails) do - index, ready = tonumber(time_string), (status == message.ready) - tn_thumbnails_indexed[index] = ready and join_paths(tn_state.cache_dir, time_string) .. tn_state.cache_extension or status - tn_thumbnails_ready[index] = ready - end - end - request_tick() -end) - -mp.register_script_message(message.debug, function() - msg.info("Thumbnailer OSC Internal States:") - msg.info("tn_state:", tn_state and utils.to_string(tn_state) or 'nil') - msg.info("tn_thumbnails_indexed:", tn_thumbnails_indexed and utils.to_string(tn_thumbnails_indexed) or 'nil') - msg.info("tn_thumbnails_ready:", tn_thumbnails_ready and utils.to_string(tn_thumbnails_ready) or 'nil') - msg.info("tn_osc_options:", tn_osc_options and utils.to_string(tn_osc_options) or 'nil') - msg.info("tn_osc_stats:", tn_osc_stats and utils.to_string(tn_osc_stats) or 'nil') - msg.info("tn_osc:", tn_osc and utils.to_string(tn_osc) or 'nil') -end) - - - - - - - - - - -------------- --- osc.lua -- -------------- local osc_param = { -- calculated by osc_init() playresy = 0, -- canvas size Y playresx = 0, -- canvas size X @@ -563,41 +74,24 @@ local osc_param = { -- calculated by osc_init() local osc_styles = { bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}", smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}", - smallButtonsLlabel = ("{\\fscx105\\fscy105\\b%d\\fn%s}"):format(user_opts.font_bold, user_opts.font_mono), + smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}", smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}", topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}", elementDown = "{\\1c&H999999}", - timecodes = ("{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20\\b%d\\fn%s}"):format(user_opts.font_bold, user_opts.font_mono), - vidtitle = ("{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\b%d\\q2\\fn%s}"):format(user_opts.font_bold, user_opts.font), + timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}", + vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}", box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}", topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}", smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}", - timecodesBar = ("{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27\\b%d\\fn%s}"):format(user_opts.font_bold, user_opts.font_mono), - timePosBar = ("{\\blur0.54\\bord%s\\1c&HFFFFFF\\3c&H000000\\fs27\\b%d\\fn%s}"):format(user_opts.tooltipborder, user_opts.font_bold, user_opts.font_mono), - vidtitleBar = ("{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\b%d\\q2\\fn%s}"):format(user_opts.font_bold, user_opts.font), + timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}", + timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}", + vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}", wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}", wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}", wcBar = "{\\1c&H000000}", - - -- bottombox样式 - bb_bigButton1 = "{\\blur0.25\\bord0\\1c&HF2A823\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}", - bb_bigButton2 = "{\\blur0.25\\bord0\\1c&HFACE87\\3c&HFFFFFF\\fs26\\fnmpv-osd-symbols}", - bb_bigButton3 = "{\\blur0.25\\bord0\\1c&H9A530E\\3c&HFFFFFF\\fs34\\fnmpv-osd-symbols}", - bb_backgroud = "{\\blur100\\bord180\\1c&H000000&\\3c&H000000&}", - bb_Atracks = "{\\blur0\\bord0\\1c&H73CBEF\\3c&HFFFFFF\\fs24\\fnmpv-osd-symbols}", - bb_Stracks = "{\\blur0\\bord0\\1c&H70DC57\\3c&HFFFFFF\\fs24\\fnmpv-osd-symbols}", - bb_volume = "{\\blur0\\bord0\\1c&H00F0FF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}", - bb_fs = "{\\blur0\\bord0\\1c&HAC328E\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}", - bb_lua_stats = "{\\blur0\\bord0\\1c&H9370DB\\3c&HFFFFFF\\fs30\\fr180\\fnmpv-osd-symbols}", - bb_seekbar = ("{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs14\\b%d\\q2\\fn%s}"):format(user_opts.font_bold, user_opts.font), - bb_seektime = ("{\\blur0.54\\bord%s\\1c&HFFFFFF\\3c&H000000\\fs18\\b%d\\fn%s}"):format(user_opts.tooltipborder, user_opts.font_bold, user_opts.font_mono), - bb_timecodes = ("{\\blur0\\bord0\\1c&HDCDCDC\\3c&HFFFFFF\\fs20\\b%d\\fn%s}"):format(user_opts.font_bold, user_opts.font_mono), - bb_cachetime = ("{\\blur0\\bord0\\1c&HDCDCDC\\3c&HFFFFFF\\fs14\\b%d\\fn%s}"):format(user_opts.font_bold, user_opts.font_mono), - bb_downtitle = ("{\\blur0\\bord0\\1c&HC0C0C0\\3c&HFFFFFF\\fs16\\b%d\\q2\\fn%s}"):format(user_opts.font_bold, user_opts.font), - bb_sub_title = ("{\\blur0\\bord0\\1c&HC0C0C0\\3c&HFFFFFF\\fs16\\b%d\\q2\\fn%s}"):format(user_opts.font_bold, user_opts.font), } -- internal states, do not touch @@ -633,6 +127,13 @@ local state = { border = true, maximized = false, osd = mp.create_osd_overlay("ass-events"), + chapter_list = {}, -- sorted by time +} + +local thumbfast = { + width = 0, + height = 0, + disabled = false } local window_control_box_width = 80 @@ -845,7 +346,7 @@ end -- Tracklist Management -- -local nicetypes = {video = "视频", audio = "音频", sub = "字幕"} +local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"} -- updates the OSC internal playlists, should be run each time the track-layout changes function update_tracklist() @@ -874,13 +375,13 @@ end -- return a nice list of tracks of the given type (video, audio, sub) function get_tracklist(type) - local msg = "可用" .. nicetypes[type] .. "轨:" - if #tracks_osc[type] == 0 then + local msg = "Available " .. nicetypes[type] .. " Tracks: " + if not tracks_osc or #tracks_osc[type] == 0 then msg = msg .. "none" else for n = 1, #tracks_osc[type] do local track = tracks_osc[type][n] - local lang, title, selected = "未知", "", "○" + local lang, title, selected = "unknown", "", "○" if not(track.lang == nil) then lang = track.lang end if not(track.title == nil) then title = track.title end if (track.id == tonumber(mp.get_property(type))) then @@ -913,11 +414,11 @@ function set_track(type, next) mp.commandv("set", type, new_track_mpv) if (new_track_osc == 0) then - show_message(nicetypes[type] .. "轨:<禁用>") + show_message(nicetypes[type] .. " Track: none") else - show_message(nicetypes[type] .. "轨:" + show_message(nicetypes[type] .. " Track: " .. new_track_osc .. "/" .. #tracks_osc[type] - .. " [".. (tracks_osc[type][new_track_osc].lang or "未知") .."] " + .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] " .. (tracks_osc[type][new_track_osc].title or "")) end end @@ -1113,16 +614,13 @@ end -- returns nil or a chapter element from the native property chapter-list function get_chapter(possec) - local cl = mp.get_property_native("chapter-list", {}) - local ch = nil + local cl = state.chapter_list -- sorted, get latest before possec, if any - -- chapters might not be sorted by time. find nearest-before/at possec - for n=1, #cl do - if possec >= cl[n].time and (not ch or cl[n].time > ch.time) then - ch = cl[n] + for n=#cl,1,-1 do + if possec >= cl[n].time then + return cl[n] end end - return ch end function render_elements(master_ass) @@ -1143,14 +641,6 @@ function render_elements(master_ass) end end - -- bottombox右侧子标题的临时显示 - - state.forced_sub_title = nil - local se, ae = state.slider_element, elements[state.active_element] - if se and (ae == se or (not ae and mouse_hit(se))) then - state.forced_sub_title = mp.command_native({"expand-text", user_opts.sub_title2}) - end - for n=1, #elements do local element = elements[n] @@ -1324,6 +814,7 @@ function render_elements(master_ass) end local tx = get_virt_mouse_pos() + local thumb_tx = tx if (slider_lo.adjust_tooltip) then if (an == 2) then if (sliderpos < (s_min + 3)) then @@ -1348,9 +839,43 @@ function render_elements(master_ass) ass_append_alpha(elem_ass, slider_lo.alpha, 0) elem_ass:append(tooltiplabel) - display_tn_osc(ty, sliderpos, elem_ass) - else - hide_thumbnail() + -- thumbnail + if not thumbfast.disabled and thumbfast.width ~= 0 and thumbfast.height ~= 0 then + local osd_w = mp.get_property_number("osd-width") + if osd_w then + local r_w, r_h = get_virt_scale_factor() + + local tooltip_font_size = (user_opts.layout == "box" or user_opts.layout == "slimbox") and 2 or 12 + local thumb_ty = user_opts.layout ~= "topbar" and element.hitbox.y1 - 8 or element.hitbox.y2 + tooltip_font_size + 8 + + local thumb_pad = 2 + local thumb_margin_x = 20 / r_w + local thumb_margin_y = (4 + user_opts.tooltipborder) / r_h + thumb_pad + local thumb_x = math.min(osd_w - thumbfast.width - thumb_margin_x, math.max(thumb_margin_x, thumb_tx / r_w - thumbfast.width / 2)) + local thumb_y = user_opts.layout ~= "topbar" and thumb_ty / r_h - thumbfast.height - tooltip_font_size / r_h - thumb_margin_y or thumb_ty / r_h + thumb_margin_y + + thumb_x = math.floor(thumb_x + 0.5) + thumb_y = math.floor(thumb_y + 0.5) + + elem_ass:new_event() + elem_ass:pos(thumb_x * r_w, thumb_y * r_h) + elem_ass:append(osc_styles.timePosBar) + elem_ass:append("{\\1a&H20&}") + elem_ass:draw_start() + elem_ass:rect_cw(-thumb_pad * r_w, -thumb_pad * r_h, (thumbfast.width + thumb_pad) * r_w, (thumbfast.height + thumb_pad) * r_h) + elem_ass:draw_stop() + + mp.commandv("script-message-to", "thumbfast", "thumb", + mp.get_property_number("duration", 0) * (sliderpos / 100), + thumb_x, + thumb_y + ) + end + end + else + if thumbfast.width ~= 0 and thumbfast.height ~= 0 then + mp.commandv("script-message-to", "thumbfast", "clear") + end end end @@ -1423,7 +948,7 @@ function get_playlist() return 'Empty playlist.' end - local message = string.format('播放列表 [%d/%d]\n', pos, count) + local message = string.format('Playlist [%d/%d]:\n', pos, count) for i, v in ipairs(limlist) do local title = v.title local _, filename = utils.split_path(v.filename) @@ -1443,7 +968,7 @@ function get_chapterlist() return 'No chapters.' end - local message = string.format('章节列表 [%d/%d]\n', pos, count) + local message = string.format('Chapters [%d/%d]:\n', pos, count) for i, v in ipairs(limlist) do local time = mp.format_time(v.time) local title = v.title @@ -1493,10 +1018,9 @@ function render_message(ass) local outline = tonumber(mp.get_property("options/osd-border-size")) local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize) local counterscale = osc_param.playresy / osc_param.unscaled_y - local hidpi_scale = mp.get_property_native("display-hidpi-scale", 1.0) - fontsize = fontsize * hidpi_scale * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1) - outline = outline * hidpi_scale * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1) + fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1) + outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1) local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}" @@ -1677,7 +1201,7 @@ function window_controls(topbar) -- Window Title ne = new_element("wctitle", "button") ne.content = function () - local title = mp.command_native({"expand-text", user_opts.wctitle}) + local title = mp.command_native({"expand-text", user_opts.title}) -- escape ASS, and strip newlines and trailing slashes title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{") return not (title == "") and title or "mpv" @@ -1879,189 +1403,6 @@ layouts["box"] = function () end --- bottombox布局 -layouts["bottombox"] = function () - - local osc_geo = { - w = osc_param.playresx, -- 宽 - h = 180, -- 高 - r = 0, -- 圆角 - p = 15, -- 内边距 - } - - -- bottombox的整体位置 - local posX = osc_param.playresx / 2 - local posY = osc_param.playresy - osc_geo.h / 2 - - -- position offset for contents aligned at the borders of the box - local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2 - local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2 - - osc_param.areas = {} -- delete areas - - -- area for active mouse input - add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h)) - - -- area for show/hide - local sh_area_y0, sh_area_y1 - if user_opts.valign > 0 then - -- deadzone above OSC - sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), - posY - (osc_geo.h / 2), 0, 0) - sh_area_y1 = osc_param.playresy - else - -- deadzone below OSC - sh_area_y0 = 0 - sh_area_y1 = (posY + (osc_geo.h / 2)) + - get_align(1 - (2*user_opts.deadzonesize), - osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0) - end - add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) - - -- fetch values - local osc_w, osc_h, osc_r, osc_p = - osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p - - local lo - - -- - -- 背景板 - -- - - new_element('bb_backgroud', 'box') - lo = add_layout('bb_backgroud') - lo.geometry = {x = posX, y = osc_param.playresy, an = 5, w = osc_w, h = 0} - lo.layer = 10 - lo.style = osc_styles.bb_backgroud - lo.alpha[1] = 255 - lo.alpha[3] = 0 - - -- - -- 下方标题 - -- - - local titlerowY = posY + pos_offsetY - 5 - - lo = add_layout("title") - lo.geometry = {x = posX, y = titlerowY, an = 5, w = 500, h = 18} - lo.style = osc_styles.bb_downtitle - lo.button.maxchars = user_opts.boxmaxchars - - -- 右侧子标题 - - lo = add_layout("sub_title") - lo.geometry = {x = posX + pos_offsetX, y = titlerowY - 55, an = 6, w = 0, h = 0} - lo.style = osc_styles.bb_sub_title - lo.button.maxchars = user_opts.boxmaxchars - - -- - -- 播放按钮 - -- - - local bigbtnrowY = posY - pos_offsetY + 65 - local bigbtndist = 50 - - lo = add_layout("playpause") - lo.geometry = - {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40} - lo.style = osc_styles.bb_bigButton1 - - lo = add_layout("skipback") - lo.geometry = - {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 15, h = 15} - lo.style = osc_styles.bb_bigButton2 - - lo = add_layout("skipfrwd") - lo.geometry = - {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 15, h = 15} - lo.style = osc_styles.bb_bigButton2 - - lo = add_layout("ch_prev") - lo.geometry = - {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 15, h = 15} - lo.style = osc_styles.bb_bigButton2 - - lo = add_layout("ch_next") - lo.geometry = - {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 15, h = 15} - lo.style = osc_styles.bb_bigButton2 - - lo = add_layout("pl_prev") - lo.geometry = - {x = posX - (bigbtndist * 3), y = bigbtnrowY, an = 5, w = 25, h = 25} - lo.style = osc_styles.bb_bigButton3 - - lo = add_layout("pl_next") - lo.geometry = - {x = posX + (bigbtndist * 3), y = bigbtnrowY, an = 5, w = 25, h = 25} - lo.style = osc_styles.bb_bigButton3 - - -- 快捷按钮 - - lo = add_layout("cy_audio") - lo.geometry = - {x = posX - pos_offsetX + 40, y = bigbtnrowY, an = 5, w = 70, h = 25} - lo.style = osc_styles.bb_Atracks - - lo = add_layout("cy_sub") - lo.geometry = - {x = posX - pos_offsetX + (50 * 2) + osc_geo.p, y = bigbtnrowY, an = 5, w = 70, h = 25} - lo.style = osc_styles.bb_Stracks - - lo = add_layout("tog_fs") - lo.geometry = - {x = posX + pos_offsetX - 25, y = bigbtnrowY, an = 5, w = 25, h = 25} - lo.style = osc_styles.bb_fs - - lo = add_layout("volume") - lo.geometry = - {x = posX + pos_offsetX - (30 * 2) - osc_geo.p, y = bigbtnrowY, an = 4, w = 25, h = 25} - lo.style = osc_styles.bb_volume - - -- 联动内置的stats.lua - - lo = add_layout("lua_stats") - lo.geometry = - {x = posX + pos_offsetX - (45 * 2) - osc_geo.p, y = bigbtnrowY + 2, an = 5, w = 25, h = 25} - lo.style = osc_styles.bb_lua_stats - - -- - -- 进度条 - -- - - lo = add_layout("seekbar") - lo.geometry = - {x = posX, y = posY + pos_offsetY - 22, an = 2, w = pos_offsetX * 2, h = 20} - lo.style = osc_styles.bb_seekbar - lo.slider.gap = 2 - lo.slider.tooltip_style = osc_styles.bb_seektime - lo.slider.tooltip_an = 5 - lo.slider.stype = user_opts["seekbarstyle"] - lo.slider.rtype = user_opts["seekrangestyle"] - - -- - -- 时间码和缓存 - -- - - local bottomrowY = posY + pos_offsetY - 5 - - lo = add_layout("tc_left") - lo.geometry = - {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 80, h = 18} - lo.style = osc_styles.bb_timecodes - - lo = add_layout("tc_right") - lo.geometry = - {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 80, h = 18} - lo.style = osc_styles.bb_timecodes - - lo = add_layout("cache") - lo.geometry = - {x = posX - pos_offsetX, y = bottomrowY - pos_offsetY + 18, an = 4, w = 110, h = 14} - lo.style = osc_styles.bb_cachetime - -end - -- slim box layout layouts["slimbox"] = function () @@ -2184,6 +1525,11 @@ function bar_layout(direction) local padY = 3 local buttonW = 27 local tcW = (state.tc_ms) and 170 or 110 + if user_opts.tcspace >= 50 and user_opts.tcspace <= 200 then + -- adjust our hardcoded font size estimation + tcW = tcW * user_opts.tcspace / 100 + end + local tsW = 90 local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2 @@ -2436,6 +1782,8 @@ function update_options(list) request_init() end +local UNICODE_MINUS = string.char(0xe2, 0x88, 0x92) -- UTF-8 for U+2212 MINUS SIGN + -- OSC INIT function osc_init() msg.debug("osc_init") @@ -2452,8 +1800,6 @@ function osc_init() else scale = user_opts.scalewindowed end - - scale = scale * mp.get_property_native("display-hidpi-scale", 1.0) if user_opts.vidscale then osc_param.unscaled_y = baseResY @@ -2505,17 +1851,6 @@ function osc_init() ne.eventresponder["mbtn_right_up"] = function () show_message(mp.get_property_osd("filename")) end - -- sub_title -- bottombox的右侧子标题 - ne = new_element("sub_title", "button") - - ne.content = function () - local title = state.forced_sub_title or - mp.command_native({"expand-text", user_opts.sub_title}) - -- escape ASS, and strip newlines and trailing slashes - title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{") - return not (title == "") and title or "mpv" - end - -- playlist buttons -- prev @@ -2629,7 +1964,7 @@ function osc_init() -- update_tracklist() - --cy_audio --全局音轨按钮增强 + --cy_audio ne = new_element("cy_audio", "button") ne.enabled = (#tracks_osc.audio > 0) @@ -2648,12 +1983,7 @@ function osc_init() ne.eventresponder["shift+mbtn_left_down"] = function () show_message(get_tracklist("audio"), 2) end - ne.eventresponder["wheel_up_press"] = - function () set_track("audio", -1) end - ne.eventresponder["wheel_down_press"] = - function () set_track("audio", 1) end - - --cy_sub --全局字幕按钮增强 + --cy_sub ne = new_element("cy_sub", "button") ne.enabled = (#tracks_osc.sub > 0) @@ -2672,11 +2002,6 @@ function osc_init() ne.eventresponder["shift+mbtn_left_down"] = function () show_message(get_tracklist("sub"), 2) end - ne.eventresponder["wheel_up_press"] = - function () set_track("sub", -1) end - ne.eventresponder["wheel_down_press"] = - function () set_track("sub", 1) end - --tog_fs ne = new_element("tog_fs", "button") ne.content = function () @@ -2765,8 +2090,7 @@ function osc_init() "absolute-percent", "exact") end ne.eventresponder["reset"] = function (element) element.state.lastseek = nil end - ne.eventresponder["mbtn_right_down"] = function (element) mp.commandv('script-message', 'Thumbnailer-toggle-osc') end - ne.eventresponder["mbtn_right_dbl_press"] = function (element) mp.commandv('script-message', 'Thumbnailer-double') end + -- tc_left (current pos) ne = new_element("tc_left", "button") @@ -2789,10 +2113,11 @@ function osc_init() ne.visible = (mp.get_property_number("duration", 0) > 0) ne.content = function () if (state.rightTC_trem) then + local minus = user_opts.unicodeminus and UNICODE_MINUS or "-" if state.tc_ms then - return ("-"..mp.get_property_osd("playtime-remaining/full")) + return (minus..mp.get_property_osd("playtime-remaining/full")) else - return ("-"..mp.get_property_osd("playtime-remaining")) + return (minus..mp.get_property_osd("playtime-remaining")) end else if state.tc_ms then @@ -2824,7 +2149,7 @@ function osc_init() end local min = math.floor(dmx_cache / 60) local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60 - return "缓冲" .. (min > 0 and + return "Cache: " .. (min > 0 and string.format("%sm%02.0fs", min, sec) or string.format("%3.0fs", sec)) end @@ -2847,23 +2172,10 @@ function osc_init() function () mp.commandv("cycle", "mute") end ne.eventresponder["wheel_up_press"] = - function () mp.commandv("osd-auto", "add", "volume", 1) end + function () mp.commandv("osd-auto", "add", "volume", 5) end ne.eventresponder["wheel_down_press"] = - function () mp.commandv("osd-auto", "add", "volume", -1) end + function () mp.commandv("osd-auto", "add", "volume", -5) end - -- bottombox的统计信息按钮 联动内置的stats.lua - ne = new_element("lua_stats", "button") - - ne.content = "\238\132\135" - ne.eventresponder["mbtn_left_up"] = - function () mp.commandv("script-binding", "stats/display-stats-toggle") end - ne.eventresponder["mbtn_right_up"] = - function () mp.commandv("script-binding", "stats/display-page-4") end - - ne.eventresponder["wheel_up_press"] = - function () mp.commandv("script-binding", "stats/display-page-1") end - ne.eventresponder["wheel_down_press"] = - function () mp.commandv("script-binding", "stats/display-page-2") end -- load layout layouts[user_opts.layout]() @@ -3190,8 +2502,6 @@ function render() -- actual OSC if state.osc_visible then render_elements(ass) - else - hide_thumbnail() end -- submit @@ -3319,29 +2629,37 @@ function tick() -- render idle message msg.trace("idle message") - local icon_x, icon_y = 320 - 26, 140 + local _, _, display_aspect = mp.get_osd_size() + local display_h = 360 + local display_w = display_h * display_aspect + -- logo is rendered at 2^(6-1) = 32 times resolution with size 1800x1800 + local icon_x, icon_y = (display_w - 1800 / 32) / 2, 140 local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y) local ass = assdraw.ass_new() -- mpv logo - for i, line in ipairs(logo_lines) do - ass:new_event() - ass:append(line_prefix .. line) + if user_opts.idlescreen then + for i, line in ipairs(logo_lines) do + ass:new_event() + ass:append(line_prefix .. line) + end end -- Santa hat - if is_december and not user_opts.greenandgrumpy then + if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then for i, line in ipairs(santa_hat_lines) do ass:new_event() ass:append(line_prefix .. line) end end - ass:new_event() - ass:pos(320, icon_y+100) - ass:an(8) - ass:append("拖入文件或网址进行播放") - set_osd(640, 360, ass.text) + if user_opts.idlescreen then + ass:new_event() + ass:pos(display_w / 2, icon_y + 65) + ass:an(8) + ass:append("Drop files or URLs to play here.") + end + set_osd(display_w, display_h, ass.text) if state.showhide_enabled then mp.disable_key_bindings("showhide") @@ -3428,9 +2746,17 @@ update_duration_watch() mp.register_event("shutdown", shutdown) mp.register_event("start-file", request_init) +mp.observe_property("osc", "bool", function(name, value) + if value == true then + mp.set_property("osc", "no") + end +end) mp.observe_property("track-list", nil, request_init) mp.observe_property("playlist", nil, request_init) -mp.observe_property("chapter-list", nil, function() +mp.observe_property("chapter-list", "native", function(_, list) + list = list or {} -- safety, shouldn't return nil + table.sort(list, function(a, b) return a.time < b.time end) + state.chapter_list = list update_duration_watch() request_init() end) @@ -3515,7 +2841,7 @@ mp.set_key_bindings({ {"wheel_down", function(e) process_event("wheel_down", "press") end}, {"mbtn_left_dbl", "ignore"}, {"shift+mbtn_left_dbl", "ignore"}, - {"mbtn_right_dbl", function(e) process_event("mbtn_right_dbl", "press") end}, + {"mbtn_right_dbl", "ignore"}, }, "input", "force") mp.enable_key_bindings("input") @@ -3572,11 +2898,11 @@ function visibility_mode(mode, no_osd) utils.shared_script_property_set("osc-visibility", mode) if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then - mp.osd_message("Thumbnailer_OSC的可见性:" .. mode) + mp.osd_message("OSC visibility: " .. mode) end -- Reset the input state on a mode change. The input state will be - -- recalcuated on the next render cycle, except in 'never' mode where it + -- recalculated on the next render cycle, except in 'never' mode where it -- will just stay disabled. mp.disable_key_bindings("input") mp.disable_key_bindings("window-controls") @@ -3586,9 +2912,44 @@ function visibility_mode(mode, no_osd) request_tick() end +function idlescreen_visibility(mode, no_osd) + if mode == "cycle" then + if user_opts.idlescreen then + mode = "no" + else + mode = "yes" + end + end + + if mode == "yes" then + user_opts.idlescreen = true + else + user_opts.idlescreen = false + end + + utils.shared_script_property_set("osc-idlescreen", mode) + + if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then + mp.osd_message("OSC logo visibility: " .. tostring(mode)) + end + + request_tick() +end + visibility_mode(user_opts.visibility, true) mp.register_script_message("osc-visibility", visibility_mode) mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end) +mp.register_script_message("osc-idlescreen", idlescreen_visibility) + +mp.register_script_message("thumbfast-info", function(json) + local data = utils.parse_json(json) + if type(data) ~= "table" or not data.width or not data.height then + msg.error("thumbfast-info: received json didn't produce a table with thumbnail information") + else + thumbfast = data + end +end) + set_virt_mouse_area(0, 0, 0, 0, "input") set_virt_mouse_area(0, 0, 0, 0, "window-controls") diff --git a/scripts/thumbfast.lua b/scripts/thumbfast.lua new file mode 100644 index 0000000..8712d79 --- /dev/null +++ b/scripts/thumbfast.lua @@ -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) diff --git a/scripts/thumbnailer.lua b/scripts/thumbnailer.lua deleted file mode 100644 index 0220157..0000000 --- a/scripts/thumbnailer.lua +++ /dev/null @@ -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) diff --git a/scripts/thumbnailer_worker.lua b/scripts/thumbnailer_worker.lua deleted file mode 100644 index aca8c71..0000000 --- a/scripts/thumbnailer_worker.lua +++ /dev/null @@ -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)