mirror of https://github.com/mpv-player/mpv
2395 lines
75 KiB
Lua
2395 lines
75 KiB
Lua
local assdraw = require 'mp.assdraw'
|
||
local msg = require 'mp.msg'
|
||
local opt = require 'mp.options'
|
||
local utils = require 'mp.utils'
|
||
|
||
--
|
||
-- Parameters
|
||
--
|
||
|
||
-- default user option values
|
||
-- do not touch, change them in osc.conf
|
||
local user_opts = {
|
||
showwindowed = true, -- show OSC when windowed?
|
||
showfullscreen = true, -- show OSC when fullscreen?
|
||
scalewindowed = 1, -- scaling of the controller when windowed
|
||
scalefullscreen = 1, -- scaling of the controller when fullscreen
|
||
scaleforcedwindow = 2, -- scaling when rendered on a forced window
|
||
vidscale = true, -- scale the controller with the video?
|
||
valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom)
|
||
halign = 0, -- horizontal alignment, -1 (left) to 1 (right)
|
||
barmargin = 0, -- vertical margin of top/bottombar
|
||
boxalpha = 80, -- alpha of the background box,
|
||
-- 0 (opaque) to 255 (fully transparent)
|
||
hidetimeout = 500, -- duration in ms until the OSC hides if no
|
||
-- mouse movement. enforced non-negative for the
|
||
-- user, but internally negative is "always-on".
|
||
fadeduration = 200, -- duration of fade out in ms, 0 = no fade
|
||
deadzonesize = 0.5, -- size of deadzone
|
||
minmousemove = 0, -- minimum amount of pixels the mouse has to
|
||
-- move between ticks to make the OSC show up
|
||
iamaprogrammer = false, -- use native mpv values and disable OSC
|
||
-- internal track list management (and some
|
||
-- functions that depend on it)
|
||
layout = "bottombar",
|
||
seekbarstyle = "bar", -- slider (diamond marker), knob (circle
|
||
-- marker with guide), or bar (fill)
|
||
seekbarkeyframes = true, -- use keyframes when dragging the seekbar
|
||
title = "${media-title}", -- string compatible with property-expansion
|
||
-- to be shown as OSC title
|
||
tooltipborder = 1, -- border of tooltip in bottom/topbar
|
||
timetotal = false, -- display total time instead of remaining time?
|
||
timems = false, -- display timecodes with milliseconds?
|
||
seekranges = true, -- display seek ranges?
|
||
visibility = "auto", -- only used at init to set visibility_mode(...)
|
||
boxmaxchars = 80, -- title crop threshold for box layout
|
||
}
|
||
|
||
-- read_options may modify hidetimeout, so save the original default value in
|
||
-- case the user set hidetimeout < 0 and we need the default instead.
|
||
local hidetimeout_def = user_opts.hidetimeout
|
||
-- read options from config and command-line
|
||
opt.read_options(user_opts, "osc")
|
||
if user_opts.hidetimeout < 0 then
|
||
user_opts.hidetimeout = hidetimeout_def
|
||
msg.warn("hidetimeout cannot be negative. Using " .. user_opts.hidetimeout)
|
||
end
|
||
|
||
local osc_param = { -- calculated by osc_init()
|
||
playresy = 0, -- canvas size Y
|
||
playresx = 0, -- canvas size X
|
||
display_aspect = 1,
|
||
unscaled_y = 0,
|
||
areas = {},
|
||
}
|
||
|
||
local osc_styles = {
|
||
bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
|
||
smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
|
||
smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}",
|
||
smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
|
||
topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
|
||
|
||
elementDown = "{\\1c&H999999}",
|
||
timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
|
||
vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
|
||
box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
|
||
|
||
topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
|
||
smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
|
||
timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
|
||
timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
|
||
vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
|
||
}
|
||
|
||
-- internal states, do not touch
|
||
local state = {
|
||
showtime, -- time of last invocation (last mouse move)
|
||
osc_visible = false,
|
||
anistart, -- time when the animation started
|
||
anitype, -- current type of animation
|
||
animation, -- current animation alpha
|
||
mouse_down_counter = 0, -- used for softrepeat
|
||
active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
|
||
active_event_source = nil, -- the "button" that issued the current event
|
||
rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
|
||
tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
|
||
mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
|
||
initREQ = false, -- is a re-init request pending?
|
||
last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
|
||
message_text,
|
||
message_timeout,
|
||
fullscreen = false,
|
||
timer = nil,
|
||
cache_idle = false,
|
||
idle = false,
|
||
enabled = true,
|
||
input_enabled = true,
|
||
showhide_enabled = false,
|
||
dmx_cache = 0,
|
||
}
|
||
|
||
|
||
|
||
|
||
--
|
||
-- Helperfunctions
|
||
--
|
||
|
||
-- scale factor for translating between real and virtual ASS coordinates
|
||
function get_virt_scale_factor()
|
||
local w, h = mp.get_osd_size()
|
||
if w <= 0 or h <= 0 then
|
||
return 0, 0
|
||
end
|
||
return osc_param.playresx / w, osc_param.playresy / h
|
||
end
|
||
|
||
-- return mouse position in virtual ASS coordinates (playresx/y)
|
||
function get_virt_mouse_pos()
|
||
local sx, sy = get_virt_scale_factor()
|
||
local x, y = mp.get_mouse_pos()
|
||
return x * sx, y * sy
|
||
end
|
||
|
||
function set_virt_mouse_area(x0, y0, x1, y1, name)
|
||
local sx, sy = get_virt_scale_factor()
|
||
mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
|
||
end
|
||
|
||
function scale_value(x0, x1, y0, y1, val)
|
||
local m = (y1 - y0) / (x1 - x0)
|
||
local b = y0 - (m * x0)
|
||
return (m * val) + b
|
||
end
|
||
|
||
-- returns hitbox spanning coordinates (top left, bottom right corner)
|
||
-- according to alignment
|
||
function get_hitbox_coords(x, y, an, w, h)
|
||
|
||
local alignments = {
|
||
[1] = function () return x, y-h, x+w, y end,
|
||
[2] = function () return x-(w/2), y-h, x+(w/2), y end,
|
||
[3] = function () return x-w, y-h, x, y end,
|
||
|
||
[4] = function () return x, y-(h/2), x+w, y+(h/2) end,
|
||
[5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
|
||
[6] = function () return x-w, y-(h/2), x, y+(h/2) end,
|
||
|
||
[7] = function () return x, y, x+w, y+h end,
|
||
[8] = function () return x-(w/2), y, x+(w/2), y+h end,
|
||
[9] = function () return x-w, y, x, y+h end,
|
||
}
|
||
|
||
return alignments[an]()
|
||
end
|
||
|
||
function get_hitbox_coords_geo(geometry)
|
||
return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
|
||
geometry.w, geometry.h)
|
||
end
|
||
|
||
function get_element_hitbox(element)
|
||
return element.hitbox.x1, element.hitbox.y1,
|
||
element.hitbox.x2, element.hitbox.y2
|
||
end
|
||
|
||
function mouse_hit(element)
|
||
return mouse_hit_coords(get_element_hitbox(element))
|
||
end
|
||
|
||
function mouse_hit_coords(bX1, bY1, bX2, bY2)
|
||
local mX, mY = get_virt_mouse_pos()
|
||
return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
|
||
end
|
||
|
||
function limit_range(min, max, val)
|
||
if val > max then
|
||
val = max
|
||
elseif val < min then
|
||
val = min
|
||
end
|
||
return val
|
||
end
|
||
|
||
-- translate value into element coordinates
|
||
function get_slider_ele_pos_for(element, val)
|
||
|
||
local ele_pos = scale_value(
|
||
element.slider.min.value, element.slider.max.value,
|
||
element.slider.min.ele_pos, element.slider.max.ele_pos,
|
||
val)
|
||
|
||
return limit_range(
|
||
element.slider.min.ele_pos, element.slider.max.ele_pos,
|
||
ele_pos)
|
||
end
|
||
|
||
-- translates global (mouse) coordinates to value
|
||
function get_slider_value_at(element, glob_pos)
|
||
|
||
local val = scale_value(
|
||
element.slider.min.glob_pos, element.slider.max.glob_pos,
|
||
element.slider.min.value, element.slider.max.value,
|
||
glob_pos)
|
||
|
||
return limit_range(
|
||
element.slider.min.value, element.slider.max.value,
|
||
val)
|
||
end
|
||
|
||
-- get value at current mouse position
|
||
function get_slider_value(element)
|
||
return get_slider_value_at(element, get_virt_mouse_pos())
|
||
end
|
||
|
||
function countone(val)
|
||
if not (user_opts.iamaprogrammer) then
|
||
val = val + 1
|
||
end
|
||
return val
|
||
end
|
||
|
||
-- align: -1 .. +1
|
||
-- frame: size of the containing area
|
||
-- obj: size of the object that should be positioned inside the area
|
||
-- margin: min. distance from object to frame (as long as -1 <= align <= +1)
|
||
function get_align(align, frame, obj, margin)
|
||
return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
|
||
end
|
||
|
||
-- multiplies two alpha values, formular can probably be improved
|
||
function mult_alpha(alphaA, alphaB)
|
||
return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
|
||
end
|
||
|
||
function add_area(name, x1, y1, x2, y2)
|
||
-- create area if needed
|
||
if (osc_param.areas[name] == nil) then
|
||
osc_param.areas[name] = {}
|
||
end
|
||
table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
|
||
end
|
||
|
||
|
||
--
|
||
-- Tracklist Management
|
||
--
|
||
|
||
local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
|
||
|
||
-- updates the OSC internal playlists, should be run each time the track-layout changes
|
||
function update_tracklist()
|
||
local tracktable = mp.get_property_native("track-list", {})
|
||
|
||
-- by osc_id
|
||
tracks_osc = {}
|
||
tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
|
||
-- by mpv_id
|
||
tracks_mpv = {}
|
||
tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
|
||
for n = 1, #tracktable do
|
||
if not (tracktable[n].type == "unknown") then
|
||
local type = tracktable[n].type
|
||
local mpv_id = tonumber(tracktable[n].id)
|
||
|
||
-- by osc_id
|
||
table.insert(tracks_osc[type], tracktable[n])
|
||
|
||
-- by mpv_id
|
||
tracks_mpv[type][mpv_id] = tracktable[n]
|
||
tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
|
||
end
|
||
end
|
||
end
|
||
|
||
-- return a nice list of tracks of the given type (video, audio, sub)
|
||
function get_tracklist(type)
|
||
local msg = "Available " .. nicetypes[type] .. " Tracks: "
|
||
if #tracks_osc[type] == 0 then
|
||
msg = msg .. "none"
|
||
else
|
||
for n = 1, #tracks_osc[type] do
|
||
local track = tracks_osc[type][n]
|
||
local lang, title, selected = "unknown", "", "○"
|
||
if not(track.lang == nil) then lang = track.lang end
|
||
if not(track.title == nil) then title = track.title end
|
||
if (track.id == tonumber(mp.get_property(type))) then
|
||
selected = "●"
|
||
end
|
||
msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title
|
||
end
|
||
end
|
||
return msg
|
||
end
|
||
|
||
-- relatively change the track of given <type> by <next> tracks
|
||
--(+1 -> next, -1 -> previous)
|
||
function set_track(type, next)
|
||
local current_track_mpv, current_track_osc
|
||
if (mp.get_property(type) == "no") then
|
||
current_track_osc = 0
|
||
else
|
||
current_track_mpv = tonumber(mp.get_property(type))
|
||
current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
|
||
end
|
||
local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
|
||
local new_track_mpv
|
||
if new_track_osc == 0 then
|
||
new_track_mpv = "no"
|
||
else
|
||
new_track_mpv = tracks_osc[type][new_track_osc].id
|
||
end
|
||
|
||
mp.commandv("set", type, new_track_mpv)
|
||
|
||
if (new_track_osc == 0) then
|
||
show_message(nicetypes[type] .. " Track: none")
|
||
else
|
||
show_message(nicetypes[type] .. " Track: "
|
||
.. new_track_osc .. "/" .. #tracks_osc[type]
|
||
.. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
|
||
.. (tracks_osc[type][new_track_osc].title or ""))
|
||
end
|
||
end
|
||
|
||
-- get the currently selected track of <type>, OSC-style counted
|
||
function get_track(type)
|
||
local track = mp.get_property(type)
|
||
if track ~= "no" and track ~= nil then
|
||
local tr = tracks_mpv[type][tonumber(track)]
|
||
if tr then
|
||
return tr.osc_id
|
||
end
|
||
end
|
||
return 0
|
||
end
|
||
|
||
|
||
--
|
||
-- Element Management
|
||
--
|
||
|
||
local elements = {}
|
||
|
||
function prepare_elements()
|
||
|
||
-- remove elements without layout or invisble
|
||
local elements2 = {}
|
||
for n, element in pairs(elements) do
|
||
if not (element.layout == nil) and (element.visible) then
|
||
table.insert(elements2, element)
|
||
end
|
||
end
|
||
elements = elements2
|
||
|
||
function elem_compare (a, b)
|
||
return a.layout.layer < b.layout.layer
|
||
end
|
||
|
||
table.sort(elements, elem_compare)
|
||
|
||
|
||
for _,element in pairs(elements) do
|
||
|
||
local elem_geo = element.layout.geometry
|
||
|
||
-- Calculate the hitbox
|
||
local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
|
||
element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
|
||
|
||
local style_ass = assdraw.ass_new()
|
||
|
||
-- prepare static elements
|
||
style_ass:append("{}") -- hack to troll new_event into inserting a \n
|
||
style_ass:new_event()
|
||
style_ass:pos(elem_geo.x, elem_geo.y)
|
||
style_ass:an(elem_geo.an)
|
||
style_ass:append(element.layout.style)
|
||
|
||
element.style_ass = style_ass
|
||
|
||
local static_ass = assdraw.ass_new()
|
||
|
||
|
||
if (element.type == "box") then
|
||
--draw box
|
||
static_ass:draw_start()
|
||
static_ass:round_rect_cw(0, 0, elem_geo.w, elem_geo.h,
|
||
element.layout.box.radius)
|
||
static_ass:draw_stop()
|
||
|
||
|
||
elseif (element.type == "slider") then
|
||
--draw static slider parts
|
||
|
||
local slider_lo = element.layout.slider
|
||
-- offset between element outline and drag-area
|
||
local foV = slider_lo.border + slider_lo.gap
|
||
|
||
-- calculate positions of min and max points
|
||
if (slider_lo.stype == "slider") or
|
||
(slider_lo.stype == "knob") then
|
||
element.slider.min.ele_pos = elem_geo.h / 2
|
||
element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
|
||
|
||
elseif (slider_lo.stype == "bar") then
|
||
element.slider.min.ele_pos =
|
||
slider_lo.border + slider_lo.gap
|
||
element.slider.max.ele_pos =
|
||
elem_geo.w - (slider_lo.border + slider_lo.gap)
|
||
end
|
||
|
||
element.slider.min.glob_pos =
|
||
element.hitbox.x1 + element.slider.min.ele_pos
|
||
element.slider.max.glob_pos =
|
||
element.hitbox.x1 + element.slider.max.ele_pos
|
||
|
||
-- -- --
|
||
|
||
static_ass:draw_start()
|
||
|
||
-- the box
|
||
static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h);
|
||
|
||
-- the "hole"
|
||
static_ass:rect_ccw(slider_lo.border, slider_lo.border,
|
||
elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border)
|
||
|
||
-- marker nibbles
|
||
if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
|
||
local markers = element.slider.markerF()
|
||
for _,marker in pairs(markers) do
|
||
if (marker > element.slider.min.value) and
|
||
(marker < element.slider.max.value) then
|
||
|
||
local s = get_slider_ele_pos_for(element, marker)
|
||
|
||
if (slider_lo.gap > 1) then -- draw triangles
|
||
|
||
local a = slider_lo.gap / 0.5 --0.866
|
||
|
||
--top
|
||
if (slider_lo.nibbles_top) then
|
||
static_ass:move_to(s - (a/2), slider_lo.border)
|
||
static_ass:line_to(s + (a/2), slider_lo.border)
|
||
static_ass:line_to(s, foV)
|
||
end
|
||
|
||
--bottom
|
||
if (slider_lo.nibbles_bottom) then
|
||
static_ass:move_to(s - (a/2),
|
||
elem_geo.h - slider_lo.border)
|
||
static_ass:line_to(s,
|
||
elem_geo.h - foV)
|
||
static_ass:line_to(s + (a/2),
|
||
elem_geo.h - slider_lo.border)
|
||
end
|
||
|
||
else -- draw 2x1px nibbles
|
||
|
||
--top
|
||
if (slider_lo.nibbles_top) then
|
||
static_ass:rect_cw(s - 1, slider_lo.border,
|
||
s + 1, slider_lo.border + slider_lo.gap);
|
||
end
|
||
|
||
--bottom
|
||
if (slider_lo.nibbles_bottom) then
|
||
static_ass:rect_cw(s - 1,
|
||
elem_geo.h -slider_lo.border -slider_lo.gap,
|
||
s + 1, elem_geo.h - slider_lo.border);
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
element.static_ass = static_ass
|
||
|
||
|
||
-- if the element is supposed to be disabled,
|
||
-- style it accordingly and kill the eventresponders
|
||
if not (element.enabled) then
|
||
element.layout.alpha[1] = 136
|
||
element.eventresponder = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
|
||
--
|
||
-- Element Rendering
|
||
--
|
||
|
||
function render_elements(master_ass)
|
||
|
||
for n=1, #elements do
|
||
local element = elements[n]
|
||
|
||
local style_ass = assdraw.ass_new()
|
||
style_ass:merge(element.style_ass)
|
||
|
||
--alpha
|
||
local ar = element.layout.alpha
|
||
if not (state.animation == nil) then
|
||
ar = {}
|
||
for ai, av in pairs(element.layout.alpha) do
|
||
ar[ai] = mult_alpha(av, state.animation)
|
||
end
|
||
end
|
||
|
||
style_ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
|
||
ar[1], ar[2], ar[3], ar[4]))
|
||
|
||
if element.eventresponder and (state.active_element == n) then
|
||
|
||
-- run render event functions
|
||
if not (element.eventresponder.render == nil) then
|
||
element.eventresponder.render(element)
|
||
end
|
||
|
||
if mouse_hit(element) then
|
||
-- mouse down styling
|
||
if (element.styledown) then
|
||
style_ass:append(osc_styles.elementDown)
|
||
end
|
||
|
||
if (element.softrepeat) and (state.mouse_down_counter >= 15
|
||
and state.mouse_down_counter % 5 == 0) then
|
||
|
||
element.eventresponder[state.active_event_source.."_down"](element)
|
||
end
|
||
state.mouse_down_counter = state.mouse_down_counter + 1
|
||
end
|
||
|
||
end
|
||
|
||
local elem_ass = assdraw.ass_new()
|
||
|
||
elem_ass:merge(style_ass)
|
||
|
||
if not (element.type == "button") then
|
||
elem_ass:merge(element.static_ass)
|
||
end
|
||
|
||
if (element.type == "slider") then
|
||
|
||
local slider_lo = element.layout.slider
|
||
local elem_geo = element.layout.geometry
|
||
local s_min = element.slider.min.value
|
||
local s_max = element.slider.max.value
|
||
|
||
-- draw pos marker
|
||
local pos = element.slider.posF()
|
||
|
||
if not (pos == nil) then
|
||
|
||
local foV = slider_lo.border + slider_lo.gap
|
||
local foH = 0
|
||
if (slider_lo.stype == "slider") or
|
||
(slider_lo.stype == "knob") then
|
||
foH = elem_geo.h / 2
|
||
elseif (slider_lo.stype == "bar") then
|
||
foH = slider_lo.border + slider_lo.gap
|
||
end
|
||
|
||
local xp = get_slider_ele_pos_for(element, pos)
|
||
|
||
-- the filling
|
||
local innerH = elem_geo.h - (2*foV)
|
||
|
||
if (slider_lo.stype == "bar") then
|
||
elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV)
|
||
elseif (slider_lo.stype == "slider") then
|
||
elem_ass:move_to(xp, foV)
|
||
elem_ass:line_to(xp+(innerH/2), (innerH/2)+foV)
|
||
elem_ass:line_to(xp, (innerH)+foV)
|
||
elem_ass:line_to(xp-(innerH/2), (innerH/2)+foV)
|
||
elseif (slider_lo.stype == "knob") then
|
||
elem_ass:rect_cw(xp, (9*innerH/20) + foV,
|
||
elem_geo.w - foH, (11*innerH/20) + foV)
|
||
elem_ass:rect_cw(foH, (3*innerH/8) + foV,
|
||
xp, (5*innerH/8) + foV)
|
||
elem_ass:round_rect_cw(xp - innerH/2, foV,
|
||
xp + innerH/2, foV + innerH, innerH/2.0)
|
||
end
|
||
end
|
||
|
||
-- seek ranges
|
||
local seekRanges = element.slider.seekRangesF()
|
||
if not (seekRanges == nil) then
|
||
for _,range in pairs(seekRanges) do
|
||
local pstart = get_slider_ele_pos_for(element, range["start"])
|
||
local pend = get_slider_ele_pos_for(element, range["end"])
|
||
elem_ass:rect_ccw(pstart, (elem_geo.h/2)-1, pend, (elem_geo.h/2) + 1)
|
||
end
|
||
end
|
||
|
||
elem_ass:draw_stop()
|
||
|
||
-- add tooltip
|
||
if not (element.slider.tooltipF == nil) then
|
||
|
||
if mouse_hit(element) then
|
||
local sliderpos = get_slider_value(element)
|
||
local tooltiplabel = element.slider.tooltipF(sliderpos)
|
||
|
||
local an = slider_lo.tooltip_an
|
||
|
||
local ty
|
||
|
||
if (an == 2) then
|
||
ty = element.hitbox.y1 - slider_lo.border
|
||
else
|
||
ty = element.hitbox.y1 + elem_geo.h/2
|
||
end
|
||
|
||
local tx = get_virt_mouse_pos()
|
||
if (slider_lo.adjust_tooltip) then
|
||
if (an == 2) then
|
||
if (sliderpos < (s_min + 3)) then
|
||
an = an - 1
|
||
elseif (sliderpos > (s_max - 3)) then
|
||
an = an + 1
|
||
end
|
||
elseif (sliderpos > (s_max-s_min)/2) then
|
||
an = an + 1
|
||
tx = tx - 5
|
||
else
|
||
an = an - 1
|
||
tx = tx + 10
|
||
end
|
||
end
|
||
|
||
-- tooltip label
|
||
elem_ass:new_event()
|
||
elem_ass:pos(tx, ty)
|
||
elem_ass:an(an)
|
||
elem_ass:append(slider_lo.tooltip_style)
|
||
|
||
--alpha
|
||
local ar = slider_lo.alpha
|
||
if not (state.animation == nil) then
|
||
ar = {}
|
||
for ai, av in pairs(slider_lo.alpha) do
|
||
ar[ai] = mult_alpha(av, state.animation)
|
||
end
|
||
end
|
||
elem_ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
|
||
ar[1], ar[2], ar[3], ar[4]))
|
||
|
||
elem_ass:append(tooltiplabel)
|
||
|
||
end
|
||
end
|
||
|
||
elseif (element.type == "button") then
|
||
|
||
local buttontext
|
||
if type(element.content) == "function" then
|
||
buttontext = element.content() -- function objects
|
||
elseif not (element.content == nil) then
|
||
buttontext = element.content -- text objects
|
||
end
|
||
|
||
local maxchars = element.layout.button.maxchars
|
||
if not (maxchars == nil) and (#buttontext > maxchars) then
|
||
local max_ratio = 1.25 -- up to 25% more chars while shrinking
|
||
local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
|
||
if (#buttontext > limit) then
|
||
while (#buttontext > limit) do
|
||
buttontext = buttontext:gsub(".[\128-\191]*$", "")
|
||
end
|
||
buttontext = buttontext .. "..."
|
||
end
|
||
local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
|
||
local stretch = (maxchars/#buttontext)*100
|
||
buttontext = string.format("{\\fscx%f}",
|
||
(maxchars/#buttontext)*100) .. buttontext
|
||
end
|
||
|
||
elem_ass:append(buttontext)
|
||
end
|
||
|
||
master_ass:merge(elem_ass)
|
||
end
|
||
end
|
||
|
||
--
|
||
-- Message display
|
||
--
|
||
|
||
-- pos is 1 based
|
||
function limited_list(prop, pos)
|
||
local proplist = mp.get_property_native(prop, {})
|
||
local count = #proplist
|
||
if count == 0 then
|
||
return count, proplist
|
||
end
|
||
|
||
local fs = tonumber(mp.get_property('options/osd-font-size'))
|
||
local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
|
||
if max % 2 == 0 then
|
||
max = max - 1
|
||
end
|
||
local delta = math.ceil(max / 2) - 1
|
||
local begi = math.max(math.min(pos - delta, count - max + 1), 1)
|
||
local endi = math.min(begi + max - 1, count)
|
||
|
||
local reslist = {}
|
||
for i=begi, endi do
|
||
local item = proplist[i]
|
||
item.current = (i == pos) and true or nil
|
||
table.insert(reslist, item)
|
||
end
|
||
return count, reslist
|
||
end
|
||
|
||
function get_playlist()
|
||
local pos = mp.get_property_number('playlist-pos', 0) + 1
|
||
local count, limlist = limited_list('playlist', pos)
|
||
if count == 0 then
|
||
return 'Empty playlist.'
|
||
end
|
||
|
||
local message = string.format('Playlist [%d/%d]:\n', pos, count)
|
||
for i, v in ipairs(limlist) do
|
||
local title = v.title
|
||
local _, filename = utils.split_path(v.filename)
|
||
if title == nil then
|
||
title = filename
|
||
end
|
||
message = string.format('%s %s %s\n', message,
|
||
(v.current and '●' or '○'), title)
|
||
end
|
||
return message
|
||
end
|
||
|
||
function get_chapterlist()
|
||
local pos = mp.get_property_number('chapter', 0) + 1
|
||
local count, limlist = limited_list('chapter-list', pos)
|
||
if count == 0 then
|
||
return 'No chapters.'
|
||
end
|
||
|
||
local message = string.format('Chapters [%d/%d]:\n', pos, count)
|
||
for i, v in ipairs(limlist) do
|
||
local time = mp.format_time(v.time)
|
||
local title = v.title
|
||
if title == nil then
|
||
title = string.format('Chapter %02d', i)
|
||
end
|
||
message = string.format('%s[%s] %s %s\n', message, time,
|
||
(v.current and '●' or '○'), title)
|
||
end
|
||
return message
|
||
end
|
||
|
||
function show_message(text, duration)
|
||
|
||
--print("text: "..text.." duration: " .. duration)
|
||
if duration == nil then
|
||
duration = tonumber(mp.get_property("options/osd-duration")) / 1000
|
||
elseif not type(duration) == "number" then
|
||
print("duration: " .. duration)
|
||
end
|
||
|
||
-- cut the text short, otherwise the following functions
|
||
-- may slow down massively on huge input
|
||
text = string.sub(text, 0, 4000)
|
||
|
||
-- replace actual linebreaks with ASS linebreaks
|
||
text = string.gsub(text, "\n", "\\N")
|
||
|
||
state.message_text = text
|
||
state.message_timeout = mp.get_time() + duration
|
||
end
|
||
|
||
function render_message(ass)
|
||
if not(state.message_timeout == nil) and not(state.message_text == nil)
|
||
and state.message_timeout > mp.get_time() then
|
||
local _, lines = string.gsub(state.message_text, "\\N", "")
|
||
|
||
local fontsize = tonumber(mp.get_property("options/osd-font-size"))
|
||
local outline = tonumber(mp.get_property("options/osd-border-size"))
|
||
local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
|
||
local counterscale = osc_param.playresy / osc_param.unscaled_y
|
||
|
||
fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
|
||
outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
|
||
|
||
local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
|
||
|
||
|
||
ass:new_event()
|
||
ass:append(style .. state.message_text)
|
||
else
|
||
state.message_text = nil
|
||
state.message_timeout = nil
|
||
end
|
||
end
|
||
|
||
--
|
||
-- Initialisation and Layout
|
||
--
|
||
|
||
function new_element(name, type)
|
||
elements[name] = {}
|
||
elements[name].type = type
|
||
|
||
-- add default stuff
|
||
elements[name].eventresponder = {}
|
||
elements[name].visible = true
|
||
elements[name].enabled = true
|
||
elements[name].softrepeat = false
|
||
elements[name].styledown = (type == "button")
|
||
elements[name].state = {}
|
||
|
||
if (type == "slider") then
|
||
elements[name].slider = {min = {value = 0}, max = {value = 100}}
|
||
end
|
||
|
||
|
||
return elements[name]
|
||
end
|
||
|
||
function add_layout(name)
|
||
if not (elements[name] == nil) then
|
||
-- new layout
|
||
elements[name].layout = {}
|
||
|
||
-- set layout defaults
|
||
elements[name].layout.layer = 50
|
||
elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
|
||
|
||
if (elements[name].type == "button") then
|
||
elements[name].layout.button = {
|
||
maxchars = nil,
|
||
}
|
||
elseif (elements[name].type == "slider") then
|
||
-- slider defaults
|
||
elements[name].layout.slider = {
|
||
border = 1,
|
||
gap = 1,
|
||
nibbles_top = true,
|
||
nibbles_bottom = true,
|
||
stype = "slider",
|
||
adjust_tooltip = true,
|
||
tooltip_style = "",
|
||
tooltip_an = 2,
|
||
alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
|
||
}
|
||
elseif (elements[name].type == "box") then
|
||
elements[name].layout.box = {radius = 0}
|
||
end
|
||
|
||
return elements[name].layout
|
||
else
|
||
msg.error("Can't add_layout to element \""..name.."\", doesn't exist.")
|
||
end
|
||
end
|
||
|
||
--
|
||
-- Layouts
|
||
--
|
||
|
||
local layouts = {}
|
||
|
||
-- Classic box layout
|
||
layouts["box"] = function ()
|
||
|
||
local osc_geo = {
|
||
w = 550, -- width
|
||
h = 138, -- height
|
||
r = 10, -- corner-radius
|
||
p = 15, -- padding
|
||
}
|
||
|
||
-- make sure the OSC actually fits into the video
|
||
if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then
|
||
osc_param.playresy = (osc_geo.w+(2*osc_geo.p))/osc_param.display_aspect
|
||
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
|
||
end
|
||
|
||
-- position of the controller according to video aspect and valignment
|
||
local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
|
||
osc_geo.w, 0))
|
||
local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
|
||
osc_geo.h, 0))
|
||
|
||
-- position offset for contents aligned at the borders of the box
|
||
local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2
|
||
local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2
|
||
|
||
osc_param.areas = {} -- delete areas
|
||
|
||
-- area for active mouse input
|
||
add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
|
||
|
||
-- area for show/hide
|
||
local sh_area_y0, sh_area_y1
|
||
if user_opts.valign > 0 then
|
||
-- deadzone above OSC
|
||
sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
|
||
posY - (osc_geo.h / 2), 0, 0)
|
||
sh_area_y1 = osc_param.playresy
|
||
else
|
||
-- deadzone below OSC
|
||
sh_area_y0 = 0
|
||
sh_area_y1 = (posY + (osc_geo.h / 2)) +
|
||
get_align(1 - (2*user_opts.deadzonesize),
|
||
osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
|
||
end
|
||
add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
|
||
|
||
-- fetch values
|
||
local osc_w, osc_h, osc_r, osc_p =
|
||
osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p
|
||
|
||
local lo
|
||
|
||
--
|
||
-- Background box
|
||
--
|
||
|
||
new_element("bgbox", "box")
|
||
lo = add_layout("bgbox")
|
||
|
||
lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
|
||
lo.layer = 10
|
||
lo.style = osc_styles.box
|
||
lo.alpha[1] = user_opts.boxalpha
|
||
lo.alpha[3] = user_opts.boxalpha
|
||
lo.box.radius = osc_r
|
||
|
||
--
|
||
-- Title row
|
||
--
|
||
|
||
local titlerowY = posY - pos_offsetY - 10
|
||
|
||
lo = add_layout("title")
|
||
lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
|
||
lo.style = osc_styles.vidtitle
|
||
lo.button.maxchars = user_opts.boxmaxchars
|
||
|
||
lo = add_layout("pl_prev")
|
||
lo.geometry =
|
||
{x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12}
|
||
lo.style = osc_styles.topButtons
|
||
|
||
lo = add_layout("pl_next")
|
||
lo.geometry =
|
||
{x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12}
|
||
lo.style = osc_styles.topButtons
|
||
|
||
--
|
||
-- Big buttons
|
||
--
|
||
|
||
local bigbtnrowY = posY - pos_offsetY + 35
|
||
local bigbtndist = 60
|
||
|
||
lo = add_layout("playpause")
|
||
lo.geometry =
|
||
{x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
|
||
lo.style = osc_styles.bigButtons
|
||
|
||
lo = add_layout("skipback")
|
||
lo.geometry =
|
||
{x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
|
||
lo.style = osc_styles.bigButtons
|
||
|
||
lo = add_layout("skipfrwd")
|
||
lo.geometry =
|
||
{x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
|
||
lo.style = osc_styles.bigButtons
|
||
|
||
lo = add_layout("ch_prev")
|
||
lo.geometry =
|
||
{x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
|
||
lo.style = osc_styles.bigButtons
|
||
|
||
lo = add_layout("ch_next")
|
||
lo.geometry =
|
||
{x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
|
||
lo.style = osc_styles.bigButtons
|
||
|
||
lo = add_layout("cy_audio")
|
||
lo.geometry =
|
||
{x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18}
|
||
lo.style = osc_styles.smallButtonsL
|
||
|
||
lo = add_layout("cy_sub")
|
||
lo.geometry =
|
||
{x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18}
|
||
lo.style = osc_styles.smallButtonsL
|
||
|
||
lo = add_layout("tog_fs")
|
||
lo.geometry =
|
||
{x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25}
|
||
lo.style = osc_styles.smallButtonsR
|
||
|
||
lo = add_layout("volume")
|
||
lo.geometry =
|
||
{x = posX+pos_offsetX - (25 * 2) - osc_geo.p,
|
||
y = bigbtnrowY, an = 4, w = 25, h = 25}
|
||
lo.style = osc_styles.smallButtonsR
|
||
|
||
--
|
||
-- Seekbar
|
||
--
|
||
|
||
lo = add_layout("seekbar")
|
||
lo.geometry =
|
||
{x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15}
|
||
lo.style = osc_styles.timecodes
|
||
lo.slider.tooltip_style = osc_styles.vidtitle
|
||
lo.slider.stype = user_opts["seekbarstyle"]
|
||
if lo.slider.stype == "knob" then
|
||
lo.slider.border = 0
|
||
end
|
||
|
||
--
|
||
-- Timecodes + Cache
|
||
--
|
||
|
||
local bottomrowY = posY + pos_offsetY - 5
|
||
|
||
lo = add_layout("tc_left")
|
||
lo.geometry =
|
||
{x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18}
|
||
lo.style = osc_styles.timecodes
|
||
|
||
lo = add_layout("tc_right")
|
||
lo.geometry =
|
||
{x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18}
|
||
lo.style = osc_styles.timecodes
|
||
|
||
lo = add_layout("cache")
|
||
lo.geometry =
|
||
{x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
|
||
lo.style = osc_styles.timecodes
|
||
|
||
end
|
||
|
||
-- slim box layout
|
||
layouts["slimbox"] = function ()
|
||
|
||
local osc_geo = {
|
||
w = 660, -- width
|
||
h = 70, -- height
|
||
r = 10, -- corner-radius
|
||
}
|
||
|
||
-- make sure the OSC actually fits into the video
|
||
if (osc_param.playresx < (osc_geo.w)) then
|
||
osc_param.playresy = (osc_geo.w)/osc_param.display_aspect
|
||
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
|
||
end
|
||
|
||
-- position of the controller according to video aspect and valignment
|
||
local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
|
||
osc_geo.w, 0))
|
||
local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
|
||
osc_geo.h, 0))
|
||
|
||
osc_param.areas = {} -- delete areas
|
||
|
||
-- area for active mouse input
|
||
add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
|
||
|
||
-- area for show/hide
|
||
local sh_area_y0, sh_area_y1
|
||
if user_opts.valign > 0 then
|
||
-- deadzone above OSC
|
||
sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
|
||
posY - (osc_geo.h / 2), 0, 0)
|
||
sh_area_y1 = osc_param.playresy
|
||
else
|
||
-- deadzone below OSC
|
||
sh_area_y0 = 0
|
||
sh_area_y1 = (posY + (osc_geo.h / 2)) +
|
||
get_align(1 - (2*user_opts.deadzonesize),
|
||
osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
|
||
end
|
||
add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
|
||
|
||
local lo
|
||
|
||
local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100
|
||
|
||
-- styles
|
||
local styles = {
|
||
box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
|
||
timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
|
||
tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}",
|
||
}
|
||
|
||
|
||
new_element("bgbox", "box")
|
||
lo = add_layout("bgbox")
|
||
|
||
lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
|
||
lo.layer = 10
|
||
lo.style = osc_styles.box
|
||
lo.alpha[1] = user_opts.boxalpha
|
||
lo.alpha[3] = 0
|
||
if not (user_opts["seekbarstyle"] == "bar") then
|
||
lo.box.radius = osc_geo.r
|
||
end
|
||
|
||
|
||
lo = add_layout("seekbar")
|
||
lo.geometry =
|
||
{x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
|
||
lo.style = osc_styles.timecodes
|
||
lo.slider.border = 0
|
||
lo.slider.gap = 1.5
|
||
lo.slider.tooltip_style = styles.tooltip
|
||
lo.slider.stype = user_opts["seekbarstyle"]
|
||
lo.slider.adjust_tooltip = false
|
||
|
||
--
|
||
-- Timecodes
|
||
--
|
||
|
||
lo = add_layout("tc_left")
|
||
lo.geometry =
|
||
{x = posX - (inner_w/2) + osc_geo.r, y = posY + 1,
|
||
an = 7, w = tc_w, h = ele_h}
|
||
lo.style = styles.timecodes
|
||
lo.alpha[3] = user_opts.boxalpha
|
||
|
||
lo = add_layout("tc_right")
|
||
lo.geometry =
|
||
{x = posX + (inner_w/2) - osc_geo.r, y = posY + 1,
|
||
an = 9, w = tc_w, h = ele_h}
|
||
lo.style = styles.timecodes
|
||
lo.alpha[3] = user_opts.boxalpha
|
||
|
||
-- Cache
|
||
|
||
lo = add_layout("cache")
|
||
lo.geometry =
|
||
{x = posX, y = posY + 1,
|
||
an = 8, w = tc_w, h = ele_h}
|
||
lo.style = styles.timecodes
|
||
lo.alpha[3] = user_opts.boxalpha
|
||
|
||
|
||
end
|
||
|
||
layouts["bottombar"] = function()
|
||
local osc_geo = {
|
||
x = -2,
|
||
y = osc_param.playresy - 54 - user_opts.barmargin,
|
||
an = 7,
|
||
w = osc_param.playresx + 4,
|
||
h = 56,
|
||
}
|
||
|
||
local padX = 9
|
||
local padY = 3
|
||
local buttonW = 27
|
||
local tcW = (state.tc_ms) and 170 or 110
|
||
local tsW = 90
|
||
local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
|
||
|
||
if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
|
||
osc_param.playresy = minW / osc_param.display_aspect
|
||
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
|
||
osc_geo.y = osc_param.playresy - 54 - user_opts.barmargin
|
||
osc_geo.w = osc_param.playresx + 4
|
||
end
|
||
|
||
local line1 = osc_geo.y + 9 + padY
|
||
local line2 = osc_geo.y + 36 + padY
|
||
|
||
osc_param.areas = {}
|
||
|
||
add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
|
||
osc_geo.w, osc_geo.h))
|
||
|
||
local sh_area_y0, sh_area_y1
|
||
sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
|
||
osc_geo.y - (osc_geo.h / 2), 0, 0)
|
||
sh_area_y1 = osc_param.playresy - user_opts.barmargin
|
||
add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
|
||
|
||
local lo, geo
|
||
|
||
-- Background bar
|
||
new_element("bgbox", "box")
|
||
lo = add_layout("bgbox")
|
||
|
||
lo.geometry = osc_geo
|
||
lo.layer = 10
|
||
lo.style = osc_styles.box
|
||
lo.alpha[1] = user_opts.boxalpha
|
||
|
||
|
||
-- Playlist prev/next
|
||
geo = { x = osc_geo.x + padX, y = line1,
|
||
an = 4, w = 18, h = 18 - padY }
|
||
lo = add_layout("pl_prev")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.topButtonsBar
|
||
|
||
geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("pl_next")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.topButtonsBar
|
||
|
||
local t_l = geo.x + geo.w + padX
|
||
|
||
-- Cache
|
||
geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
|
||
an = 6, w = 150, h = geo.h }
|
||
lo = add_layout("cache")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.vidtitleBar
|
||
|
||
local t_r = geo.x - geo.w - padX*2
|
||
|
||
-- Title
|
||
geo = { x = t_l, y = geo.y, an = 4,
|
||
w = t_r - t_l, h = geo.h }
|
||
lo = add_layout("title")
|
||
lo.geometry = geo
|
||
lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
|
||
osc_styles.vidtitleBar,
|
||
geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
|
||
|
||
|
||
-- Playback control buttons
|
||
geo = { x = osc_geo.x + padX, y = line2, an = 4,
|
||
w = buttonW, h = 36 - padY*2}
|
||
lo = add_layout("playpause")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("ch_prev")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("ch_next")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
-- Left timecode
|
||
geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
|
||
w = tcW, h = geo.h }
|
||
lo = add_layout("tc_left")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.timecodesBar
|
||
|
||
local sb_l = geo.x + padX
|
||
|
||
-- Fullscreen button
|
||
geo = { x = osc_geo.x + osc_geo.w - buttonW - padX, y = geo.y, an = 4,
|
||
w = buttonW, h = geo.h }
|
||
lo = add_layout("tog_fs")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
-- Volume
|
||
geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("volume")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
-- Track selection buttons
|
||
geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
|
||
lo = add_layout("cy_sub")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("cy_audio")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
|
||
-- Right timecode
|
||
geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an,
|
||
w = tcW, h = geo.h }
|
||
lo = add_layout("tc_right")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.timecodesBar
|
||
|
||
local sb_r = geo.x - padX
|
||
|
||
|
||
-- Seekbar
|
||
geo = { x = sb_l, y = geo.y, an = geo.an,
|
||
w = math.max(0, sb_r - sb_l), h = geo.h }
|
||
new_element("bgbar1", "box")
|
||
lo = add_layout("bgbar1")
|
||
|
||
lo.geometry = geo
|
||
lo.layer = 15
|
||
lo.style = osc_styles.timecodesBar
|
||
lo.alpha[1] =
|
||
math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
|
||
|
||
lo = add_layout("seekbar")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.timecodes
|
||
lo.slider.border = 0
|
||
lo.slider.gap = 2
|
||
lo.slider.tooltip_style = osc_styles.timePosBar
|
||
lo.slider.tooltip_an = 5
|
||
lo.slider.stype = user_opts["seekbarstyle"]
|
||
end
|
||
|
||
layouts["topbar"] = function()
|
||
local osc_geo = {
|
||
x = -2,
|
||
y = 54 + user_opts.barmargin,
|
||
an = 1,
|
||
w = osc_param.playresx + 4,
|
||
h = 56,
|
||
}
|
||
|
||
local padX = 9
|
||
local padY = 3
|
||
local buttonW = 27
|
||
local tcW = (state.tc_ms) and 170 or 110
|
||
local tsW = 90
|
||
local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
|
||
|
||
if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
|
||
osc_param.playresy = minW / osc_param.display_aspect
|
||
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
|
||
osc_geo.y = 54 + user_opts.barmargin
|
||
osc_geo.w = osc_param.playresx + 4
|
||
end
|
||
|
||
local line1 = osc_geo.y - 36 - padY
|
||
local line2 = osc_geo.y - 9 - padY
|
||
|
||
osc_param.areas = {}
|
||
|
||
add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
|
||
osc_geo.w, osc_geo.h))
|
||
|
||
local sh_area_y0, sh_area_y1
|
||
sh_area_y0 = user_opts.barmargin
|
||
sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) +
|
||
get_align(1 - (2*user_opts.deadzonesize),
|
||
osc_param.playresy - (osc_geo.y + (osc_geo.h / 2)), 0, 0)
|
||
add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
|
||
|
||
local lo, geo
|
||
|
||
-- Background bar
|
||
new_element("bgbox", "box")
|
||
lo = add_layout("bgbox")
|
||
|
||
lo.geometry = osc_geo
|
||
lo.layer = 10
|
||
lo.style = osc_styles.box
|
||
lo.alpha[1] = user_opts.boxalpha
|
||
|
||
|
||
-- Playback control buttons
|
||
geo = { x = osc_geo.x + padX, y = line1, an = 4,
|
||
w = buttonW, h = 36 - padY*2 }
|
||
lo = add_layout("playpause")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("ch_prev")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("ch_next")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
|
||
-- Left timecode
|
||
geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
|
||
w = tcW, h = geo.h }
|
||
lo = add_layout("tc_left")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.timecodesBar
|
||
|
||
local sb_l = geo.x + padX
|
||
|
||
-- Fullscreen button
|
||
geo = { x = osc_geo.x + osc_geo.w - buttonW - padX, y = geo.y, an = 4,
|
||
w = buttonW, h = geo.h }
|
||
lo = add_layout("tog_fs")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
-- Volume
|
||
geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("volume")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
-- Track selection buttons
|
||
geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
|
||
lo = add_layout("cy_sub")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("cy_audio")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.smallButtonsBar
|
||
|
||
|
||
-- Right timecode
|
||
geo = { x = geo.x - geo.w - padX - tcW - 10, y = geo.y, an = 4,
|
||
w = tcW, h = geo.h }
|
||
lo = add_layout("tc_right")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.timecodesBar
|
||
|
||
local sb_r = geo.x - padX
|
||
|
||
|
||
-- Seekbar
|
||
geo = { x = sb_l, y = user_opts.barmargin, an = 7,
|
||
w = math.max(0, sb_r - sb_l), h = geo.h }
|
||
new_element("bgbar1", "box")
|
||
lo = add_layout("bgbar1")
|
||
|
||
lo.geometry = geo
|
||
lo.layer = 15
|
||
lo.style = osc_styles.timecodesBar
|
||
lo.alpha[1] =
|
||
math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
|
||
|
||
lo = add_layout("seekbar")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.timecodesBar
|
||
lo.slider.border = 0
|
||
lo.slider.gap = 2
|
||
lo.slider.tooltip_style = osc_styles.timePosBar
|
||
lo.slider.stype = user_opts["seekbarstyle"]
|
||
lo.slider.tooltip_an = 5
|
||
|
||
|
||
-- Playlist prev/next
|
||
geo = { x = osc_geo.x + padX, y = line2, an = 4, w = 18, h = 18 - padY }
|
||
lo = add_layout("pl_prev")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.topButtonsBar
|
||
|
||
geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
|
||
lo = add_layout("pl_next")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.topButtonsBar
|
||
|
||
local t_l = geo.x + geo.w + padX
|
||
|
||
-- Cache
|
||
geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
|
||
an = 6, w = 150, h = geo.h }
|
||
lo = add_layout("cache")
|
||
lo.geometry = geo
|
||
lo.style = osc_styles.vidtitleBar
|
||
|
||
local t_r = geo.x - geo.w - padX*2
|
||
|
||
-- Title
|
||
geo = { x = t_l, y = geo.y, an = 4,
|
||
w = t_r - t_l, h = geo.h }
|
||
lo = add_layout("title")
|
||
lo.geometry = geo
|
||
lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
|
||
osc_styles.vidtitleBar,
|
||
geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
|
||
end
|
||
|
||
-- Validate string type user options
|
||
function validate_user_opts()
|
||
if layouts[user_opts.layout] == nil then
|
||
msg.warn("Invalid setting \""..user_opts.layout.."\" for layout")
|
||
user_opts.layout = "box"
|
||
end
|
||
|
||
if user_opts.seekbarstyle ~= "slider" and
|
||
user_opts.seekbarstyle ~= "bar" and
|
||
user_opts.seekbarstyle ~= "knob" then
|
||
msg.warn("Invalid setting \"" .. user_opts.seekbarstyle
|
||
.. "\" for seekbarstyle")
|
||
user_opts.seekbarstyle = "slider"
|
||
end
|
||
end
|
||
|
||
|
||
-- OSC INIT
|
||
function osc_init()
|
||
msg.debug("osc_init")
|
||
|
||
-- set canvas resolution according to display aspect and scaling setting
|
||
local baseResY = 720
|
||
local display_w, display_h, display_aspect = mp.get_osd_size()
|
||
local scale = 1
|
||
|
||
if (mp.get_property("video") == "no") then -- dummy/forced window
|
||
scale = user_opts.scaleforcedwindow
|
||
elseif state.fullscreen then
|
||
scale = user_opts.scalefullscreen
|
||
else
|
||
scale = user_opts.scalewindowed
|
||
end
|
||
|
||
if user_opts.vidscale then
|
||
osc_param.unscaled_y = baseResY
|
||
else
|
||
osc_param.unscaled_y = display_h
|
||
end
|
||
osc_param.playresy = osc_param.unscaled_y / scale
|
||
if (display_aspect > 0) then
|
||
osc_param.display_aspect = display_aspect
|
||
end
|
||
osc_param.playresx = osc_param.playresy * osc_param.display_aspect
|
||
|
||
-- stop seeking with the slider to prevent skipping files
|
||
state.active_element = nil
|
||
|
||
|
||
|
||
|
||
elements = {}
|
||
|
||
-- some often needed stuff
|
||
local pl_count = mp.get_property_number("playlist-count", 0)
|
||
local have_pl = (pl_count > 1)
|
||
local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
|
||
local have_ch = (mp.get_property_number("chapters", 0) > 0)
|
||
local loop = mp.get_property("loop-playlist", "no")
|
||
|
||
local ne
|
||
|
||
-- title
|
||
ne = new_element("title", "button")
|
||
|
||
ne.content = function ()
|
||
local title = mp.command_native({"expand-text", user_opts.title})
|
||
-- escape ASS, and strip newlines and trailing slashes
|
||
title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
|
||
return not (title == "") and title or "mpv"
|
||
end
|
||
|
||
ne.eventresponder["mbtn_left_up"] = function ()
|
||
local title = mp.get_property_osd("media-title")
|
||
if (have_pl) then
|
||
title = string.format("[%d/%d] %s", countone(pl_pos - 1),
|
||
pl_count, title)
|
||
end
|
||
show_message(title)
|
||
end
|
||
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () show_message(mp.get_property_osd("filename")) end
|
||
|
||
-- playlist buttons
|
||
|
||
-- prev
|
||
ne = new_element("pl_prev", "button")
|
||
|
||
ne.content = "\238\132\144"
|
||
ne.enabled = (pl_pos > 1) or (loop ~= "no")
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function ()
|
||
mp.commandv("playlist-prev", "weak")
|
||
show_message(get_playlist(), 3)
|
||
end
|
||
ne.eventresponder["shift+mbtn_left_up"] =
|
||
function () show_message(get_playlist(), 3) end
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () show_message(get_playlist(), 3) end
|
||
|
||
--next
|
||
ne = new_element("pl_next", "button")
|
||
|
||
ne.content = "\238\132\129"
|
||
ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function ()
|
||
mp.commandv("playlist-next", "weak")
|
||
show_message(get_playlist(), 3)
|
||
end
|
||
ne.eventresponder["shift+mbtn_left_up"] =
|
||
function () show_message(get_playlist(), 3) end
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () show_message(get_playlist(), 3) end
|
||
|
||
|
||
-- big buttons
|
||
|
||
--playpause
|
||
ne = new_element("playpause", "button")
|
||
|
||
ne.content = function ()
|
||
if mp.get_property("pause") == "yes" then
|
||
return ("\238\132\129")
|
||
else
|
||
return ("\238\128\130")
|
||
end
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function () mp.commandv("cycle", "pause") end
|
||
|
||
--skipback
|
||
ne = new_element("skipback", "button")
|
||
|
||
ne.softrepeat = true
|
||
ne.content = "\238\128\132"
|
||
ne.eventresponder["mbtn_left_down"] =
|
||
function () mp.commandv("seek", -5, "relative", "keyframes") end
|
||
ne.eventresponder["shift+mbtn_left_down"] =
|
||
function () mp.commandv("frame-back-step") end
|
||
ne.eventresponder["mbtn_right_down"] =
|
||
function () mp.commandv("seek", -30, "relative", "keyframes") end
|
||
|
||
--skipfrwd
|
||
ne = new_element("skipfrwd", "button")
|
||
|
||
ne.softrepeat = true
|
||
ne.content = "\238\128\133"
|
||
ne.eventresponder["mbtn_left_down"] =
|
||
function () mp.commandv("seek", 10, "relative", "keyframes") end
|
||
ne.eventresponder["shift+mbtn_left_down"] =
|
||
function () mp.commandv("frame-step") end
|
||
ne.eventresponder["mbtn_right_down"] =
|
||
function () mp.commandv("seek", 60, "relative", "keyframes") end
|
||
|
||
--ch_prev
|
||
ne = new_element("ch_prev", "button")
|
||
|
||
ne.enabled = have_ch
|
||
ne.content = "\238\132\132"
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function ()
|
||
mp.commandv("add", "chapter", -1)
|
||
show_message(get_chapterlist(), 3)
|
||
end
|
||
ne.eventresponder["shift+mbtn_left_up"] =
|
||
function () show_message(get_chapterlist(), 3) end
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () show_message(get_chapterlist(), 3) end
|
||
|
||
--ch_next
|
||
ne = new_element("ch_next", "button")
|
||
|
||
ne.enabled = have_ch
|
||
ne.content = "\238\132\133"
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function ()
|
||
mp.commandv("add", "chapter", 1)
|
||
show_message(get_chapterlist(), 3)
|
||
end
|
||
ne.eventresponder["shift+mbtn_left_up"] =
|
||
function () show_message(get_chapterlist(), 3) end
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () show_message(get_chapterlist(), 3) end
|
||
|
||
--
|
||
update_tracklist()
|
||
|
||
--cy_audio
|
||
ne = new_element("cy_audio", "button")
|
||
|
||
ne.enabled = (#tracks_osc.audio > 0)
|
||
ne.content = function ()
|
||
local aid = "–"
|
||
if not (get_track("audio") == 0) then
|
||
aid = get_track("audio")
|
||
end
|
||
return ("\238\132\134" .. osc_styles.smallButtonsLlabel
|
||
.. " " .. aid .. "/" .. #tracks_osc.audio)
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function () set_track("audio", 1) end
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () set_track("audio", -1) end
|
||
ne.eventresponder["shift+mbtn_left_down"] =
|
||
function () show_message(get_tracklist("audio"), 2) end
|
||
|
||
--cy_sub
|
||
ne = new_element("cy_sub", "button")
|
||
|
||
ne.enabled = (#tracks_osc.sub > 0)
|
||
ne.content = function ()
|
||
local sid = "–"
|
||
if not (get_track("sub") == 0) then
|
||
sid = get_track("sub")
|
||
end
|
||
return ("\238\132\135" .. osc_styles.smallButtonsLlabel
|
||
.. " " .. sid .. "/" .. #tracks_osc.sub)
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function () set_track("sub", 1) end
|
||
ne.eventresponder["mbtn_right_up"] =
|
||
function () set_track("sub", -1) end
|
||
ne.eventresponder["shift+mbtn_left_down"] =
|
||
function () show_message(get_tracklist("sub"), 2) end
|
||
|
||
--tog_fs
|
||
ne = new_element("tog_fs", "button")
|
||
ne.content = function ()
|
||
if (state.fullscreen) then
|
||
return ("\238\132\137")
|
||
else
|
||
return ("\238\132\136")
|
||
end
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function () mp.commandv("cycle", "fullscreen") end
|
||
|
||
|
||
--seekbar
|
||
ne = new_element("seekbar", "slider")
|
||
|
||
ne.enabled = not (mp.get_property("percent-pos") == nil)
|
||
ne.slider.markerF = function ()
|
||
local duration = mp.get_property_number("duration", nil)
|
||
if not (duration == nil) then
|
||
local chapters = mp.get_property_native("chapter-list", {})
|
||
local markers = {}
|
||
for n = 1, #chapters do
|
||
markers[n] = (chapters[n].time / duration * 100)
|
||
end
|
||
return markers
|
||
else
|
||
return {}
|
||
end
|
||
end
|
||
ne.slider.posF =
|
||
function () return mp.get_property_number("percent-pos", nil) end
|
||
ne.slider.tooltipF = function (pos)
|
||
local duration = mp.get_property_number("duration", nil)
|
||
if not ((duration == nil) or (pos == nil)) then
|
||
possec = duration * (pos / 100)
|
||
return mp.format_time(possec)
|
||
else
|
||
return ""
|
||
end
|
||
end
|
||
ne.slider.seekRangesF = function()
|
||
if not (user_opts.seekranges) then
|
||
return nil
|
||
end
|
||
local cache_state = mp.get_property_native("demuxer-cache-state", nil)
|
||
if not cache_state then
|
||
return nil
|
||
end
|
||
local duration = mp.get_property_number("duration", nil)
|
||
if (duration == nil) or duration <= 0 then
|
||
return nil
|
||
end
|
||
local ranges = cache_state["seekable-ranges"]
|
||
for _, range in pairs(ranges) do
|
||
range["start"] = 100 * range["start"] / duration
|
||
range["end"] = 100 * range["end"] / duration
|
||
end
|
||
return ranges
|
||
end
|
||
ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
|
||
function (element)
|
||
-- mouse move events may pile up during seeking and may still get
|
||
-- sent when the user is done seeking, so we need to throw away
|
||
-- identical seeks
|
||
local seekto = get_slider_value(element)
|
||
if (element.state.lastseek == nil) or
|
||
(not (element.state.lastseek == seekto)) then
|
||
mp.commandv("seek", seekto, "absolute-percent",
|
||
user_opts.seekbarkeyframes and "keyframes" or "exact")
|
||
element.state.lastseek = seekto
|
||
end
|
||
|
||
end
|
||
ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks
|
||
function (element) mp.commandv("seek", get_slider_value(element),
|
||
"absolute-percent", "exact") end
|
||
ne.eventresponder["reset"] =
|
||
function (element) element.state.lastseek = nil end
|
||
|
||
|
||
-- tc_left (current pos)
|
||
ne = new_element("tc_left", "button")
|
||
|
||
ne.content = function ()
|
||
if (state.tc_ms) then
|
||
return (mp.get_property_osd("playback-time/full"))
|
||
else
|
||
return (mp.get_property_osd("playback-time"))
|
||
end
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] = function ()
|
||
state.tc_ms = not state.tc_ms
|
||
request_init()
|
||
end
|
||
|
||
-- tc_right (total/remaining time)
|
||
ne = new_element("tc_right", "button")
|
||
|
||
ne.visible = (mp.get_property_number("duration", 0) > 0)
|
||
ne.content = function ()
|
||
if (state.rightTC_trem) then
|
||
if state.tc_ms then
|
||
return ("-"..mp.get_property_osd("playtime-remaining/full"))
|
||
else
|
||
return ("-"..mp.get_property_osd("playtime-remaining"))
|
||
end
|
||
else
|
||
if state.tc_ms then
|
||
return (mp.get_property_osd("duration/full"))
|
||
else
|
||
return (mp.get_property_osd("duration"))
|
||
end
|
||
end
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function () state.rightTC_trem = not state.rightTC_trem end
|
||
|
||
-- cache
|
||
ne = new_element("cache", "button")
|
||
|
||
ne.content = function ()
|
||
local cache_state = mp.get_property_native("demuxer-cache-state", {})
|
||
if not (cache_state["seekable-ranges"] and
|
||
#cache_state["seekable-ranges"] > 0) then
|
||
-- probably not a network stream
|
||
return ""
|
||
end
|
||
local dmx_cache = mp.get_property_number("demuxer-cache-duration")
|
||
if dmx_cache and (dmx_cache > state.dmx_cache * 1.1 or
|
||
dmx_cache < state.dmx_cache * 0.9) then
|
||
state.dmx_cache = dmx_cache
|
||
else
|
||
dmx_cache = state.dmx_cache
|
||
end
|
||
local min = math.floor(dmx_cache / 60)
|
||
local sec = dmx_cache % 60
|
||
return "Cache: " .. (min > 0 and
|
||
string.format("%sm%02.0fs", min, sec) or
|
||
string.format("%3.0fs", dmx_cache))
|
||
end
|
||
|
||
-- volume
|
||
ne = new_element("volume", "button")
|
||
|
||
ne.content = function()
|
||
local volume = mp.get_property_number("volume", 0)
|
||
local mute = mp.get_property_native("mute")
|
||
local volicon = {"\238\132\139", "\238\132\140",
|
||
"\238\132\141", "\238\132\142"}
|
||
if volume == 0 or mute then
|
||
return "\238\132\138"
|
||
else
|
||
return volicon[math.min(4,math.ceil(volume / (100/3)))]
|
||
end
|
||
end
|
||
ne.eventresponder["mbtn_left_up"] =
|
||
function () mp.commandv("cycle", "mute") end
|
||
|
||
ne.eventresponder["wheel_up_press"] =
|
||
function () mp.commandv("osd-auto", "add", "volume", 5) end
|
||
ne.eventresponder["wheel_down_press"] =
|
||
function () mp.commandv("osd-auto", "add", "volume", -5) end
|
||
|
||
|
||
-- load layout
|
||
layouts[user_opts.layout]()
|
||
|
||
--do something with the elements
|
||
prepare_elements()
|
||
|
||
end
|
||
|
||
|
||
|
||
--
|
||
-- Other important stuff
|
||
--
|
||
|
||
|
||
function show_osc()
|
||
-- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
|
||
if not state.enabled then return end
|
||
|
||
msg.trace("show_osc")
|
||
--remember last time of invocation (mouse move)
|
||
state.showtime = mp.get_time()
|
||
|
||
osc_visible(true)
|
||
|
||
if (user_opts.fadeduration > 0) then
|
||
state.anitype = nil
|
||
end
|
||
end
|
||
|
||
function hide_osc()
|
||
msg.trace("hide_osc")
|
||
if not state.enabled then
|
||
-- typically hide happens at render() from tick(), but now tick() is
|
||
-- no-op and won't render again to remove the osc, so do that manually.
|
||
state.osc_visible = false
|
||
timer_stop()
|
||
render_wipe()
|
||
elseif (user_opts.fadeduration > 0) then
|
||
if not(state.osc_visible == false) then
|
||
state.anitype = "out"
|
||
control_timer()
|
||
end
|
||
else
|
||
osc_visible(false)
|
||
end
|
||
end
|
||
|
||
function osc_visible(visible)
|
||
state.osc_visible = visible
|
||
control_timer()
|
||
end
|
||
|
||
function pause_state(name, enabled)
|
||
state.paused = enabled
|
||
control_timer()
|
||
end
|
||
|
||
function cache_state(name, idle)
|
||
state.cache_idle = idle
|
||
control_timer()
|
||
end
|
||
|
||
function control_timer()
|
||
if (state.paused) and (state.osc_visible) and
|
||
( not(state.cache_idle) or not (state.anitype == nil) ) then
|
||
|
||
timer_start()
|
||
else
|
||
timer_stop()
|
||
end
|
||
end
|
||
|
||
function timer_start()
|
||
if not (state.timer_active) then
|
||
msg.trace("timer start")
|
||
|
||
if (state.timer == nil) then
|
||
-- create new timer
|
||
state.timer = mp.add_periodic_timer(0.03, tick)
|
||
else
|
||
-- resume existing one
|
||
state.timer:resume()
|
||
end
|
||
|
||
state.timer_active = true
|
||
end
|
||
end
|
||
|
||
function timer_stop()
|
||
if (state.timer_active) then
|
||
msg.trace("timer stop")
|
||
|
||
if not (state.timer == nil) then
|
||
-- kill timer
|
||
state.timer:kill()
|
||
end
|
||
|
||
state.timer_active = false
|
||
end
|
||
end
|
||
|
||
|
||
|
||
function mouse_leave()
|
||
if user_opts.hidetimeout >= 0 then
|
||
hide_osc()
|
||
end
|
||
-- reset mouse position
|
||
state.last_mouseX, state.last_mouseY = nil, nil
|
||
end
|
||
|
||
function request_init()
|
||
state.initREQ = true
|
||
end
|
||
|
||
function render_wipe()
|
||
msg.trace("render_wipe()")
|
||
mp.set_osd_ass(0, 0, "{}")
|
||
end
|
||
|
||
function render()
|
||
msg.trace("rendering")
|
||
local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
|
||
local mouseX, mouseY = get_virt_mouse_pos()
|
||
local now = mp.get_time()
|
||
|
||
-- check if display changed, if so request reinit
|
||
if not (state.mp_screen_sizeX == current_screen_sizeX
|
||
and state.mp_screen_sizeY == current_screen_sizeY) then
|
||
|
||
request_init()
|
||
|
||
state.mp_screen_sizeX = current_screen_sizeX
|
||
state.mp_screen_sizeY = current_screen_sizeY
|
||
end
|
||
|
||
-- init management
|
||
if state.initREQ then
|
||
osc_init()
|
||
state.initREQ = false
|
||
|
||
-- store initial mouse position
|
||
if (state.last_mouseX == nil or state.last_mouseY == nil)
|
||
and not (mouseX == nil or mouseY == nil) then
|
||
|
||
state.last_mouseX, state.last_mouseY = mouseX, mouseY
|
||
end
|
||
end
|
||
|
||
|
||
-- fade animation
|
||
if not(state.anitype == nil) then
|
||
|
||
if (state.anistart == nil) then
|
||
state.anistart = now
|
||
end
|
||
|
||
if (now < state.anistart + (user_opts.fadeduration/1000)) then
|
||
|
||
if (state.anitype == "in") then --fade in
|
||
osc_visible(true)
|
||
state.animation = scale_value(state.anistart,
|
||
(state.anistart + (user_opts.fadeduration/1000)),
|
||
255, 0, now)
|
||
elseif (state.anitype == "out") then --fade out
|
||
state.animation = scale_value(state.anistart,
|
||
(state.anistart + (user_opts.fadeduration/1000)),
|
||
0, 255, now)
|
||
end
|
||
|
||
else
|
||
if (state.anitype == "out") then
|
||
osc_visible(false)
|
||
end
|
||
state.anistart = nil
|
||
state.animation = nil
|
||
state.anitype = nil
|
||
end
|
||
else
|
||
state.anistart = nil
|
||
state.animation = nil
|
||
state.anitype = nil
|
||
end
|
||
|
||
--mouse show/hide area
|
||
for k,cords in pairs(osc_param.areas["showhide"]) do
|
||
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
|
||
end
|
||
do_enable_keybindings()
|
||
|
||
--mouse input area
|
||
local mouse_over_osc = false
|
||
|
||
for _,cords in ipairs(osc_param.areas["input"]) do
|
||
if state.osc_visible then -- activate only when OSC is actually visible
|
||
set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
|
||
end
|
||
if state.osc_visible ~= state.input_enabled then
|
||
if state.osc_visible then
|
||
mp.enable_key_bindings("input")
|
||
else
|
||
mp.disable_key_bindings("input")
|
||
end
|
||
state.input_enabled = state.osc_visible
|
||
end
|
||
|
||
if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
|
||
mouse_over_osc = true
|
||
end
|
||
end
|
||
|
||
-- autohide
|
||
if not (state.showtime == nil) and (user_opts.hidetimeout >= 0)
|
||
and (state.showtime + (user_opts.hidetimeout/1000) < now)
|
||
and (state.active_element == nil) and not (mouse_over_osc) then
|
||
|
||
hide_osc()
|
||
end
|
||
|
||
|
||
-- actual rendering
|
||
local ass = assdraw.ass_new()
|
||
|
||
-- Messages
|
||
render_message(ass)
|
||
|
||
-- actual OSC
|
||
if state.osc_visible then
|
||
render_elements(ass)
|
||
end
|
||
|
||
-- submit
|
||
mp.set_osd_ass(osc_param.playresy * osc_param.display_aspect,
|
||
osc_param.playresy, ass.text)
|
||
|
||
|
||
|
||
|
||
end
|
||
|
||
--
|
||
-- Eventhandling
|
||
--
|
||
|
||
local function element_has_action(element, action)
|
||
return element and element.eventresponder and
|
||
element.eventresponder[action]
|
||
end
|
||
|
||
function process_event(source, what)
|
||
local action = string.format("%s%s", source,
|
||
what and ("_" .. what) or "")
|
||
|
||
if what == "down" or what == "press" then
|
||
|
||
for n = 1, #elements do
|
||
|
||
if mouse_hit(elements[n]) and
|
||
elements[n].eventresponder and
|
||
(elements[n].eventresponder[source .. "_up"] or
|
||
elements[n].eventresponder[action]) then
|
||
|
||
if what == "down" then
|
||
state.active_element = n
|
||
state.active_event_source = source
|
||
end
|
||
-- fire the down or press event if the element has one
|
||
if element_has_action(elements[n], action) then
|
||
elements[n].eventresponder[action](elements[n])
|
||
end
|
||
|
||
end
|
||
end
|
||
|
||
elseif what == "up" then
|
||
|
||
if elements[state.active_element] then
|
||
local n = state.active_element
|
||
|
||
if n == 0 then
|
||
--click on background (does not work)
|
||
elseif element_has_action(elements[n], action) and
|
||
mouse_hit(elements[n]) then
|
||
|
||
elements[n].eventresponder[action](elements[n])
|
||
end
|
||
|
||
--reset active element
|
||
if element_has_action(elements[n], "reset") then
|
||
elements[n].eventresponder["reset"](elements[n])
|
||
end
|
||
|
||
end
|
||
state.active_element = nil
|
||
state.mouse_down_counter = 0
|
||
|
||
elseif source == "mouse_move" then
|
||
|
||
local mouseX, mouseY = get_virt_mouse_pos()
|
||
if (user_opts.minmousemove == 0) or
|
||
(not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
|
||
((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
|
||
or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
|
||
)
|
||
) then
|
||
show_osc()
|
||
end
|
||
state.last_mouseX, state.last_mouseY = mouseX, mouseY
|
||
|
||
local n = state.active_element
|
||
if element_has_action(elements[n], action) then
|
||
elements[n].eventresponder[action](elements[n])
|
||
end
|
||
tick()
|
||
end
|
||
end
|
||
|
||
-- called by mpv on every frame
|
||
function tick()
|
||
if (not state.enabled) then return end
|
||
|
||
if (state.idle) then
|
||
|
||
-- render idle message
|
||
msg.trace("idle message")
|
||
local icon_x, icon_y = 320 - 26, 140
|
||
|
||
local ass = assdraw.ass_new()
|
||
ass:new_event()
|
||
ass:pos(icon_x, icon_y)
|
||
ass:append("{\\rDefault\\an7\\c&H430142&\\1a&H00&\\bord0\\shad0\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}")
|
||
ass:new_event()
|
||
ass:pos(icon_x, icon_y)
|
||
ass:append("{\\rDefault\\an7\\c&HDDDBDD&\\1a&H00&\\bord0\\shad0\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}")
|
||
ass:new_event()
|
||
ass:pos(icon_x, icon_y)
|
||
ass:append("{\\rDefault\\an7\\c&H691F69&\\1a&H00&\\bord0\\shad0\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}")
|
||
ass:new_event()
|
||
ass:pos(icon_x, icon_y)
|
||
ass:append("{\\rDefault\\an7\\c&H682167&\\1a&H00&\\bord0\\shad0\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42 m 925 42 m 977 200 b 1324 200 1605 482 1605 828 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200{\\p0}")
|
||
ass:new_event()
|
||
ass:pos(icon_x, icon_y)
|
||
ass:append("{\\rDefault\\an7\\c&H753074&\\1a&H00&\\bord0\\shad0\\p6}m 977 198 b 630 198 348 480 348 828 348 1176 630 1458 977 1458 1325 1458 1607 1176 1607 828 1607 480 1325 198 977 198 m 977 198 m 977 202 b 1323 202 1604 483 1604 828 1604 1174 1323 1454 977 1454 632 1454 351 1174 351 828 351 483 632 202 977 202{\\p0}")
|
||
ass:new_event()
|
||
ass:pos(icon_x, icon_y)
|
||
ass:append("{\\rDefault\\an7\\c&HE5E5E5&\\1a&H00&\\bord0\\shad0\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 m 895 10 m 925 42 b 1388 42 1763 418 1763 880 1763 1343 1388 1718 925 1718 463 1718 87 1343 87 880 87 418 463 42 925 42{\\p0}")
|
||
ass:new_event()
|
||
ass:pos(320, icon_y+65)
|
||
ass:an(8)
|
||
ass:append("Drop files or URLs to play here.")
|
||
mp.set_osd_ass(640, 360, ass.text)
|
||
|
||
if state.showhide_enabled then
|
||
mp.disable_key_bindings("showhide")
|
||
state.showhide_enabled = false
|
||
end
|
||
|
||
|
||
elseif (state.fullscreen and user_opts.showfullscreen)
|
||
or (not state.fullscreen and user_opts.showwindowed) then
|
||
|
||
-- render the OSC
|
||
render()
|
||
else
|
||
-- Flush OSD
|
||
mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "")
|
||
end
|
||
end
|
||
|
||
function do_enable_keybindings()
|
||
if state.enabled then
|
||
if not state.showhide_enabled then
|
||
mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
|
||
end
|
||
state.showhide_enabled = true
|
||
end
|
||
end
|
||
|
||
function enable_osc(enable)
|
||
state.enabled = enable
|
||
if enable then
|
||
do_enable_keybindings()
|
||
else
|
||
hide_osc() -- acts immediately when state.enabled == false
|
||
if state.showhide_enabled then
|
||
mp.disable_key_bindings("showhide")
|
||
end
|
||
state.showhide_enabled = false
|
||
end
|
||
end
|
||
|
||
validate_user_opts()
|
||
|
||
mp.register_event("start-file", request_init)
|
||
mp.register_event("tracks-changed", request_init)
|
||
mp.observe_property("playlist", nil, request_init)
|
||
|
||
mp.register_script_message("osc-message", show_message)
|
||
mp.register_script_message("osc-chapterlist", function(dur)
|
||
show_message(get_chapterlist(), dur)
|
||
end)
|
||
mp.register_script_message("osc-playlist", function(dur)
|
||
show_message(get_playlist(), dur)
|
||
end)
|
||
mp.register_script_message("osc-tracklist", function(dur)
|
||
local msg = {}
|
||
for k,v in pairs(nicetypes) do
|
||
table.insert(msg, get_tracklist(k))
|
||
end
|
||
show_message(table.concat(msg, '\n\n'), dur)
|
||
end)
|
||
|
||
mp.observe_property("fullscreen", "bool",
|
||
function(name, val)
|
||
state.fullscreen = val
|
||
request_init()
|
||
end
|
||
)
|
||
mp.observe_property("idle-active", "bool",
|
||
function(name, val)
|
||
state.idle = val
|
||
tick()
|
||
end
|
||
)
|
||
mp.observe_property("pause", "bool", pause_state)
|
||
mp.observe_property("cache-idle", "bool", cache_state)
|
||
mp.observe_property("vo-configured", "bool", function(name, val)
|
||
if val then
|
||
mp.register_event("tick", tick)
|
||
else
|
||
mp.unregister_event(tick)
|
||
end
|
||
end)
|
||
|
||
-- mouse show/hide bindings
|
||
mp.set_key_bindings({
|
||
{"mouse_move", function(e) process_event("mouse_move", nil) end},
|
||
{"mouse_leave", mouse_leave},
|
||
}, "showhide", "force")
|
||
do_enable_keybindings()
|
||
|
||
--mouse input bindings
|
||
mp.set_key_bindings({
|
||
{"mbtn_left", function(e) process_event("mbtn_left", "up") end,
|
||
function(e) process_event("mbtn_left", "down") end},
|
||
{"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
|
||
function(e) process_event("shift+mbtn_left", "down") end},
|
||
{"mbtn_right", function(e) process_event("mbtn_right", "up") end,
|
||
function(e) process_event("mbtn_right", "down") end},
|
||
{"wheel_up", function(e) process_event("wheel_up", "press") end},
|
||
{"wheel_down", function(e) process_event("wheel_down", "press") end},
|
||
{"mbtn_left_dbl", "ignore"},
|
||
{"shift+mbtn_left_dbl", "ignore"},
|
||
{"mbtn_right_dbl", "ignore"},
|
||
}, "input", "force")
|
||
mp.enable_key_bindings("input")
|
||
|
||
|
||
user_opts.hidetimeout_orig = user_opts.hidetimeout
|
||
|
||
function always_on(val)
|
||
if val then
|
||
user_opts.hidetimeout = -1 -- disable autohide
|
||
if state.enabled then show_osc() end
|
||
else
|
||
user_opts.hidetimeout = user_opts.hidetimeout_orig
|
||
if state.enabled then hide_osc() end
|
||
end
|
||
end
|
||
|
||
-- mode can be auto/always/never/cycle
|
||
-- the modes only affect internal variables and not stored on its own.
|
||
function visibility_mode(mode, no_osd)
|
||
if mode == "cycle" then
|
||
if not state.enabled then
|
||
mode = "auto"
|
||
elseif user_opts.hidetimeout >= 0 then
|
||
mode = "always"
|
||
else
|
||
mode = "never"
|
||
end
|
||
end
|
||
|
||
if mode == "auto" then
|
||
always_on(false)
|
||
enable_osc(true)
|
||
elseif mode == "always" then
|
||
enable_osc(true)
|
||
always_on(true)
|
||
elseif mode == "never" then
|
||
enable_osc(false)
|
||
else
|
||
msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
|
||
return
|
||
end
|
||
|
||
if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
|
||
mp.osd_message("OSC visibility: " .. mode)
|
||
end
|
||
end
|
||
|
||
visibility_mode(user_opts.visibility, true)
|
||
mp.register_script_message("osc-visibility", visibility_mode)
|
||
mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
|
||
|
||
set_virt_mouse_area(0, 0, 0, 0, "input")
|