summaryrefslogtreecommitdiffstats
path: root/mpvcore/player/lua
diff options
context:
space:
mode:
Diffstat (limited to 'mpvcore/player/lua')
-rw-r--r--mpvcore/player/lua/assdraw.lua98
-rw-r--r--mpvcore/player/lua/defaults.lua82
-rw-r--r--mpvcore/player/lua/osc.lua1288
3 files changed, 1468 insertions, 0 deletions
diff --git a/mpvcore/player/lua/assdraw.lua b/mpvcore/player/lua/assdraw.lua
new file mode 100644
index 0000000000..fc3b727f57
--- /dev/null
+++ b/mpvcore/player/lua/assdraw.lua
@@ -0,0 +1,98 @@
+local ass_mt = {}
+ass_mt.__index = ass_mt
+
+local function ass_new()
+ return setmetatable({ scale = 4, text = "" }, ass_mt)
+end
+
+function ass_mt.new_event(ass)
+ -- osd_libass.c adds an event per line
+ if #ass.text > 0 then
+ ass.text = ass.text .. "\n"
+ end
+end
+
+function ass_mt.draw_start(ass)
+ ass.text = string.format("%s{\\p%d}", ass.text, ass.scale)
+end
+
+function ass_mt.draw_stop(ass)
+ ass.text = ass.text .. "{\\p0}"
+end
+
+function ass_mt.coord(ass, x, y)
+ local scale = math.pow(2, ass.scale - 1)
+ local ix = math.ceil(x * scale)
+ local iy = math.ceil(y * scale)
+ ass.text = string.format("%s %d %d", ass.text, ix, iy)
+end
+
+function ass_mt.append(ass, s)
+ ass.text = ass.text .. s
+end
+
+function ass_mt.merge(ass1, ass2)
+ ass1.text = ass1.text .. ass2.text
+end
+
+function ass_mt.pos(ass, x, y)
+ ass:append(string.format("{\\pos(%f,%f)}", x, y))
+end
+
+function ass_mt.an(ass, an)
+ ass:append(string.format("{\\an%d}", an))
+end
+
+function ass_mt.move_to(ass, x, y)
+ ass:append(" m")
+ ass:coord(x, y)
+end
+
+function ass_mt.line_to(ass, x, y)
+ ass:append(" l")
+ ass:coord(x, y)
+end
+
+function ass_mt.bezier_curve(ass, x1, y1, x2, y2, x3, y3)
+ ass:append(" b")
+ ass:coord(x1, y1)
+ ass:coord(x2, y2)
+ ass:coord(x3, y3)
+end
+
+
+function ass_mt.rect_ccw(ass, x0, y0, x1, y1)
+ ass:move_to(x0, y0)
+ ass:line_to(x0, y1)
+ ass:line_to(x1, y1)
+ ass:line_to(x1, y0)
+end
+
+function ass_mt.rect_cw(ass, x0, y0, x1, y1)
+ ass:move_to(x0, y0)
+ ass:line_to(x1, y0)
+ ass:line_to(x1, y1)
+ ass:line_to(x0, y1)
+end
+
+function ass_mt.round_rect_cw(ass, x0, y0, x1, y1, r)
+ ass:move_to(x0 + r, y0)
+ ass:line_to(x1 - r, y0) -- top line
+ if r > 0 then
+ ass:bezier_curve(x1, y0, x1, y0, x1, y0 + r) -- top right corner
+ end
+ ass:line_to(x1, y1 - r) -- right line
+ if r > 0 then
+ ass:bezier_curve(x1, y1, x1, y1, x1 - r, y1) -- bottom right corner
+ end
+ ass:line_to(x0 + r, y1) -- bottom line
+ if r > 0 then
+ ass:bezier_curve(x0, y1, x0, y1, x0, y1 - r) -- bottom left corner
+ end
+ ass:line_to(x0, y0 + r) -- left line
+ if r > 0 then
+ ass:bezier_curve(x0, y0, x0, y0, x0 + r, y0) -- top left corner
+ end
+end
+
+return {ass_new = ass_new}
diff --git a/mpvcore/player/lua/defaults.lua b/mpvcore/player/lua/defaults.lua
new file mode 100644
index 0000000000..d24cda9cbe
--- /dev/null
+++ b/mpvcore/player/lua/defaults.lua
@@ -0,0 +1,82 @@
+
+local callbacks = {}
+-- each script has its own section, so that they don't conflict
+local default_section = "input_" .. mp.script_name
+
+-- Set the list of key bindings. These will override the user's bindings, so
+-- you should use this sparingly.
+-- A call to this function will remove all bindings previously set with this
+-- function. For example, set_key_bindings({}) would remove all script defined
+-- key bindings.
+-- Note: the bindings are not active by default. Use enable_key_bindings().
+--
+-- list is an array of key bindings, where each entry is an array as follow:
+-- {key, callback}
+-- {key, callback, callback_down}
+-- key is the key string as used in input.conf, like "ctrl+a"
+-- callback is a Lua function that is called when the key binding is used.
+-- callback_down can be given too, and is called when a mouse button is pressed
+-- if the key is a mouse button. (The normal callback will be for mouse button
+-- down.)
+--
+-- callback can be a string too, in which case the following will be added like
+-- an input.conf line: key .. " " .. callback
+-- (And callback_down is ignored.)
+function mp.set_key_bindings(list, section)
+ local cfg = ""
+ for i = 1, #list do
+ local entry = list[i]
+ local key = entry[1]
+ local cb = entry[2]
+ local cb_down = entry[3]
+ if type(cb) == "function" then
+ callbacks[#callbacks + 1] = {press=cb, before_press=cb_down}
+ cfg = cfg .. key .. " script_dispatch " .. mp.script_name
+ .. " " .. #callbacks .. "\n"
+ else
+ cfg = cfg .. key .. " " .. cb .. "\n"
+ end
+ end
+ mp.input_define_section(section or default_section, cfg)
+end
+
+function mp.enable_key_bindings(section, flags)
+ mp.input_enable_section(section or default_section, flags)
+end
+
+function mp.disable_key_bindings(section)
+ mp.input_disable_section(section or default_section)
+end
+
+function mp.set_mouse_area(x0, y0, x1, y1, section)
+ mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1)
+end
+
+-- called by C on script_dispatch input command
+function mp_script_dispatch(id, event)
+ local cb = callbacks[id]
+ if cb then
+ if event == "press" and cb.press then
+ cb.press()
+ elseif event == "keyup_follows" and cb.before_press then
+ cb.before_press()
+ end
+ end
+end
+
+mp.msg = {
+ log = mp.log,
+ fatal = function(...) return mp.log("fatal", ...) end,
+ error = function(...) return mp.log("error", ...) end,
+ warn = function(...) return mp.log("warn", ...) end,
+ info = function(...) return mp.log("info", ...) end,
+ verbose = function(...) return mp.log("verbose", ...) end,
+ debug = function(...) return mp.log("debug", ...) end,
+}
+
+_G.print = mp.msg.info
+
+package.loaded["mp"] = mp
+package.loaded["mp.msg"] = mp.msg
+
+return {}
diff --git a/mpvcore/player/lua/osc.lua b/mpvcore/player/lua/osc.lua
new file mode 100644
index 0000000000..f105d10a9c
--- /dev/null
+++ b/mpvcore/player/lua/osc.lua
@@ -0,0 +1,1288 @@
+-- osc.lua
+
+local assdraw = require 'mp.assdraw'
+local msg = require 'mp.msg'
+
+--
+-- Parameters
+--
+
+-- default user option values
+-- do not touch, change them in plugin_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 of the controller when rendered on a forced (dummy) 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)
+ 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, negative value disables autohide
+ fadeduration = 200, -- duration of fade out in ms, 0 = no fade
+ deadzonesize = 0, -- size of deadzone
+ minmousemove = 3, -- minimum amount of pixels the mouse has to move between ticks to make the OSC show up
+ seektooltip = false, -- display tooltip over the seekbar indicating time at mouse position
+ iamaprogrammer = false, -- use native mpv values and disable OSC internal playlist management (and some functions that depend on it)
+}
+
+local osc_param = {
+ osc_w = 550, -- width, height, corner-radius, padding of the OSC box
+ osc_h = 138,
+ osc_r = 10,
+ osc_p = 15,
+
+ -- calculated by osc_init()
+ playresy = 0, -- canvas size Y
+ playresx = 0, -- canvas size X
+ posX, posY = 0,0, -- position of the controler
+ pos_offsetX, pos_offsetY = 0,0, -- vertical/horizontal position offset for contents aligned at the borders of the box
+}
+
+local osc_styles = {
+ bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
+ smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20\\fnmpv-osd-symbols}",
+ smallButtonsLlabel = "{\\fs17\\fn" .. mp.property_get("options/osd-font") .. "}",
+ smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
+
+ elementDown = "{\\1c&H999999}",
+ timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
+ vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12}",
+ box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
+}
+
+-- 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 = true, -- if the right timcode should display total or remaining time
+ tc_ms = false, -- 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_seek, -- last seek position, to avoid deadlocks by repeatedly seeking to the same position
+ last_mouseX, last_mouseY, -- last mouse position, to detect siginificant mouse movement
+ message_text,
+ message_timeout,
+}
+
+--
+-- User Settings Management
+--
+
+function val2str(val)
+ local strval = val
+ if type(val) == "boolean" then
+ if val then strval = "yes" else strval = "no" end
+ end
+
+ return strval
+end
+
+-- converts val to type of desttypeval
+function typeconv(desttypeval, val)
+ if type(desttypeval) == "boolean" then
+ if val == "yes" then
+ val = true
+ elseif val == "no" then
+ val = false
+ else
+ msg.error("Error: Can't convert " .. val .. " to boolean!")
+ val = nil
+ end
+ elseif type(desttypeval) == "number" then
+ if not (tonumber(val) == nil) then
+ val = tonumber(val)
+ else
+ msg.error("Error: Can't convert " .. val .. " to number!")
+ val = nil
+ end
+ end
+ return val
+end
+
+-- Automagical config handling
+-- options: A table with options setable via config with assigned default values. The type of the default values is important for
+-- converting the values read from the config file back. Do not use "nil" as a default value!
+-- identifier: A simple indentifier string for the config file. Make sure this doesn't collide with other scripts.
+
+-- How does it work:
+-- Existance of the configfile will be checked, if it doesn't exist, the default values from the options table will be written in a new
+-- file, commented out. If it exits, the key/value pairs will be read, and values of keys that exist in the options table will overwrite
+-- their value. Keys that don't exist in the options table will be ignored, keys that don't exits in the config will keep their default
+-- value. The value's types will automatically be converted to the type used in the options table.
+function read_config(options, identifier)
+
+ local conffilename = "plugin_" .. identifier .. ".conf"
+ local conffile = mp.find_config_file(conffilename)
+ local f = io.open(conffile,"r")
+ if f == nil then
+ -- config not found
+ else
+ -- config exists, read values
+ local linecounter = 1
+ for line in f:lines() do
+ if string.find(line, "#") == 1 then
+
+ else
+ local eqpos = string.find(line, "=")
+ if eqpos == nil then
+
+ else
+ local key = string.sub(line, 1, eqpos-1)
+ local val = string.sub(line, eqpos+1)
+
+ -- match found values with defaults
+ if options[key] == nil then
+ msg.warn(conffilename..":"..linecounter.." unknown key " .. key .. ", ignoring")
+ else
+ local convval = typeconv(options[key], val)
+ if convval == nil then
+ msg.error(conffilename..":"..linecounter.." error converting value '" .. val .. "' for key '" .. key .. "'")
+ else
+ options[key] = convval
+ end
+ end
+ end
+ end
+ linecounter = linecounter + 1
+ end
+ io.close(f)
+ end
+end
+
+-- read configfile
+read_config(user_opts, "osc")
+
+
+--
+-- Helperfunctions
+--
+
+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_element_hitbox(element)
+ return element.hitbox.x1, element.hitbox.y1, element.hitbox.x2, element.hitbox.y2
+end
+
+function mouse_hit(element)
+ local mX, mY = mp.get_mouse_pos()
+ local bX1, bY1, bX2, bY2 = get_element_hitbox(element)
+
+ 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
+
+function get_slider_value(element)
+ local fill_offsetV = element.metainfo.slider.border + element.metainfo.slider.gap
+ local paddingH = (element.h - (2*fill_offsetV)) / 2
+
+ local b_x1, b_x2 = element.hitbox.x1 + paddingH, element.hitbox.x2 - paddingH
+ local s_min, s_max = element.metainfo.slider.min, element.metainfo.slider.max
+
+ local pos = scale_value(b_x1, b_x2, s_min, s_max, mp.get_mouse_pos())
+
+ return limit_range(s_min, s_max, 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
+
+--
+-- 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_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 == "unkown") 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: "
+ local select_scale = 100
+ 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 = "unkown", "", "{\\fscx" .. select_scale .. "\\fscy" .. select_scale .. "}○{\\fscx100\\fscy100}"
+ if not(track.language == nil) then lang = track.language end
+ if not(track.title == nil) then title = track.title end
+ if (track.id == tonumber(mp.property_get(type))) then
+ selected = "{\\fscx" .. select_scale .. "\\fscy" .. select_scale .. "}●{\\fscx100\\fscy100}"
+ 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.property_get(type) == "no") then
+ current_track_osc = 0
+ else
+ current_track_mpv = tonumber(mp.property_get(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.send_command("no-osd 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].language or "unkown") .. "] " .. (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.property_get(type)
+ if (track == "no" or track == nil) then
+ return 0
+ else
+ return tracks_mpv[type][tonumber(track)].osc_id
+ end
+end
+
+
+--
+-- Element Management
+--
+
+-- do not use this function, use the wrappers below
+function register_element(type, x, y, an, w, h, style, content, eventresponder, metainfo2)
+ -- type button, slider or box
+ -- x, y position
+ -- an alignment (see ASS standard)
+ -- w, h size of hitbox
+ -- style main style
+ -- content what the element should display, can be a string or a function(ass)
+ -- eventresponder A table containing functions mapped to events that shall be run on those events
+ -- metainfo A table containing additional parameters for the element
+
+ -- set default metainfo
+ local metainfo = {}
+ if not (metainfo2 == nil) then metainfo = metainfo2 end
+ if metainfo.visible == nil then metainfo.visible = true end -- element visible at all?
+ if metainfo.enabled == nil then metainfo.enabled = true end -- element clickable?
+ if metainfo.styledown == nil then metainfo.styledown = true end -- should the element be styled with the elementDown style when clicked?
+ if metainfo.softrepeat == nil then metainfo.softrepeat = false end -- should the *_down event be executed with "hold for repeat" behaviour?
+ if metainfo.alpha1 == nil then metainfo.alpha1 = 0 end -- alpha1 of the element, 0 = opaque, 255 = transparent (primary fill alpha)
+ if metainfo.alpha2 == nil then metainfo.alpha2 = 255 end -- alpha1 of the element, 0 = opaque, 255 = transparent (secondary fill alpha)
+ if metainfo.alpha3 == nil then metainfo.alpha3 = 255 end -- alpha1 of the element, 0 = opaque, 255 = transparent (border alpha)
+ if metainfo.alpha4 == nil then metainfo.alpha4 = 255 end -- alpha1 of the element, 0 = opaque, 255 = transparent (shadow alpha)
+
+ if metainfo.visible then
+ local ass = assdraw.ass_new()
+
+ ass:append("{}") -- shitty hack to troll the new_event function into inserting a \n
+ ass:new_event()
+ ass:pos(x, y) -- positioning
+ ass:an(an)
+ ass:append(style) -- styling
+
+ -- if the element is supposed to be disabled, style it accordingly and kill the eventresponders
+ if metainfo.enabled == false then
+ metainfo.alpha1 = 136
+ eventresponder = nil
+ end
+
+ -- Calculate the hitbox
+ local bX1, bY1, bX2, bY2 = get_hitbox_coords(x, y, an, w, h)
+ local hitbox
+ if type == "slider" then
+ -- if it's a slider, cut the border and gap off, as those aren't of interest for eventhandling
+ local fill_offset = metainfo.slider.border + metainfo.slider.gap
+ hitbox = {x1 = bX1 + fill_offset, y1 = bY1 + fill_offset, x2 = bX2 - fill_offset, y2 = bY2 - fill_offset}
+ else
+ hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
+ end
+
+ local element = {
+ type = type,
+ elem_ass = ass,
+ hitbox = hitbox,
+ w = w,
+ h = h,
+ x = x,
+ y = y,
+ content = content,
+ eventresponder = eventresponder,
+ metainfo = metainfo,
+ }
+
+ table.insert(elements, element)
+ end
+end
+
+function register_button(x, y, an, w, h, style, content, eventresponder, metainfo)
+ register_element("button", x, y, an, w, h, style, content, eventresponder, metainfo)
+end
+
+function register_box(x, y, an, w, h, r, style, metainfo2)
+ local ass = assdraw.ass_new()
+ ass:draw_start()
+ ass:round_rect_cw(0, 0, w, h, r)
+ ass:draw_stop()
+
+ local metainfo = {}
+ if not (metainfo2 == nil) then metainfo = metainfo2 end
+
+ metainfo.styledown = false
+
+ register_element("box", x, y, an, w, h, style, ass, nil, metainfo)
+end
+
+function register_slider(x, y, an, w, h, style, min, max, markerF, posF, eventresponder, metainfo2)
+ local metainfo = {}
+ if not (metainfo2 == nil) then metainfo = metainfo2 end
+ local slider1 = {}
+ if (metainfo.slider == nil) then metainfo.slider = slider1 end
+
+ -- defaults
+ if min == nil then metainfo.slider.min = 0 else metainfo.slider.min = min end
+ if max == nil then metainfo.slider.max = 100 else metainfo.slider.max = max end
+ if metainfo.slider.border == nil then metainfo.slider.border = 1 end
+ if metainfo.slider.gap == nil then metainfo.slider.gap = 2 end
+ if metainfo.slider.type == nil then metainfo.slider.type = "slider" end
+
+ metainfo.slider.markerF = markerF
+ metainfo.slider.posF = posF
+
+ -- prepare the box with markers
+ local ass = assdraw.ass_new()
+ local border, gap = metainfo.slider.border, metainfo.slider.gap
+ local fill_offsetV = border + gap -- Vertical offset between element outline and drag-area
+ local fill_offsetH = h / 2 -- Horizontal offset between element outline and drag-area
+
+ ass:draw_start()
+
+ -- the box
+ ass:rect_cw(0, 0, w, h);
+
+ -- the "hole"
+ ass:rect_ccw(border, border, w - border, h - border)
+
+ -- marker nibbles
+ if not (markerF == nil) and gap > 0 then
+ local markers = markerF()
+ for n = 1, #markers do
+ if (markers[n] > min) and (markers[n] < max) then
+
+ local coordL, coordR = fill_offsetH, (w - fill_offsetH)
+
+ local s = scale_value(min, max, coordL, coordR, markers[n])
+
+ if gap > 1 then
+ -- draw triangles
+ local a = gap / 0.5 --0.866
+ --top
+ ass:move_to(s - (a/2), border)
+ ass:line_to(s + (a/2), border)
+ ass:line_to(s, border + gap)
+
+ --bottom
+ ass:move_to(s - (a/2), h - border)
+ ass:line_to(s, h - border - gap)
+ ass:line_to(s + (a/2), h - border)
+
+ else
+ -- draw 1px nibbles
+ ass:rect_cw(s - 0.5, border, s + 0.5, border*2);
+ ass:rect_cw(s - 0.5, h - border*2, s + 0.5, h - border);
+ end
+
+ end
+ end
+ end
+
+ register_element("slider", x, y, an, w, h, style, ass, eventresponder, metainfo)
+end
+
+--
+-- Element Rendering
+--
+
+function render_elements(master_ass)
+
+ for n = 1, #elements do
+
+ local element = elements[n]
+ local elem_ass = assdraw.ass_new()
+ local elem_ass1 = element.elem_ass
+ elem_ass:merge(elem_ass1)
+
+ --alpha
+ local alpha1 = element.metainfo.alpha1
+ local alpha2 = element.metainfo.alpha2
+ local alpha3 = element.metainfo.alpha3
+ local alpha4 = element.metainfo.alpha4
+
+ if not(state.animation == nil) then
+ alpha1 = mult_alpha(element.metainfo.alpha1, state.animation)
+ alpha2 = mult_alpha(element.metainfo.alpha2, state.animation)
+ alpha3 = mult_alpha(element.metainfo.alpha3, state.animation)
+ alpha4 = mult_alpha(element.metainfo.alpha4, state.animation)
+ end
+
+ elem_ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}", alpha1, alpha2, alpha3, alpha4))
+
+
+ if 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.metainfo.styledown then
+ elem_ass:append(osc_styles.elementDown)
+ end
+
+ if (element.metainfo.softrepeat == true) 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
+
+ if element.type == "slider" then
+
+ elem_ass:merge(element.content) -- ASS objects
+
+ -- draw pos marker
+ local pos = element.metainfo.slider.posF()
+
+ if not (pos == nil) then
+
+ pos = limit_range(element.metainfo.slider.min, element.metainfo.slider.max, pos)
+
+ local fill_offsetV = element.metainfo.slider.border + element.metainfo.slider.gap
+ local fill_offsetH = element.h/2
+
+ local coordL, coordR = fill_offsetH, (element.w - fill_offsetH)
+
+ local xp = scale_value(element.metainfo.slider.min, element.metainfo.slider.max, coordL, coordR, pos)
+
+ -- the filling, draw it only if positive
+ local innerH = element.h - (2*fill_offsetV)
+
+ if element.metainfo.slider.type == "bar" then
+ elem_ass:rect_cw(fill_offsetV, fill_offsetV, xp, element.h - fill_offsetV)
+ else
+ elem_ass:move_to(xp, fill_offsetV)
+ elem_ass:line_to(xp+(innerH/2), (innerH/2)+fill_offsetV)
+ elem_ass:line_to(xp, (innerH)+fill_offsetV)
+ elem_ass:line_to(xp-(innerH/2), (innerH/2)+fill_offsetV)
+ end
+ end
+
+ elem_ass:draw_stop()
+
+ -- add tooltip
+ if not (element.metainfo.slider.tooltipF == nil) then
+
+ if mouse_hit(element) then
+ local sliderpos = get_slider_value(element)
+ local tooltiplabel = element.metainfo.slider.tooltipF(sliderpos)
+ local s_min, s_max = element.metainfo.slider.min, element.metainfo.slider.max
+
+ local an = 2
+ if (sliderpos < (s_min + 10)) then
+ an = 1
+ elseif (sliderpos > (s_max - 10)) then
+ an = 3
+ end
+
+ elem_ass:new_event()
+ elem_ass:pos(mp.get_mouse_pos(), element.y - (element.h) - 0) -- positioning
+ elem_ass:an(an)
+ elem_ass:append(osc_styles.vidtitle) -- styling
+ elem_ass:append(tooltiplabel)
+
+ end
+ end
+
+
+
+ elseif element.type == "box" then
+ elem_ass:merge(element.content) -- ASS objects
+ elseif type(element.content) == "function" then
+ element.content(elem_ass) -- function objects
+ else
+ elem_ass:append(element.content) -- text objects
+ end
+
+ master_ass:merge(elem_ass)
+ end
+end
+
+--
+-- Message display
+--
+
+function show_message(text, duration)
+
+ if duration == nil then
+ duration = tonumber(mp.property_get("options/osd-duration")) / 1000
+ 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 and get the amount of lines along the way
+ local lines
+ text, lines = string.gsub(text, "\n", "\\N")
+
+ -- append a Zero-Width-Space to . and _ to enable linebreaking of long filenames
+ text = string.gsub(text, "%.", ".\226\128\139")
+ text = string.gsub(text, "_", "_\226\128\139")
+
+ -- scale the fontsize for longer multi-line output
+ local fontsize, outline = tonumber(mp.property_get("options/osd-font-size")), tonumber(mp.property_get("options/osd-border-size"))
+ if lines > 12 then
+ fontsize, outline = fontsize / 2, outline / 1.5
+ elseif lines > 8 then
+ fontsize, outline = fontsize / 1.5, outline / 1.25
+ end
+
+ local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
+
+ state.message_text = style .. text
+ state.message_timeout = mp.get_timer() + duration
+end
+
+function render_message(ass)
+ if not(state.message_timeout == nil) and not(state.message_text == nil) and state.message_timeout > mp.get_timer() then
+ ass:new_event()
+ ass:append(state.message_text)
+ else
+ state.message_text = nil
+ state.message_timeout = nil
+ end
+end
+
+--
+-- Initialisation and Layout
+--
+
+-- OSC INIT
+function osc_init()
+ -- kill old Elements
+ elements = {}
+
+ -- set canvas resolution according to display aspect and scaling setting
+ local baseResY = 720
+ local display_w, display_h, display_aspect = mp.get_screen_size()
+ local scale = 1
+
+ if (mp.property_get("video") == "no") then -- dummy/forced window
+ scale = user_opts.scaleforcedwindow
+ elseif (mp.property_get("fullscreen") == "yes") then
+ scale = user_opts.scalefullscreen
+ else
+ scale = user_opts.scalewindowed
+ end
+
+
+ if user_opts.vidscale then
+ osc_param.playresy = baseResY / scale
+ else
+ osc_param.playresy = display_h / scale
+ end
+ osc_param.playresx = osc_param.playresy * display_aspect
+
+ -- make sure the OSC actually fits into the video
+ if (osc_param.playresx < (osc_param.osc_w + (2 * osc_param.osc_p))) then
+ osc_param.playresy = (osc_param.osc_w + (2 * osc_param.osc_p)) / display_aspect
+ osc_param.playresx = osc_param.playresy * display_aspect
+ end
+
+ -- position of the controller according to video aspect and valignment
+ osc_param.posX = math.floor(get_align(user_opts.halign, osc_param.playresx, osc_param.osc_w, 0))
+ osc_param.posY = math.floor(get_align(user_opts.valign, osc_param.playresy, osc_param.osc_h, 0))
+
+ -- Some calculations on stuff we'll need
+ -- vertical/horizontal position offset for contents aligned at the borders of the box
+ osc_param.pos_offsetX, osc_param.pos_offsetY = (osc_param.osc_w - (2*osc_param.osc_p)) / 2, (osc_param.osc_h - (2*osc_param.osc_p)) / 2
+
+ -- fetch values
+ local osc_w, osc_h, osc_r, osc_p = osc_param.osc_w, osc_param.osc_h, osc_param.osc_r, osc_param.osc_p
+ local pos_offsetX, pos_offsetY = osc_param.pos_offsetX, osc_param.pos_offsetY
+ local posX, posY = osc_param.posX, osc_param.posY
+
+ --
+ -- Backround box
+ --
+
+ local metainfo = {}
+ metainfo.alpha1 = user_opts.boxalpha
+ metainfo.alpha3 = user_opts.boxalpha
+ register_box(posX, posY, 5, osc_w, osc_h, osc_r, osc_styles.box, metainfo)
+
+ --
+ -- Title row
+ --
+
+ local titlerowY = posY - pos_offsetY - 10
+
+ -- title
+ local contentF = function (ass)
+ local title = mp.property_get_string("media-title")
+ if not (title == nil) then
+
+ if #title > 80 then
+ title = string.format("{\\fscx%f}", (80 / #title) * 100) .. title
+ end
+
+ ass:append(title)
+ else
+ ass:append("mpv")
+ end
+ end
+
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function ()
+
+ local title = mp.property_get("media-title")
+ local pl_count = tonumber(mp.property_get("playlist-count"))
+
+ if pl_count > 1 then
+ local playlist_pos = countone(tonumber(mp.property_get("playlist-pos")))
+ title = "[" .. playlist_pos .. "/" .. pl_count .. "] " .. title
+ end
+
+ show_message(title)
+ end
+ eventresponder.mouse_btn2_up = function () show_message(mp.property_get("filename")) end
+
+ register_button(posX, titlerowY, 8, 496, 12, osc_styles.vidtitle, contentF, eventresponder, nil)
+
+ -- If we have more than one playlist entry, render playlist navigation buttons
+ local metainfo = {}
+ metainfo.visible = (tonumber(mp.property_get("playlist-count")) > 1)
+
+ -- playlist prev
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("playlist_prev weak") end
+ eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("playlist"), 3) end
+ register_button(posX - pos_offsetX, titlerowY, 7, 12, 12, osc_styles.vidtitle, "◀", eventresponder, metainfo)
+
+ -- playlist next
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("playlist_next weak") end
+ eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("playlist"), 3) end
+ register_button(posX + pos_offsetX, titlerowY, 9, 12, 12, osc_styles.vidtitle, "▶", eventresponder, metainfo)
+
+ --
+ -- Big buttons
+ --
+
+ local bigbuttonrowY = posY - pos_offsetY + 35
+ local bigbuttondistance = 60
+
+ --play/pause
+ local contentF = function (ass)
+ if mp.property_get("pause") == "yes" then
+ ass:append("\238\132\129")
+ else
+