set mpv config to mfpbar
parent
6601bdcb3f
commit
a61f01792d
|
@ -0,0 +1,3 @@
|
|||
A script-binding mdmenu/tracklist
|
||||
C script-binding mdmenu/chapters
|
||||
P script-binding mdmenu/playlist
|
|
@ -1,9 +1,9 @@
|
|||
#hwdec=vaapi
|
||||
#gpu-context=wayland
|
||||
#osc=no
|
||||
hwdec=vaapi
|
||||
gpu-context=wayland
|
||||
osc=no
|
||||
alang=jpn
|
||||
slang=eng
|
||||
#ao=pipewire,
|
||||
ao=pipewire,
|
||||
|
||||
#subtitles
|
||||
sub-font='sans'
|
||||
|
@ -21,5 +21,8 @@ loop-file=inf
|
|||
loop-file=inf
|
||||
|
||||
#added for theme
|
||||
osc = no
|
||||
border = no
|
||||
#[idle]
|
||||
#profile-cond=p["idle-active"]
|
||||
#profile-restore=copy-equal
|
||||
#background=1
|
||||
osd-bar=no
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
# if enabled, dmenu will be embedded inside the mpv instance.
|
||||
# on older mpv versions (v0.35.0 and below) depends on
|
||||
# [xdo](https://github.com/baskerville/xdo) to get mpv's xwindow id.
|
||||
embed=no
|
||||
|
||||
# if enabled, the "current item" (e.g current chapter) will be preselected in dmenu.
|
||||
# requires [preselect](https://tools.suckless.org/dmenu/patches/preselect/)
|
||||
preselect=no
|
||||
|
||||
# command that gets invoked.
|
||||
# can be replaced with anything else that's "dmenu compliant" (such as rofi's dmenu mode).
|
||||
# arguments are comma separated.
|
||||
#cmd=dmenu,-i,-l,16
|
||||
cmd=rofi,-dmenu
|
|
@ -0,0 +1,59 @@
|
|||
# example config for mfpbar.
|
||||
#
|
||||
# NOTE: all colors are in RRGGBB format.
|
||||
|
||||
# height of the progressbar when hovering over it
|
||||
# the value is in percentage, 2 means 2% of the screen height
|
||||
pbar_height=2
|
||||
# height of the progressbar when minimized.
|
||||
# set to 0 disable minimized state.
|
||||
pbar_minimized_height=0.5
|
||||
# color of progressbar
|
||||
pbar_color=CCCCCC
|
||||
# opacity of the progressbar background. in hex.
|
||||
# "00" means fully black, "FF" means fully transparent.
|
||||
pbar_bg_alpha=3F
|
||||
# hide progressbar when fullscreen
|
||||
pbar_fullscreen_hide=yes
|
||||
|
||||
|
||||
# height of the cachebar in percentage. set to 0 to disable.
|
||||
cachebar_height=0.24
|
||||
cachebar_color=1C6C89
|
||||
# color and opacity of the uncached area.
|
||||
# setting the alpha to "FF" will make it fully transparent (which in effect disables it).
|
||||
cachebar_uncached_color=CC3A2A
|
||||
cachebar_uncached_alpha=70
|
||||
|
||||
# what to show in the right hand side of the timeline. valid options are:
|
||||
# time-remaining: shows the remaining time (default)
|
||||
# playtime-remaining: shows the remaining time scaled by the playback speed
|
||||
timeline_rhs=time-remaining
|
||||
|
||||
# color of the hover bar
|
||||
hover_bar_color=BDAE93
|
||||
|
||||
# font size
|
||||
font_size=16
|
||||
# horizontal font padding
|
||||
font_pad=4
|
||||
# font borders
|
||||
font_border_width=2
|
||||
font_border_color=000000
|
||||
|
||||
# trigger range for the progressbar
|
||||
proximity=40
|
||||
|
||||
# width of preview border. set to 0 to disable.
|
||||
preview_border_width=2
|
||||
preview_border_color=BDAE93
|
||||
|
||||
# chapter marker size. set 0 to disable.
|
||||
chapter_marker_size=3
|
||||
chapter_marker_color=BDAE93
|
||||
# chapter marker border width. set 0 to disable.
|
||||
chapter_marker_border_width=1
|
||||
chapter_marker_border_color=161616
|
||||
|
||||
# seconds before auto minimizing. set to 0 to disable auto-minimize.
|
||||
minimize_timeout=3
|
|
@ -0,0 +1,31 @@
|
|||
# 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
|
||||
|
||||
# Custom path to the mpv executable
|
||||
mpv_path=mpv
|
|
@ -0,0 +1,255 @@
|
|||
--[[
|
||||
This file is part of mdmenu.
|
||||
|
||||
mdmenu is free software: you can redistribute it and/or modify it
|
||||
under the terms of the GNU Affero General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
mdmenu is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with mdmenu. If not, see <https://www.gnu.org/licenses/>.
|
||||
]]
|
||||
|
||||
local msg = require('mp.msg')
|
||||
local mpopt = require('mp.options')
|
||||
local utils = require('mp.utils')
|
||||
|
||||
local state = {
|
||||
playlist = nil,
|
||||
playlist_current = nil,
|
||||
tracklist = nil,
|
||||
chapters = nil,
|
||||
chapters_raw = nil,
|
||||
}
|
||||
|
||||
local opt = {
|
||||
embed = true,
|
||||
preselect = false,
|
||||
cmd = { "dmenu", "-i", "-l", "16" },
|
||||
|
||||
debug = false,
|
||||
}
|
||||
|
||||
local ob = { [false] = ' ', [true] = '[' }
|
||||
local cb = { [false] = ' ', [true] = ']' }
|
||||
|
||||
local zassert = function() end
|
||||
|
||||
local function format_time(t)
|
||||
local h = math.floor(t / (60 * 60))
|
||||
t = t - (h * 60 * 60)
|
||||
local m = math.floor(t / 60)
|
||||
local s = t - (m * 60)
|
||||
return string.format("%.2d:%.2d:%.2d", h, m, s)
|
||||
end
|
||||
|
||||
local function humantime_to_sec(str)
|
||||
zassert(string.len(str) >= 8)
|
||||
local h = tonumber(string.sub(str, 1, 2))
|
||||
local m = tonumber(string.sub(str, 4, 5))
|
||||
local s = tonumber(string.sub(str, 7, 8))
|
||||
if h and m and s and
|
||||
string.sub(str, 3, 3) == ':' and
|
||||
string.sub(str, 6, 6) == ':'
|
||||
then
|
||||
return (h * 60 * 60) + (m * 60) + s
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function grab_xid(kind, ws)
|
||||
zassert(kind == "vo-configured")
|
||||
if (ws) then
|
||||
local wid = mp.get_property('window-id')
|
||||
if (wid == nil) then
|
||||
local pid = mp.get_property('pid')
|
||||
local r = mp.command_native({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
args = {"xdo", "id", "-p", pid},
|
||||
})
|
||||
if (r.status == 0 and string.len(r.stdout) > 0) then
|
||||
wid = string.match(r.stdout, "0x%x+")
|
||||
end
|
||||
end
|
||||
if (wid) then
|
||||
table.insert(opt.cmd, "-w")
|
||||
table.insert(opt.cmd, wid)
|
||||
else
|
||||
msg.warn("couldn't get mpv's xwindow id. make sure `xdo` is installed.")
|
||||
end
|
||||
mp.unobserve_property(grab_xid)
|
||||
end
|
||||
end
|
||||
|
||||
local function set_playlist(kind, plist)
|
||||
zassert(kind == "playlist")
|
||||
local s = ''
|
||||
local f = "%"..(string.len(#plist) + string.len(ob[true]) + string.len(cb[true])).."s"
|
||||
state.playlist_current = nil
|
||||
for k,pl in ipairs(plist) do
|
||||
state.playlist_current = pl.current and k or state.playlist_current
|
||||
s = s .. string.format(f, ob[pl.current or false] .. k .. cb[pl.current or false]) .. ' '
|
||||
s = s .. (pl.title or select(2, utils.split_path(pl.filename))) .. '\n'
|
||||
end
|
||||
state.playlist = s
|
||||
end
|
||||
|
||||
local function set_tracklist(kind, tlist)
|
||||
zassert(kind == "track-list")
|
||||
local s = ''
|
||||
for _,t in ipairs(tlist) do
|
||||
s = s .. ob[t.selected] .. string.sub(t.type, 1, 1)
|
||||
s = s .. t.id .. cb[t.selected] .. ' '
|
||||
|
||||
if (t.title) then
|
||||
s = s .. t.title .. ' '
|
||||
end
|
||||
if (t.lang) then
|
||||
s = s .. t.lang .. ' '
|
||||
end
|
||||
s = s .. '\n'
|
||||
end
|
||||
state.tracklist = s
|
||||
end
|
||||
|
||||
local function set_chapter_list(kind, c)
|
||||
zassert(kind == "chapter-list")
|
||||
if (c and #c > 0) then
|
||||
local s = ''
|
||||
for _,ch in ipairs(c) do
|
||||
s = s .. format_time(ch.time) .. ' '
|
||||
s = s .. ch.title .. '\n'
|
||||
end
|
||||
state.chapters = s
|
||||
state.chapters_raw = c
|
||||
else
|
||||
state.chapters = nil
|
||||
state.chapters_raw = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function call_dmenu(stdin, extra_arg)
|
||||
local cmd = opt.cmd
|
||||
if (extra_arg) then
|
||||
for _,v in ipairs(extra_arg) do
|
||||
table.insert(cmd, v)
|
||||
end
|
||||
end
|
||||
return mp.command_native({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
stdin_data = stdin,
|
||||
capture_stdout = true,
|
||||
args = cmd
|
||||
})
|
||||
end
|
||||
|
||||
local function menu_playlist()
|
||||
if (state.playlist == nil) then
|
||||
return
|
||||
end
|
||||
local narg = nil
|
||||
if (opt.preselect and state.playlist_current ~= nil) then
|
||||
narg = { "-n", tostring(state.playlist_current - 1) }
|
||||
end
|
||||
local r = call_dmenu(state.playlist, narg)
|
||||
if (r.status == 0 and string.len(r.stdout) > 2) then
|
||||
s = string.match(r.stdout, "[%s%[]*(%d+)")
|
||||
if (tonumber(s)) then
|
||||
mp.set_property("playlist-pos-1", s)
|
||||
else
|
||||
msg.warn("bad playlist position: " .. r.stdout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function menu_tracklist()
|
||||
if (state.tracklist == nil) then
|
||||
return
|
||||
end
|
||||
|
||||
local r = call_dmenu(state.tracklist)
|
||||
if (r.status == 0 and string.len(r.stdout) > 4) then
|
||||
local active = string.sub(r.stdout, 1, 1) == '['
|
||||
local type = string.sub(r.stdout, 2, 2)
|
||||
local cmd = { ['v'] = 'vid', ['a'] = 'audio', ['s'] = 'sub' }
|
||||
local arg = { [false] = string.sub(r.stdout, 3, 3), [true] = 'no' }
|
||||
|
||||
if (cmd[type]) then
|
||||
mp.commandv('set', cmd[type], arg[active])
|
||||
else
|
||||
msg.warn("messed up input: " .. r.stdout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function menu_chapters()
|
||||
if (state.chapters == nil) then
|
||||
return
|
||||
end
|
||||
local narg = nil
|
||||
if (opt.preselect) then
|
||||
local t = mp.get_property_native('time-pos') or 0
|
||||
local n = 0
|
||||
for i,c in ipairs(state.chapters_raw) do
|
||||
if (t > c.time) then
|
||||
n = i - 1
|
||||
end
|
||||
end
|
||||
narg = { "-n", tostring(n) }
|
||||
end
|
||||
|
||||
local r = call_dmenu(state.chapters, narg)
|
||||
if (r.status == 0 and string.len(r.stdout) > 8) then
|
||||
local t = humantime_to_sec(r.stdout)
|
||||
if (t) then
|
||||
mp.set_property("time-pos", t)
|
||||
else
|
||||
msg.warn("bad chapter position: " .. r.stdout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function init()
|
||||
mpopt.read_options(opt, "mdmenu")
|
||||
if (type(opt.cmd) == "string") then -- what a pain
|
||||
local s = opt.cmd
|
||||
opt.cmd = {}
|
||||
for arg in string.gmatch(s, '[^,]+') do
|
||||
table.insert(opt.cmd, arg)
|
||||
end
|
||||
end
|
||||
|
||||
if opt.debug then
|
||||
msg.debug("[ASSERTIONS] enabled")
|
||||
zassert = assert
|
||||
else
|
||||
zassert(false)
|
||||
end
|
||||
|
||||
-- grab mpv's xwindow id
|
||||
if (opt.embed) then
|
||||
-- HACK: mpv doesn't open the window instantly by default.
|
||||
-- so wait for 'vo-configured' to be true before trying to
|
||||
-- grab the xid.
|
||||
mp.observe_property('vo-configured', 'native', grab_xid)
|
||||
end
|
||||
|
||||
mp.observe_property('playlist', 'native', set_playlist)
|
||||
mp.add_key_binding(nil, 'playlist', menu_playlist)
|
||||
|
||||
mp.observe_property('track-list', 'native', set_tracklist)
|
||||
mp.add_key_binding(nil, 'tracklist', menu_tracklist)
|
||||
|
||||
mp.observe_property('chapter-list', 'native', set_chapter_list)
|
||||
mp.add_forced_key_binding(nil, 'chapters', menu_chapters)
|
||||
end
|
||||
|
||||
init()
|
|
@ -0,0 +1,567 @@
|
|||
--[[
|
||||
This file is part of mfpbar.
|
||||
|
||||
mfpbar is free software: you can redistribute it and/or modify it
|
||||
under the terms of the GNU Affero General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
mfpbar is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with mfpbar. If not, see <https://www.gnu.org/licenses/>.
|
||||
]]
|
||||
|
||||
local msg = require('mp.msg')
|
||||
local utils = require('mp.utils')
|
||||
local mpopt = require('mp.options')
|
||||
|
||||
-- poor man's enum
|
||||
|
||||
local pbar_uninit = 0
|
||||
local pbar_hidden = 1
|
||||
local pbar_minimized = 2
|
||||
local pbar_active = 3
|
||||
|
||||
-- globals
|
||||
|
||||
local state = {
|
||||
osd = nil,
|
||||
dpy_w = 0,
|
||||
dpy_h = 0,
|
||||
pbar = pbar_uninit,
|
||||
mouse = nil,
|
||||
mouse_prev = nil,
|
||||
cached_ranges = nil,
|
||||
duration = nil,
|
||||
chapters = nil,
|
||||
timeout = nil,
|
||||
time_observed = false,
|
||||
press_bounded = false,
|
||||
fullscreen = false,
|
||||
thumbfast = {
|
||||
width = 0,
|
||||
height = 0,
|
||||
disabled = true,
|
||||
available = false
|
||||
},
|
||||
}
|
||||
|
||||
local opt = {
|
||||
pbar_height = 2,
|
||||
pbar_minimized_height = 0.5,
|
||||
pbar_color = "CCCCCC",
|
||||
pbar_bg_alpha = "3F",
|
||||
pbar_fullscreen_hide = true,
|
||||
cachebar_height = 0.24,
|
||||
cachebar_color = "1C6C89",
|
||||
cachebar_uncached_color = "CC3A2A",
|
||||
cachebar_uncached_alpha = "70",
|
||||
-- TODO: allow selecting "duration" as well ?
|
||||
timeline_rhs = "time-remaining",
|
||||
hover_bar_color = "BDAE93",
|
||||
font_size = 16,
|
||||
font_pad = 4, -- TODO: rename this to hpad ?
|
||||
-- TODO: add a configurable vpad as well ?
|
||||
font_border_width = 2,
|
||||
font_border_color = "000000",
|
||||
proximity = 40,
|
||||
preview_border_width = 2,
|
||||
preview_border_color = "BDAE93",
|
||||
chapter_marker_size = 3,
|
||||
chapter_marker_color = "BDAE93",
|
||||
chapter_marker_border_width = 1,
|
||||
chapter_marker_border_color = "161616",
|
||||
minimize_timeout = 3,
|
||||
|
||||
debug = false,
|
||||
}
|
||||
|
||||
local zassert = function() end
|
||||
|
||||
-- function implementation
|
||||
|
||||
-- ASS uses BBGGRR format, which fucking sucks
|
||||
local function rgb_to_ass(color)
|
||||
if (not string.len(color) == 6) then
|
||||
msg.error("Invalid color: " .. color)
|
||||
return "FFFFFF"
|
||||
end
|
||||
local r = string.sub(color, 1, 2)
|
||||
local g = string.sub(color, 3, 4)
|
||||
local b = string.sub(color, 5, 6)
|
||||
return string.upper(b .. g .. r)
|
||||
end
|
||||
|
||||
local function grab_chapter_name_at(sec)
|
||||
zassert(state.chapters)
|
||||
local name = nil
|
||||
local psec = -1
|
||||
for _, c in ipairs(state.chapters) do
|
||||
if (sec > c.time) then
|
||||
name = c.title
|
||||
end
|
||||
zassert(psec <= c.time)
|
||||
psec = c.time
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
local function format_time(t)
|
||||
local h = math.floor(t / (60 * 60))
|
||||
t = t - (h * 60 * 60)
|
||||
local m = math.floor(t / 60)
|
||||
local s = t - (m * 60)
|
||||
return string.format("%.2d:%.2d:%.2d", h, m, s)
|
||||
end
|
||||
|
||||
local function round(n)
|
||||
zassert(n >= 0)
|
||||
return math.floor(n + 0.5)
|
||||
end
|
||||
|
||||
local function clamp(n, min, max)
|
||||
return math.min(math.max(n, min), max)
|
||||
end
|
||||
|
||||
local function hover_to_sec(mx, dw, duration)
|
||||
zassert(duration)
|
||||
local n = duration * ((mx + 0.5) / dw)
|
||||
return clamp(n, 0, duration)
|
||||
end
|
||||
|
||||
local function render()
|
||||
state.osd:update()
|
||||
state.osd.data = nil
|
||||
end
|
||||
|
||||
local function draw_append(text)
|
||||
if (state.osd.data == nil) then
|
||||
state.osd.data = text
|
||||
else
|
||||
state.osd.data = state.osd.data .. '\n' .. text
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_rect_point(x0, y0, x1, y1, x2, y2, x3, y3, color, opt)
|
||||
local s = '{\\pos(0, 0)}'
|
||||
opt = opt or {}
|
||||
s = s .. '{\\1c&' .. color .. '&}'
|
||||
s = s .. '{\\1a&' .. (opt.alpha or "00") .. '&}'
|
||||
s = s .. '{\\bord' .. (opt.bw or '0') .. '}'
|
||||
s = s .. '{\\3c&' .. (opt.bcolor or "000000") .. '&}'
|
||||
s = s .. string.format(
|
||||
'{\\p1}m %d %d l %d %d %d %d %d %d{\\p0}',
|
||||
x0, y0, x1, y1, x2, y2, x3, y3
|
||||
)
|
||||
draw_append(s)
|
||||
end
|
||||
|
||||
local function draw_rect(x, y, w, h, color, opt)
|
||||
draw_rect_point(
|
||||
x, y,
|
||||
x + w, y,
|
||||
x + w, y + h,
|
||||
x, y + h,
|
||||
color, opt
|
||||
)
|
||||
end
|
||||
|
||||
local function draw_text(x, y, size, text, opt)
|
||||
local s = string.format('{\\pos(%d, %d)}{\\fs%d}', x, y, size)
|
||||
opt = opt or {}
|
||||
s = s .. '{\\bord' .. (opt.bw or '0') .. '}'
|
||||
s = s .. '{\\3c&' .. (opt.bcolor or "000000") .. '&}'
|
||||
s = s .. text
|
||||
draw_append(s)
|
||||
end
|
||||
|
||||
-- TODO: make this less janky.
|
||||
local function pbar_draw()
|
||||
local dpy_w = state.dpy_w
|
||||
local dpy_h = state.dpy_h
|
||||
local ypos = 0
|
||||
local play_pos = mp.get_property_native("percent-pos")
|
||||
local duration = state.duration
|
||||
local clist = state.chapters
|
||||
|
||||
zassert(state.pbar == pbar_minimized or state.pbar == pbar_active)
|
||||
|
||||
if (play_pos == nil or dpy_w == 0 or dpy_h == 0) then
|
||||
return
|
||||
end
|
||||
|
||||
-- L0: playback cursor
|
||||
local pb_h = state.pbar == pbar_minimized and opt.pbar_minimized_height or opt.pbar_height
|
||||
zassert(pb_h > 0)
|
||||
pb_h = dpy_h * (pb_h / 100)
|
||||
pb_h = math.max(round(pb_h), 4)
|
||||
local pb_w = dpy_w * (play_pos/100.0)
|
||||
local pb_y = dpy_h - (pb_h + ypos)
|
||||
draw_rect(0, pb_y, pb_w, pb_h, opt.pbar_color)
|
||||
draw_rect(pb_w, pb_y, dpy_w - pb_w, pb_h, "000000", { alpha = opt.pbar_bg_alpha })
|
||||
ypos = ypos + pb_h
|
||||
|
||||
if (duration) then
|
||||
-- L1: cache cusor
|
||||
if (state.cached_ranges and opt.cachebar_height > 0) then
|
||||
zassert(#state.cached_ranges > 0)
|
||||
local ch = dpy_h * (opt.cachebar_height / 100)
|
||||
ch = math.max(round(ch), 2)
|
||||
draw_rect(
|
||||
0, dpy_h - (ch + ypos), dpy_w, ch,
|
||||
opt.cachebar_uncached_color,
|
||||
{ alpha = opt.cachebar_uncached_alpha }
|
||||
)
|
||||
for _, range in ipairs(state.cached_ranges) do
|
||||
local s = range['start']
|
||||
local e = range['end']
|
||||
local sp = dpy_w * (s / duration)
|
||||
local ep = (dpy_w * (e / duration)) - sp
|
||||
|
||||
draw_rect(sp, dpy_h - (ch + ypos), ep, ch, opt.cachebar_color)
|
||||
end
|
||||
ypos = ypos + ch
|
||||
end
|
||||
|
||||
-- L0-???: chapters
|
||||
if (clist and opt.chapter_marker_size > 0) then
|
||||
zassert(#clist > 0)
|
||||
local bw = opt.chapter_marker_border_width
|
||||
local tw = opt.chapter_marker_size
|
||||
local miny = tw + bw + 1 -- +1 for pad
|
||||
local y = dpy_h - math.max(pb_h / 2, miny)
|
||||
for _, c in ipairs(clist) do
|
||||
local x = dpy_w * (c.time / duration)
|
||||
draw_rect_point(
|
||||
x - tw, y,
|
||||
x, y - tw,
|
||||
x + tw, y,
|
||||
x, y + tw,
|
||||
opt.chapter_marker_color,
|
||||
{ bw = bw, bcolor = opt.chapter_marker_border_color }
|
||||
)
|
||||
end
|
||||
ypos = math.max(ypos, dpy_h - (y + tw + bw))
|
||||
end
|
||||
end
|
||||
|
||||
if (state.pbar == pbar_active) then
|
||||
local fs = opt.font_size
|
||||
local pad = opt.font_pad
|
||||
local fopt = { bw = opt.font_border_width, bcolor = opt.font_border_color }
|
||||
|
||||
-- L2: timeline
|
||||
-- LHS: current playback position
|
||||
local time = mp.get_property_osd("time-pos", "00:00:00")
|
||||
draw_text(pad, dpy_h - (ypos + fs), fs, time, fopt)
|
||||
-- RHS: time/playback remaining
|
||||
local rem = "-" .. mp.get_property_osd(opt.timeline_rhs, "99:99:99")
|
||||
draw_text(dpy_w - pad, dpy_h - (ypos + fs), fs, "{\\an9}" .. rem, fopt)
|
||||
ypos = ypos + fs + (fopt.bw * 2)
|
||||
|
||||
if (duration) then
|
||||
zassert(state.mouse)
|
||||
|
||||
-- L0-2: hovered timeline
|
||||
local hover_sec = hover_to_sec(state.mouse.x, dpy_w, duration)
|
||||
local hover_text = format_time(hover_sec)
|
||||
draw_rect(
|
||||
math.max(state.mouse.x - 1, 0), dpy_h - ypos,
|
||||
2, ypos, opt.hover_bar_color
|
||||
)
|
||||
local fw = fs * 2 -- guesstimate ¯\_(ツ)_/¯
|
||||
local x = clamp(state.mouse.x, pad + fw, dpy_w - (pad + fw))
|
||||
draw_text(
|
||||
x, dpy_h - (ypos + fs), fs,
|
||||
"{\\an8}" .. hover_text, fopt
|
||||
)
|
||||
ypos = ypos + fs + (fopt.bw * 2)
|
||||
|
||||
-- L3: chapter name
|
||||
local cname = clist and grab_chapter_name_at(hover_sec) or nil
|
||||
if cname then
|
||||
zassert(cname)
|
||||
local fw = string.len(cname) * fs * 0.28 -- guesstimate again
|
||||
local x = clamp(state.mouse.x, pad + fw, dpy_w - (pad + fw))
|
||||
draw_text(
|
||||
x, dpy_h - (ypos + fs),
|
||||
fs, "{\\an8}" .. cname, fopt
|
||||
)
|
||||
ypos = ypos + fs + (fopt.bw * 2)
|
||||
end
|
||||
|
||||
-- L4: preview thumbnail
|
||||
if not state.thumbfast.disabled then
|
||||
local pw = opt.preview_border_width
|
||||
local hpad = 4 + pw
|
||||
local tw = state.thumbfast.width
|
||||
local th = state.thumbfast.height
|
||||
local y = dpy_h - (ypos + th + pw)
|
||||
local x = state.mouse.x - (tw / 2)
|
||||
x = clamp(x, hpad, dpy_w - (hpad + tw))
|
||||
mp.commandv(
|
||||
"script-message-to", "thumbfast", "thumb",
|
||||
hover_sec, x, y
|
||||
)
|
||||
ypos = ypos + th + pw
|
||||
|
||||
-- L4: preview border
|
||||
if pw > 0 then
|
||||
local c = opt.preview_border_color
|
||||
draw_rect(
|
||||
x, y, tw, th, "161616",
|
||||
{ alpha = "7F", bw = pw, bcolor = c }
|
||||
)
|
||||
ypos = ypos + pw
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
render()
|
||||
end
|
||||
|
||||
local function pbar_pressed()
|
||||
zassert(state.mouse.hover)
|
||||
zassert(state.pbar == pbar_active)
|
||||
if (state.duration) then
|
||||
mp.set_property("time-pos", hover_to_sec(
|
||||
state.mouse.x, state.dpy_w, state.duration
|
||||
));
|
||||
end
|
||||
end
|
||||
|
||||
local function pbar_update(next_state)
|
||||
local dpy_w = state.dpy_w
|
||||
local dpy_h = state.dpy_h
|
||||
|
||||
if (dpy_w == 0 or dpy_h == 0 or
|
||||
state.pbar == next_state or state.pbar == pbar_uninit)
|
||||
then
|
||||
return
|
||||
end
|
||||
|
||||
zassert(dpy_w > 0)
|
||||
zassert(dpy_h > 0)
|
||||
|
||||
local statestr = {
|
||||
[pbar_uninit] = "uninit", [pbar_active] = "active",
|
||||
[pbar_minimized] = "minimized", [pbar_hidden] = "hidden"
|
||||
}
|
||||
msg.debug('[UPDATE]: ', statestr[state.pbar], '=> ', statestr[next_state]);
|
||||
|
||||
-- TODO: reduce latency when pbar is active
|
||||
if (next_state == pbar_active) then
|
||||
state.pbar = pbar_active
|
||||
pbar_draw()
|
||||
if (not state.press_bounded) then
|
||||
mp.add_forced_key_binding('mbtn_left', 'pbar_pressed', pbar_pressed)
|
||||
state.press_bounded = true
|
||||
end
|
||||
if (not state.time_observed) then
|
||||
mp.observe_property("time-pos", nil, pbar_draw)
|
||||
state.time_observed = true
|
||||
end
|
||||
else
|
||||
if (next_state == pbar_minimized) then
|
||||
zassert(opt.pbar_minimized_height > 0)
|
||||
state.pbar = pbar_minimized
|
||||
pbar_draw()
|
||||
if (not state.time_observed) then
|
||||
mp.observe_property("time-pos", nil, pbar_draw)
|
||||
state.time_observed = true
|
||||
end
|
||||
elseif (next_state == pbar_hidden) then
|
||||
zassert(state.pbar ~= pbar_hidden)
|
||||
state.pbar = pbar_hidden
|
||||
state.osd.data = '' -- clear everything
|
||||
render()
|
||||
zassert(state.time_observed)
|
||||
mp.unobserve_property(pbar_draw)
|
||||
state.time_observed = false
|
||||
else
|
||||
zassert(false, "unreachable")
|
||||
end
|
||||
|
||||
if (state.press_bounded) then
|
||||
mp.remove_key_binding('pbar_pressed')
|
||||
state.press_bounded = false
|
||||
end
|
||||
state.mouse = nil
|
||||
if (state.thumbfast.available) then
|
||||
mp.commandv("script-message-to", "thumbfast", "clear")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function pbar_minimize_or_hide()
|
||||
msg.debug("[MIN-HIDE]")
|
||||
if (opt.pbar_minimized_height > 0 and not (opt.pbar_fullscreen_hide and state.fullscreen)) then
|
||||
pbar_update(pbar_minimized)
|
||||
else
|
||||
pbar_update(pbar_hidden)
|
||||
end
|
||||
end
|
||||
|
||||
local function mouse_isactive(m)
|
||||
return m.hover and math.abs(m.y - state.dpy_h) < opt.proximity
|
||||
end
|
||||
|
||||
local function update_mouse_pos(kind, mouse)
|
||||
zassert(kind == "mouse-pos")
|
||||
state.mouse_prev = state.mouse or { hover = false }
|
||||
state.mouse = mouse
|
||||
msg.debug('[MOUSE] hover = ', mouse.hover, ' x = ', mouse.x, ' y = ', mouse.y)
|
||||
|
||||
local dpy_w = state.dpy_w
|
||||
local dpy_h = state.dpy_h
|
||||
|
||||
if (dpy_w == 0 or dpy_h == 0) then
|
||||
return
|
||||
end
|
||||
|
||||
zassert(dpy_w > 0)
|
||||
zassert(dpy_h > 0)
|
||||
zassert(mouse)
|
||||
|
||||
-- TODO: ensure there's enough height to draw our stuff ?
|
||||
if (mouse_isactive(state.mouse_prev) and mouse_isactive(mouse)) then
|
||||
-- TODO: a better way to do this without killing/resuming a
|
||||
-- timer on each mouse update?
|
||||
state.timeout:kill()
|
||||
state.timeout:resume()
|
||||
pbar_update(pbar_active)
|
||||
else
|
||||
state.timeout:kill()
|
||||
pbar_minimize_or_hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function update_fullscreen(kind, fs)
|
||||
zassert(kind == "fullscreen")
|
||||
state.fullscreen = fs
|
||||
msg.debug('[FULLSCREEN] fs = ', fs)
|
||||
pbar_minimize_or_hide()
|
||||
end
|
||||
|
||||
local function update_focus(kind, foc)
|
||||
zassert(kind == "focused")
|
||||
msg.debug('[FOCUS] focus = ', foc)
|
||||
state.mouse_prev = { hover = false, x = 0, y = 0 }
|
||||
end
|
||||
|
||||
local function set_dpy_size(kind, osd)
|
||||
zassert(kind == "osd-dimensions")
|
||||
state.dpy_w = osd.w
|
||||
state.osd.res_x = osd.w
|
||||
state.dpy_h = osd.h
|
||||
state.osd.res_y = osd.h
|
||||
msg.debug('[DPY] w = ', osd.w, ' h = ', osd.h)
|
||||
|
||||
-- HACK: ensure we don't obstruct the console (excluding the preview and hovered timeline)
|
||||
-- the shared_script_property_* functions are documented as undocumented :)
|
||||
-- and users are discouraged to use them, but whatever...
|
||||
local b = (opt.font_size + (opt.font_border_width * 2) + 8) / state.dpy_h -- +8 padding
|
||||
b = b + ((opt.pbar_minimized_height + opt.cachebar_height) / 100.0)
|
||||
utils.shared_script_property_set(
|
||||
'osc-margins',
|
||||
string.format('%f,%f,%f,%f', 0, 0, 0, b)
|
||||
)
|
||||
end
|
||||
|
||||
local function set_cache_state(kind, c)
|
||||
zassert(kind == "demuxer-cache-state")
|
||||
if (c == nil) then
|
||||
state.cached_ranges = nil
|
||||
else
|
||||
local r = c['seekable-ranges']
|
||||
if #r > 0 then
|
||||
state.cached_ranges = r
|
||||
else
|
||||
state.cached_ranges = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function set_duration(kind, d)
|
||||
zassert(kind == "duration")
|
||||
state.duration = d
|
||||
end
|
||||
|
||||
local function set_chapter_list(kind, c)
|
||||
zassert(kind == "chapter-list")
|
||||
if (c and #c > 0) then
|
||||
state.chapters = c
|
||||
table.sort(state.chapters, function(a, b) return a.time < b.time end)
|
||||
else
|
||||
state.chapters = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function set_thumbfast(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
|
||||
state.thumbfast = data
|
||||
end
|
||||
end
|
||||
|
||||
local function pbar_init(kind, thing)
|
||||
zassert(kind == 'vo-configured')
|
||||
msg.debug("[VO-CONFIGURED]", thing, state.pbar)
|
||||
|
||||
if thing then
|
||||
zassert(state.pbar == pbar_uninit)
|
||||
state.pbar = pbar_hidden
|
||||
if (opt.pbar_minimized_height > 0) then
|
||||
pbar_update(pbar_minimized)
|
||||
end
|
||||
mp.unobserve_property(pbar_init)
|
||||
end
|
||||
end
|
||||
|
||||
local function init()
|
||||
mpopt.read_options(opt, "mfpbar")
|
||||
for k,v in pairs(opt) do
|
||||
if string.find(k, "_color$") then
|
||||
opt[k] = rgb_to_ass(v)
|
||||
end
|
||||
end
|
||||
|
||||
if opt.debug then
|
||||
msg.debug("[ASSERTIONS] enabled")
|
||||
zassert = assert
|
||||
else
|
||||
zassert(false)
|
||||
end
|
||||
|
||||
state.osd = mp.create_osd_overlay("ass-events")
|
||||
mp.observe_property("osd-dimensions", "native", set_dpy_size)
|
||||
mp.observe_property('demuxer-cache-state', 'native', set_cache_state)
|
||||
mp.observe_property('duration', 'native', set_duration)
|
||||
mp.observe_property('chapter-list', 'native', set_chapter_list)
|
||||
mp.register_script_message("thumbfast-info", set_thumbfast)
|
||||
mp.observe_property('fullscreen', 'native', update_fullscreen)
|
||||
mp.observe_property('focused', 'native', update_focus)
|
||||
|
||||
-- NOTE: mouse-pos doesn't work mpv versions older than v33
|
||||
mp.observe_property("mouse-pos", "native", update_mouse_pos)
|
||||
|
||||
if (opt.minimize_timeout > 0) then
|
||||
state.timeout = mp.add_timeout(opt.minimize_timeout, pbar_minimize_or_hide)
|
||||
state.timeout:kill() -- update_mouse_pos() will kill/resume this as needed
|
||||
else
|
||||
state.timeout = { kill = function() end, resume = function() end }
|
||||
end
|
||||
|
||||
-- HACK: mpv doesn't open the window instantly by default.
|
||||
-- so wait for 'vo-configured' as a hook for when the window opens.
|
||||
mp.observe_property('vo-configured', 'native', pbar_init)
|
||||
end
|
||||
|
||||
init()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,778 @@
|
|||
-- 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,
|
||||
|
||||
-- Custom path to the mpv executable
|
||||
mpv_path = "mpv"
|
||||
}
|
||||
|
||||
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
|
||||
local pre_0_33_0 = true
|
||||
|
||||
function subprocess(args, async, callback)
|
||||
callback = callback or function() end
|
||||
|
||||
if not pre_0_30_0 then
|
||||
if async then
|
||||
return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback)
|
||||
else
|
||||
return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args})
|
||||
end
|
||||
else
|
||||
if async then
|
||||
return mp.utils.subprocess_detached({args = args}, callback)
|
||||
else
|
||||
return mp.utils.subprocess({args = args})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local winapi = {}
|
||||
if options.direct_io then
|
||||
local ffi_loaded, ffi = pcall(require, "ffi")
|
||||
if ffi_loaded then
|
||||
winapi = {
|
||||
ffi = ffi,
|
||||
C = ffi.C,
|
||||
bit = require("bit"),
|
||||
socket_wc = "",
|
||||
|
||||
-- WinAPI constants
|
||||
CP_UTF8 = 65001,
|
||||
GENERIC_WRITE = 0x40000000,
|
||||
OPEN_EXISTING = 3,
|
||||
FILE_FLAG_WRITE_THROUGH = 0x80000000,
|
||||
FILE_FLAG_NO_BUFFERING = 0x20000000,
|
||||
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
|
||||
|
||||
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
|
||||
|
||||
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
|
||||
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
|
||||
}
|
||||
-- cache flags used in run() to avoid bor() call
|
||||
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
|
||||
|
||||
ffi.cdef[[
|
||||
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
|
||||
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
|
||||
bool __stdcall CloseHandle(void *hObject);
|
||||
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
|
||||
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
|
||||
]]
|
||||
|
||||
winapi.MultiByteToWideChar = function(MultiByteStr)
|
||||
if MultiByteStr then
|
||||
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
|
||||
if utf16_len > 0 then
|
||||
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
|
||||
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
|
||||
return utf16_str
|
||||
end
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
else
|
||||
options.direct_io = false
|
||||
end
|
||||
end
|
||||
|
||||
local spawned = false
|
||||
local network = false
|
||||
local disabled = false
|
||||
local force_disabled = false
|
||||
local spawn_waiting = false
|
||||
local spawn_working = false
|
||||
|
||||
local dirty = false
|
||||
|
||||
local x = nil
|
||||
local y = nil
|
||||
local last_x = x
|
||||
local last_y = y
|
||||
|
||||
local last_seek_time = nil
|
||||
|
||||
local effective_w = options.max_width
|
||||
local effective_h = options.max_height
|
||||
local real_w = nil
|
||||
local real_h = nil
|
||||
local last_real_w = nil
|
||||
local last_real_h = nil
|
||||
|
||||
local script_name = nil
|
||||
|
||||
local show_thumbnail = false
|
||||
|
||||
local filters_reset = {["lavfi-crop"]=true, crop=true}
|
||||
local filters_runtime = {hflip=true, vflip=true}
|
||||
local filters_all = 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 allow_fast_seek = true
|
||||
|
||||
local client_script = [=[
|
||||
#!/usr/bin/env bash
|
||||
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
|
||||
trap "kill 0" EXIT
|
||||
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
|
||||
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
|
||||
]=]
|
||||
|
||||
local function get_os()
|
||||
local raw_os_name = ""
|
||||
|
||||
if jit and jit.os and jit.arch then
|
||||
raw_os_name = jit.os
|
||||
else
|
||||
if package.config:sub(1,1) == "\\" then
|
||||
-- Windows
|
||||
local env_OS = os.getenv("OS")
|
||||
if env_OS then
|
||||
raw_os_name = env_OS
|
||||
end
|
||||
else
|
||||
raw_os_name = subprocess({"uname", "-s"}).stdout
|
||||
end
|
||||
end
|
||||
|
||||
raw_os_name = (raw_os_name):lower()
|
||||
|
||||
local os_patterns = {
|
||||
["windows"] = "Windows",
|
||||
["linux"] = "Linux",
|
||||
|
||||
["osx"] = "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()
|
||||
local path_separator = os_name == "Windows" and "\\" or "/"
|
||||
|
||||
if options.socket == "" then
|
||||
if os_name == "Windows" then
|
||||
options.socket = "thumbfast"
|
||||
else
|
||||
options.socket = "/tmp/thumbfast"
|
||||
end
|
||||
end
|
||||
|
||||
if options.thumbnail == "" then
|
||||
if os_name == "Windows" then
|
||||
options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
|
||||
else
|
||||
options.thumbnail = "/tmp/thumbfast.out"
|
||||
end
|
||||
end
|
||||
|
||||
local unique = mp.utils.getpid()
|
||||
|
||||
options.socket = options.socket .. unique
|
||||
options.thumbnail = options.thumbnail .. unique
|
||||
|
||||
if options.direct_io then
|
||||
if os_name == "Windows" then
|
||||
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
|
||||
end
|
||||
|
||||
if winapi.socket_wc == "" then
|
||||
options.direct_io = false
|
||||
end
|
||||
end
|
||||
|
||||
local mpv_path = options.mpv_path
|
||||
local libmpv = false
|
||||
|
||||
if mpv_path == "mpv" and os_name == "Mac" and unique then
|
||||
-- TODO: look into ~~osxbundle/
|
||||
mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
|
||||
if mpv_path ~= "mpv" then
|
||||
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
|
||||
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
|
||||
if mpv_bin and mpv_bin.is_file then
|
||||
mpv_path = "/usr/local/mpv"
|
||||
else
|
||||
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
|
||||
if mpv_app and mpv_app.is_file then
|
||||
mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||
else
|
||||
mp.msg.warn("drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function 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) 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
|
||||
|
||||
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, "--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="..(mp.get_property_number("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 == "Mac" and mp.get_property("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)
|
||||
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, "--scripts="..client_script_path)
|
||||
end
|
||||
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
|
||||
mp.msg.error("mpv subprocess create failed")
|
||||
if not spawn_working then -- notify users of required configuration
|
||||
if options.mpv_path == "mpv" then
|
||||
if 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 ~= "Mac" then
|
||||
force_disabled = true
|
||||
info(real_w or effective_w, real_h or effective_h)
|
||||
end
|
||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||
end
|
||||
else
|
||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||
if os_name == "Windows" then
|
||||
mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||
mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
|
||||
end
|
||||
end
|
||||
else
|
||||
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
|
||||
-- found ImPlay but not defined in config
|
||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||
end
|
||||
end
|
||||
elseif success == true and (result.status == 0 or result.status == -2) then
|
||||
if not spawn_working and libmpv and options.mpv_path ~= mpv_path then
|
||||
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
|
||||
end
|
||||
spawn_working = true
|
||||
spawn_waiting = false
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
local function run(command)
|
||||
if not spawned then return end
|
||||
|
||||
if options.direct_io then
|
||||
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil)
|
||||
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
|
||||
local buf = command .. "\n"
|
||||
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
|
||||
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
|
||||
winapi.C.CloseHandle(hPipe)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local file = nil
|
||||
if os_name == "Windows" then
|
||||
file = io.open("\\\\.\\pipe\\"..options.socket, "r+")
|
||||
elseif pre_0_33_0 then
|
||||
subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket})
|
||||
return
|
||||
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(allow_fast_seek)
|
||||
seek_period_counter = 1
|
||||
else
|
||||
if seek_period_counter == 2 then
|
||||
if allow_fast_seek then
|
||||
seek_timer:kill()
|
||||
seek()
|
||||
end
|
||||
else seek_period_counter = seek_period_counter + 1 end
|
||||
end
|
||||
end)
|
||||
seek_timer:kill()
|
||||
|
||||
local function request_seek()
|
||||
if seek_timer:is_enabled() then
|
||||
seek_period_counter = 0
|
||||
else
|
||||
seek_timer:resume()
|
||||
seek(allow_fast_seek)
|
||||
seek_period_counter = 1
|
||||
end
|
||||
end
|
||||
|
||||
local function check_new_thumb()
|
||||
-- the slave might start writing to the file after checking existance and
|
||||
-- validity but before actually moving the file, so move to a temporary
|
||||
-- location before validity check to make sure everything stays consistant
|
||||
-- and valid thumbnails don't get overwritten by invalid ones
|
||||
local tmp = options.thumbnail..".tmp"
|
||||
move_file(options.thumbnail, tmp)
|
||||
local finfo = mp.utils.file_info(tmp)
|
||||
if not finfo then return false end
|
||||
spawn_waiting = false
|
||||
local w, h = real_res(effective_w, effective_h, finfo.size)
|
||||
if w then -- only accept valid thumbnails
|
||||
move_file(tmp, options.thumbnail..".bgra")
|
||||
|
||||
real_w, real_h = w, h
|
||||
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
|
||||
last_real_w, last_real_h = real_w, real_h
|
||||
info(real_w, real_h)
|
||||
end
|
||||
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_time = 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()
|
||||
if not dirty then return end
|
||||
|
||||
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
|
||||
dirty = false
|
||||
end
|
||||
|
||||
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)
|
||||
dirty = true
|
||||
end
|
||||
|
||||
local function mark_dirty()
|
||||
dirty = true
|
||||
end
|
||||
|
||||
local function file_load()
|
||||
libmpv = mp.get_property("current-vo") == "libmpv"
|
||||
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))
|
||||
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
|
||||
|
||||
local function on_duration(prop, val)
|
||||
allow_fast_seek = (val or 30) >= 30
|
||||
end
|
||||
|
||||
mp.observe_property("display-hidpi-scale", "native", mark_dirty)
|
||||
mp.observe_property("video-out-params", "native", mark_dirty)
|
||||
mp.observe_property("vf", "native", mark_dirty)
|
||||
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.add_hook("on_before_start_file", 0, function()
|
||||
pre_0_33_0 = false
|
||||
end)
|
||||
|
||||
mp.register_idle(watch_changes)
|
Loading…
Reference in New Issue