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