thumbfast.lua 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  1. -- thumbfast.lua
  2. --
  3. -- High-performance on-the-fly thumbnailer
  4. --
  5. -- Built for easy integration in third-party UIs.
  6. --[[
  7. This Source Code Form is subject to the terms of the Mozilla Public
  8. License, v. 2.0. If a copy of the MPL was not distributed with this
  9. file, You can obtain one at https://mozilla.org/MPL/2.0/.
  10. ]]
  11. local options = {
  12. -- Socket path (leave empty for auto)
  13. socket = "",
  14. -- Thumbnail path (leave empty for auto)
  15. thumbnail = "",
  16. -- Maximum thumbnail generation size in pixels (scaled down to fit)
  17. -- Values are scaled when hidpi is enabled
  18. max_height = 200,
  19. max_width = 200,
  20. -- Scale factor for thumbnail display size (requires mpv 0.38+)
  21. -- Note that this is lower quality than increasing max_height and max_width
  22. scale_factor = 1,
  23. -- Apply tone-mapping, no to disable
  24. tone_mapping = "auto",
  25. -- Overlay id
  26. overlay_id = 42,
  27. -- Spawn thumbnailer on file load for faster initial thumbnails
  28. spawn_first = false,
  29. -- Close thumbnailer process after an inactivity period in seconds, 0 to disable
  30. quit_after_inactivity = 0,
  31. -- Enable on network playback
  32. network = false,
  33. -- Enable on audio playback
  34. audio = false,
  35. -- Enable hardware decoding
  36. hwdec = false,
  37. -- Windows only: use native Windows API to write to pipe (requires LuaJIT)
  38. direct_io = false,
  39. -- Custom path to the mpv executable
  40. mpv_path = "mpv"
  41. }
  42. mp.utils = require "mp.utils"
  43. mp.options = require "mp.options"
  44. mp.options.read_options(options, "thumbfast")
  45. local properties = {}
  46. local pre_0_30_0 = mp.command_native_async == nil
  47. local pre_0_33_0 = true
  48. function subprocess(args, async, callback)
  49. callback = callback or function() end
  50. if not pre_0_30_0 then
  51. if async then
  52. return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback)
  53. else
  54. return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args})
  55. end
  56. else
  57. if async then
  58. return mp.utils.subprocess_detached({args = args}, callback)
  59. else
  60. return mp.utils.subprocess({args = args})
  61. end
  62. end
  63. end
  64. local winapi = {}
  65. if options.direct_io then
  66. local ffi_loaded, ffi = pcall(require, "ffi")
  67. if ffi_loaded then
  68. winapi = {
  69. ffi = ffi,
  70. C = ffi.C,
  71. bit = require("bit"),
  72. socket_wc = "",
  73. -- WinAPI constants
  74. CP_UTF8 = 65001,
  75. GENERIC_WRITE = 0x40000000,
  76. OPEN_EXISTING = 3,
  77. FILE_FLAG_WRITE_THROUGH = 0x80000000,
  78. FILE_FLAG_NO_BUFFERING = 0x20000000,
  79. PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
  80. INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
  81. -- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
  82. _lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
  83. }
  84. -- cache flags used in run() to avoid bor() call
  85. winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
  86. ffi.cdef[[
  87. void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
  88. bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
  89. bool __stdcall CloseHandle(void *hObject);
  90. bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
  91. int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
  92. ]]
  93. winapi.MultiByteToWideChar = function(MultiByteStr)
  94. if MultiByteStr then
  95. local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
  96. if utf16_len > 0 then
  97. local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
  98. if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
  99. return utf16_str
  100. end
  101. end
  102. end
  103. return ""
  104. end
  105. else
  106. options.direct_io = false
  107. end
  108. end
  109. local file
  110. local file_bytes = 0
  111. local spawned = false
  112. local disabled = false
  113. local force_disabled = false
  114. local spawn_waiting = false
  115. local spawn_working = false
  116. local script_written = false
  117. local dirty = false
  118. local x, y
  119. local last_x, last_y
  120. local last_seek_time
  121. local effective_w, effective_h = options.max_width, options.max_height
  122. local real_w, real_h
  123. local last_real_w, last_real_h
  124. local script_name
  125. local show_thumbnail = false
  126. local filters_reset = {["lavfi-crop"]=true, ["crop"]=true}
  127. local filters_runtime = {["hflip"]=true, ["vflip"]=true}
  128. local filters_all = {["hflip"]=true, ["vflip"]=true, ["lavfi-crop"]=true, ["crop"]=true}
  129. local tone_mappings = {["none"]=true, ["clip"]=true, ["linear"]=true, ["gamma"]=true, ["reinhard"]=true, ["hable"]=true, ["mobius"]=true}
  130. local last_tone_mapping
  131. local last_vf_reset = ""
  132. local last_vf_runtime = ""
  133. local last_rotate = 0
  134. local par = ""
  135. local last_par = ""
  136. local last_crop = nil
  137. local last_has_vid = 0
  138. local has_vid = 0
  139. local file_timer
  140. local file_check_period = 1/60
  141. local allow_fast_seek = true
  142. local client_script = [=[
  143. #!/usr/bin/env bash
  144. MPV_IPC_FD=0; MPV_IPC_PATH="%s"
  145. trap "kill 0" EXIT
  146. while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
  147. 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
  148. ]=]
  149. local function get_os()
  150. local raw_os_name = ""
  151. if jit and jit.os and jit.arch then
  152. raw_os_name = jit.os
  153. else
  154. if package.config:sub(1,1) == "\\" then
  155. -- Windows
  156. local env_OS = os.getenv("OS")
  157. if env_OS then
  158. raw_os_name = env_OS
  159. end
  160. else
  161. raw_os_name = subprocess({"uname", "-s"}).stdout
  162. end
  163. end
  164. raw_os_name = (raw_os_name):lower()
  165. local os_patterns = {
  166. ["windows"] = "windows",
  167. ["linux"] = "linux",
  168. ["osx"] = "darwin",
  169. ["mac"] = "darwin",
  170. ["darwin"] = "darwin",
  171. ["^mingw"] = "windows",
  172. ["^cygwin"] = "windows",
  173. ["bsd$"] = "darwin",
  174. ["sunos"] = "darwin"
  175. }
  176. -- Default to linux
  177. local str_os_name = "linux"
  178. for pattern, name in pairs(os_patterns) do
  179. if raw_os_name:match(pattern) then
  180. str_os_name = name
  181. break
  182. end
  183. end
  184. return str_os_name
  185. end
  186. local os_name = mp.get_property("platform") or get_os()
  187. local path_separator = os_name == "windows" and "\\" or "/"
  188. if options.socket == "" then
  189. if os_name == "windows" then
  190. options.socket = "thumbfast"
  191. else
  192. options.socket = "/tmp/thumbfast"
  193. end
  194. end
  195. if options.thumbnail == "" then
  196. if os_name == "windows" then
  197. options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
  198. else
  199. options.thumbnail = "/tmp/thumbfast.out"
  200. end
  201. end
  202. local unique = mp.utils.getpid()
  203. options.socket = options.socket .. unique
  204. options.thumbnail = options.thumbnail .. unique
  205. if options.direct_io then
  206. if os_name == "windows" then
  207. winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
  208. end
  209. if winapi.socket_wc == "" then
  210. options.direct_io = false
  211. end
  212. end
  213. options.scale_factor = math.floor(options.scale_factor)
  214. local mpv_path = options.mpv_path
  215. if mpv_path == "mpv" and os_name == "darwin" and unique then
  216. -- TODO: look into ~~osxbundle/
  217. mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
  218. if mpv_path ~= "mpv" then
  219. mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
  220. local mpv_bin = mp.utils.file_info("/usr/local/mpv")
  221. if mpv_bin and mpv_bin.is_file then
  222. mpv_path = "/usr/local/mpv"
  223. else
  224. local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
  225. if mpv_app and mpv_app.is_file then
  226. mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
  227. else
  228. 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`")
  229. end
  230. end
  231. end
  232. end
  233. local function vo_tone_mapping()
  234. local passes = mp.get_property_native("vo-passes")
  235. if passes and passes["fresh"] then
  236. for k, v in pairs(passes["fresh"]) do
  237. for k2, v2 in pairs(v) do
  238. if k2 == "desc" and v2 then
  239. local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
  240. if tone_mapping then
  241. return tone_mapping
  242. end
  243. end
  244. end
  245. end
  246. end
  247. end
  248. local function vf_string(filters, full)
  249. local vf = ""
  250. local vf_table = properties["vf"]
  251. if (properties["video-crop"] or "") ~= "" then
  252. vf = "lavfi-crop="..string.gsub(properties["video-crop"], "(%d*)x?(%d*)%+(%d+)%+(%d+)", "w=%1:h=%2:x=%3:y=%4")..","
  253. local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
  254. local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
  255. if width and height then
  256. vf = string.gsub(vf, "w=:h=:", "w="..width..":h="..height..":")
  257. end
  258. end
  259. if vf_table and #vf_table > 0 then
  260. for i = #vf_table, 1, -1 do
  261. if filters[vf_table[i].name] then
  262. local args = ""
  263. for key, value in pairs(vf_table[i].params) do
  264. if args ~= "" then
  265. args = args .. ":"
  266. end
  267. args = args .. key .. "=" .. value
  268. end
  269. vf = vf .. vf_table[i].name .. "=" .. args .. ","
  270. end
  271. end
  272. end
  273. if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
  274. if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
  275. local tone_mapping = options.tone_mapping
  276. if tone_mapping == "auto" then
  277. tone_mapping = last_tone_mapping or properties["tone-mapping"]
  278. if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
  279. tone_mapping = vo_tone_mapping()
  280. end
  281. end
  282. if not tone_mappings[tone_mapping] then
  283. tone_mapping = "hable"
  284. end
  285. last_tone_mapping = tone_mapping
  286. vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap="..tone_mapping..",zscale=transfer=bt709,"
  287. end
  288. end
  289. if full then
  290. vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra"
  291. end
  292. return vf
  293. end
  294. local function calc_dimensions()
  295. local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
  296. local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
  297. if not width or not height then return end
  298. local scale = properties["display-hidpi-scale"] or 1
  299. if width / height > options.max_width / options.max_height then
  300. effective_w = math.floor(options.max_width * scale + 0.5)
  301. effective_h = math.floor(height / width * effective_w + 0.5)
  302. else
  303. effective_h = math.floor(options.max_height * scale + 0.5)
  304. effective_w = math.floor(width / height * effective_h + 0.5)
  305. end
  306. local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
  307. if v_par == 1 then
  308. par = ":force_original_aspect_ratio=decrease"
  309. else
  310. par = ""
  311. end
  312. end
  313. local info_timer = nil
  314. local function info(w, h)
  315. local rotate = properties["video-params"] and properties["video-params"]["rotate"]
  316. local image = properties["current-tracks/video"] and properties["current-tracks/video"]["image"]
  317. local albumart = image and properties["current-tracks/video"]["albumart"]
  318. disabled = (w or 0) == 0 or (h or 0) == 0 or
  319. has_vid == 0 or
  320. (properties["demuxer-via-network"] and not options.network) or
  321. (albumart and not options.audio) or
  322. (image and not albumart) or
  323. force_disabled
  324. if info_timer then
  325. info_timer:kill()
  326. info_timer = nil
  327. elseif has_vid == 0 or (rotate == nil and not disabled) then
  328. info_timer = mp.add_timeout(0.05, function() info(w, h) end)
  329. end
  330. 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})
  331. if pre_0_30_0 then
  332. mp.command_native({"script-message", "thumbfast-info", json})
  333. else
  334. mp.command_native_async({"script-message", "thumbfast-info", json}, function() end)
  335. end
  336. end
  337. local function remove_thumbnail_files()
  338. if file then
  339. file:close()
  340. file = nil
  341. file_bytes = 0
  342. end
  343. os.remove(options.thumbnail)
  344. os.remove(options.thumbnail..".bgra")
  345. end
  346. local activity_timer
  347. local function spawn(time)
  348. if disabled then return end
  349. local path = properties["path"]
  350. if path == nil then return end
  351. if options.quit_after_inactivity > 0 then
  352. if show_thumbnail or activity_timer:is_enabled() then
  353. activity_timer:kill()
  354. end
  355. activity_timer:resume()
  356. end
  357. local open_filename = properties["stream-open-filename"]
  358. local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
  359. if ytdl then
  360. path = open_filename
  361. end
  362. remove_thumbnail_files()
  363. local vid = properties["vid"]
  364. has_vid = vid or 0
  365. local args = {
  366. mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
  367. "--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", "--load-auto-profiles=no",
  368. "--edition="..(properties["edition"] or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio",
  369. "--start="..time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
  370. "--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
  371. "--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", "--hwdec="..(options.hwdec and "auto" or "no"),
  372. "--vf="..vf_string(filters_all, true),
  373. "--sws-scaler=fast-bilinear",
  374. "--video-rotate="..last_rotate,
  375. "--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail
  376. }
  377. if not pre_0_30_0 then
  378. table.insert(args, "--sws-allow-zimg=no")
  379. end
  380. if os_name == "darwin" and properties["macos-app-activation-policy"] then
  381. table.insert(args, "--macos-app-activation-policy=accessory")
  382. end
  383. if os_name == "windows" or pre_0_33_0 then
  384. table.insert(args, "--input-ipc-server="..options.socket)
  385. elseif not script_written then
  386. local client_script_path = options.socket..".run"
  387. local script = io.open(client_script_path, "w+")
  388. if script == nil then
  389. mp.msg.error("client script write failed")
  390. return
  391. else
  392. script_written = true
  393. script:write(string.format(client_script, options.socket))
  394. script:close()
  395. subprocess({"chmod", "+x", client_script_path}, true)
  396. table.insert(args, "--scripts="..client_script_path)
  397. end
  398. else
  399. local client_script_path = options.socket..".run"
  400. table.insert(args, "--scripts="..client_script_path)
  401. end
  402. table.insert(args, "--")
  403. table.insert(args, path)
  404. spawned = true
  405. spawn_waiting = true
  406. subprocess(args, true,
  407. function(success, result)
  408. if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
  409. spawned = false
  410. spawn_waiting = false
  411. options.tone_mapping = "no"
  412. mp.msg.error("mpv subprocess create failed")
  413. if not spawn_working then -- notify users of required configuration
  414. if options.mpv_path == "mpv" then
  415. if properties["current-vo"] == "libmpv" then
  416. if options.mpv_path == mpv_path then -- attempt to locate ImPlay
  417. mpv_path = "ImPlay"
  418. spawn(time)
  419. else -- ImPlay not in path
  420. if os_name ~= "darwin" then
  421. force_disabled = true
  422. info(real_w or effective_w, real_h or effective_h)
  423. end
  424. mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
  425. mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
  426. end
  427. else
  428. mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
  429. if os_name == "windows" then
  430. mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
  431. mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
  432. end
  433. end
  434. else
  435. mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
  436. -- found ImPlay but not defined in config
  437. mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
  438. end
  439. end
  440. elseif success == true and (result.status == 0 or result.status == -2) then
  441. if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
  442. mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
  443. end
  444. spawn_working = true
  445. spawn_waiting = false
  446. end
  447. end
  448. )
  449. end
  450. local function run(command)
  451. if not spawned then return end
  452. if options.direct_io then
  453. local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil)
  454. if hPipe ~= winapi.INVALID_HANDLE_VALUE then
  455. local buf = command .. "\n"
  456. winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
  457. winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
  458. winapi.C.CloseHandle(hPipe)
  459. end
  460. return
  461. end
  462. local command_n = command.."\n"
  463. if os_name == "windows" then
  464. if file and file_bytes + #command_n >= 4096 then
  465. file:close()
  466. file = nil
  467. file_bytes = 0
  468. end
  469. if not file then
  470. file = io.open("\\\\.\\pipe\\"..options.socket, "r+b")
  471. end
  472. elseif pre_0_33_0 then
  473. subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket})
  474. return
  475. elseif not file then
  476. file = io.open(options.socket, "r+")
  477. end
  478. if file then
  479. file_bytes = file:seek("end")
  480. file:write(command_n)
  481. file:flush()
  482. end
  483. end
  484. local function draw(w, h, script)
  485. if not w or not show_thumbnail then return end
  486. if x ~= nil then
  487. 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
  488. if pre_0_30_0 then
  489. mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w), scale_w, scale_h})
  490. else
  491. 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)
  492. end
  493. elseif script then
  494. 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})
  495. mp.commandv("script-message-to", script, "thumbfast-render", json)
  496. end
  497. end
  498. local function real_res(req_w, req_h, filesize)
  499. local count = filesize / 4
  500. local diff = (req_w * req_h) - count
  501. if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
  502. req_w, req_h = req_h, req_w
  503. end
  504. if diff == 0 then
  505. return req_w, req_h
  506. else
  507. local threshold = 5 -- throw out results that change too much
  508. local long_side, short_side = req_w, req_h
  509. if req_h > req_w then
  510. long_side, short_side = req_h, req_w
  511. end
  512. for a = short_side, short_side - threshold, -1 do
  513. if count % a == 0 then
  514. local b = count / a
  515. if long_side - b < threshold then
  516. if req_h < req_w then return b, a else return a, b end
  517. end
  518. end
  519. end
  520. return nil
  521. end
  522. end
  523. local function move_file(from, to)
  524. if os_name == "windows" then
  525. os.remove(to)
  526. end
  527. -- move the file because it can get overwritten while overlay-add is reading it, and crash the player
  528. os.rename(from, to)
  529. end
  530. local function seek(fast)
  531. if last_seek_time then
  532. run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
  533. end
  534. end
  535. local seek_period = 3/60
  536. local seek_period_counter = 0
  537. local seek_timer
  538. seek_timer = mp.add_periodic_timer(seek_period, function()
  539. if seek_period_counter == 0 then
  540. seek(allow_fast_seek)
  541. seek_period_counter = 1
  542. else
  543. if seek_period_counter == 2 then
  544. if allow_fast_seek then
  545. seek_timer:kill()
  546. seek()
  547. end
  548. else seek_period_counter = seek_period_counter + 1 end
  549. end
  550. end)
  551. seek_timer:kill()
  552. local function request_seek()
  553. if seek_timer:is_enabled() then
  554. seek_period_counter = 0
  555. else
  556. seek_timer:resume()
  557. seek(allow_fast_seek)
  558. seek_period_counter = 1
  559. end
  560. end
  561. local function check_new_thumb()
  562. -- the slave might start writing to the file after checking existance and
  563. -- validity but before actually moving the file, so move to a temporary
  564. -- location before validity check to make sure everything stays consistant
  565. -- and valid thumbnails don't get overwritten by invalid ones
  566. local tmp = options.thumbnail..".tmp"
  567. move_file(options.thumbnail, tmp)
  568. local finfo = mp.utils.file_info(tmp)
  569. if not finfo then return false end
  570. spawn_waiting = false
  571. local w, h = real_res(effective_w, effective_h, finfo.size)
  572. if w then -- only accept valid thumbnails
  573. move_file(tmp, options.thumbnail..".bgra")
  574. real_w, real_h = w, h
  575. if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
  576. last_real_w, last_real_h = real_w, real_h
  577. info(real_w, real_h)
  578. end
  579. if not show_thumbnail then
  580. file_timer:kill()
  581. end
  582. return true
  583. end
  584. return false
  585. end
  586. file_timer = mp.add_periodic_timer(file_check_period, function()
  587. if check_new_thumb() then
  588. draw(real_w, real_h, script_name)
  589. end
  590. end)
  591. file_timer:kill()
  592. local function clear()
  593. file_timer:kill()
  594. seek_timer:kill()
  595. if options.quit_after_inactivity > 0 then
  596. if show_thumbnail or activity_timer:is_enabled() then
  597. activity_timer:kill()
  598. end
  599. activity_timer:resume()
  600. end
  601. last_seek_time = nil
  602. show_thumbnail = false
  603. last_x = nil
  604. last_y = nil
  605. if script_name then return end
  606. if pre_0_30_0 then
  607. mp.command_native({"overlay-remove", options.overlay_id})
  608. else
  609. mp.command_native_async({"overlay-remove", options.overlay_id}, function() end)
  610. end
  611. end
  612. local function quit()
  613. activity_timer:kill()
  614. if show_thumbnail then
  615. activity_timer:resume()
  616. return
  617. end
  618. run("quit")
  619. spawned = false
  620. real_w, real_h = nil, nil
  621. clear()
  622. end
  623. activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
  624. activity_timer:kill()
  625. local function thumb(time, r_x, r_y, script)
  626. if disabled then return end
  627. time = tonumber(time)
  628. if time == nil then return end
  629. if r_x == "" or r_y == "" then
  630. x, y = nil, nil
  631. else
  632. x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
  633. end
  634. script_name = script
  635. if last_x ~= x or last_y ~= y or not show_thumbnail then
  636. show_thumbnail = true
  637. last_x, last_y = x, y
  638. draw(real_w, real_h, script)
  639. end
  640. if options.quit_after_inactivity > 0 then
  641. if show_thumbnail or activity_timer:is_enabled() then
  642. activity_timer:kill()
  643. end
  644. activity_timer:resume()
  645. end
  646. if time == last_seek_time then return end
  647. last_seek_time = time
  648. if not spawned then spawn(time) end
  649. request_seek()
  650. if not file_timer:is_enabled() then file_timer:resume() end
  651. end
  652. local function watch_changes()
  653. if not dirty or not properties["video-out-params"] then return end
  654. dirty = false
  655. local old_w = effective_w
  656. local old_h = effective_h
  657. calc_dimensions()
  658. local vf_reset = vf_string(filters_reset)
  659. local rotate = properties["video-rotate"] or 0
  660. local resized = old_w ~= effective_w or
  661. old_h ~= effective_h or
  662. last_vf_reset ~= vf_reset or
  663. (last_rotate % 180) ~= (rotate % 180) or
  664. par ~= last_par or last_crop ~= properties["video-crop"]
  665. if resized then
  666. last_rotate = rotate
  667. info(effective_w, effective_h)
  668. elseif last_has_vid ~= has_vid and has_vid ~= 0 then
  669. info(effective_w, effective_h)
  670. end
  671. if spawned then
  672. if resized then
  673. -- mpv doesn't allow us to change output size
  674. local seek_time = last_seek_time
  675. run("quit")
  676. clear()
  677. spawned = false
  678. spawn(seek_time or mp.get_property_number("time-pos", 0))
  679. file_timer:resume()
  680. else
  681. if rotate ~= last_rotate then
  682. run("set video-rotate "..rotate)
  683. end
  684. local vf_runtime = vf_string(filters_runtime)
  685. if vf_runtime ~= last_vf_runtime then
  686. run("vf set "..vf_string(filters_all, true))
  687. last_vf_runtime = vf_runtime
  688. end
  689. end
  690. else
  691. last_vf_runtime = vf_string(filters_runtime)
  692. end
  693. last_vf_reset = vf_reset
  694. last_rotate = rotate
  695. last_par = par
  696. last_crop = properties["video-crop"]
  697. last_has_vid = has_vid
  698. if not spawned and not disabled and options.spawn_first and resized then
  699. spawn(mp.get_property_number("time-pos", 0))
  700. file_timer:resume()
  701. end
  702. end
  703. local function update_property(name, value)
  704. properties[name] = value
  705. end
  706. local function update_property_dirty(name, value)
  707. properties[name] = value
  708. dirty = true
  709. if name == "tone-mapping" then
  710. last_tone_mapping = nil
  711. end
  712. end
  713. local function update_tracklist(name, value)
  714. -- current-tracks shim
  715. for _, track in ipairs(value) do
  716. if track.type == "video" and track.selected then
  717. properties["current-tracks/video"] = track
  718. return
  719. end
  720. end
  721. end
  722. local function sync_changes(prop, val)
  723. update_property(prop, val)
  724. if val == nil then return end
  725. if type(val) == "boolean" then
  726. if prop == "vid" then
  727. has_vid = 0
  728. last_has_vid = 0
  729. info(effective_w, effective_h)
  730. clear()
  731. return
  732. end
  733. val = val and "yes" or "no"
  734. end
  735. if prop == "vid" then
  736. has_vid = 1
  737. end
  738. if not spawned then return end
  739. run("set "..prop.." "..val)
  740. dirty = true
  741. end
  742. local function file_load()
  743. clear()
  744. spawned = false
  745. real_w, real_h = nil, nil
  746. last_real_w, last_real_h = nil, nil
  747. last_tone_mapping = nil
  748. last_seek_time = nil
  749. if info_timer then
  750. info_timer:kill()
  751. info_timer = nil
  752. end
  753. calc_dimensions()
  754. info(effective_w, effective_h)
  755. end
  756. local function shutdown()
  757. run("quit")
  758. remove_thumbnail_files()
  759. if os_name ~= "windows" then
  760. os.remove(options.socket)
  761. os.remove(options.socket..".run")
  762. end
  763. end
  764. local function on_duration(prop, val)
  765. allow_fast_seek = (val or 30) >= 30
  766. end
  767. mp.observe_property("current-tracks/video", "native", function(name, value)
  768. if pre_0_33_0 then
  769. mp.unobserve_property(update_tracklist)
  770. pre_0_33_0 = false
  771. end
  772. update_property(name, value)
  773. end)
  774. mp.observe_property("track-list", "native", update_tracklist)
  775. mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
  776. mp.observe_property("video-out-params", "native", update_property_dirty)
  777. mp.observe_property("video-params", "native", update_property_dirty)
  778. mp.observe_property("vf", "native", update_property_dirty)
  779. mp.observe_property("tone-mapping", "native", update_property_dirty)
  780. mp.observe_property("demuxer-via-network", "native", update_property)
  781. mp.observe_property("stream-open-filename", "native", update_property)
  782. mp.observe_property("macos-app-activation-policy", "native", update_property)
  783. mp.observe_property("current-vo", "native", update_property)
  784. mp.observe_property("video-rotate", "native", update_property)
  785. mp.observe_property("video-crop", "native", update_property)
  786. mp.observe_property("path", "native", update_property)
  787. mp.observe_property("vid", "native", sync_changes)
  788. mp.observe_property("edition", "native", sync_changes)
  789. mp.observe_property("duration", "native", on_duration)
  790. mp.register_script_message("thumb", thumb)
  791. mp.register_script_message("clear", clear)
  792. mp.register_event("file-loaded", file_load)
  793. mp.register_event("shutdown", shutdown)
  794. mp.register_idle(watch_changes)