From e44911142914783c9ec717f329bd9b6a8bb9b70e Mon Sep 17 00:00:00 2001 From: wm4 Date: Tue, 17 Dec 2013 00:53:22 +0100 Subject: Move mpvcore/player/ to player/ --- player/lua/assdraw.lua | 98 ++++ player/lua/defaults.lua | 82 +++ player/lua/osc.lua | 1288 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1468 insertions(+) create mode 100644 player/lua/assdraw.lua create mode 100644 player/lua/defaults.lua create mode 100644 player/lua/osc.lua (limited to 'player/lua') diff --git a/player/lua/assdraw.lua b/player/lua/assdraw.lua new file mode 100644 index 0000000000..fc3b727f57 --- /dev/null +++ b/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/player/lua/defaults.lua b/player/lua/defaults.lua new file mode 100644 index 0000000000..d24cda9cbe --- /dev/null +++ b/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/player/lua/osc.lua b/player/lua/osc.lua new file mode 100644 index 0000000000..d1dbf3e8b4 --- /dev/null +++ b/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 by 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 , 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 + ass:append("\238\128\130") + end + end + local eventresponder = {} + eventresponder.mouse_btn0_up = function () mp.send_command("no-osd cycle pause") end + register_button(posX, bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, contentF, eventresponder, nil) + + --skipback + local metainfo = {} + metainfo.softrepeat = true + + local eventresponder = {} + eventresponder.mouse_btn0_down = function () mp.send_command("no-osd seek -5 relative keyframes") end + eventresponder["shift+mouse_btn0_down"] = function () mp.send_command("no-osd frame_back_step") end + eventresponder.mouse_btn2_down = function () mp.send_command("no-osd seek -30 relative keyframes") end + register_button(posX - bigbuttondistance, bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\128\132", eventresponder, metainfo) + + --skipfrwd + local eventresponder = {} + eventresponder.mouse_btn0_down = function () mp.send_command("no-osd seek 10 relative keyframes") end + eventresponder["shift+mouse_btn0_down"] = function () mp.send_command("no-osd frame_step") end + eventresponder.mouse_btn2_down = function () mp.send_command("no-osd seek 60 relative keyframes") end + register_button(posX + bigbuttondistance, bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\128\133", eventresponder, metainfo) + + --chapters + -- do we have any? + local metainfo = {} + metainfo.enabled = ((#mp.get_chapter_list()) > 0) + + --prev + local eventresponder = {} + eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add chapter -1") end + eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("chapter-list"), 3) end + register_button(posX - (bigbuttondistance * 2), bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\132\132", eventresponder, metainfo) + + --next + local eventresponder = {} + eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add chapter 1") end + eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("chapter-list"), 3) end + register_button(posX + (bigbuttondistance * 2), bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\132\133", eventresponder, metainfo) + + + -- + -- Smaller buttons + -- + + if not (user_opts.iamaprogrammer) then + update_tracklist() + end + + --cycle audio tracks + + local metainfo = {} + local eventresponder = {} + local contentF + + if not (user_opts.iamaprogrammer) then + metainfo.enabled = (#tracks_osc.audio > 0) + + contentF = function (ass) + local aid = "–" + if not (get_track("audio") == 0) then + aid = get_track("audio") + end + ass:append("\238\132\134" .. osc_styles.smallButtonsLlabel .. " " .. aid .. "/" .. #tracks_osc.audio) + end + + eventresponder.mouse_btn0_up = function () set_track("audio", 1) end + eventresponder.mouse_btn2_up = function () set_track("audio", -1) end + eventresponder["shift+mouse_btn0_down"] = function () + show_message(get_tracklist("audio"), 2) + end + else + metainfo.enabled = true + contentF = function (ass) + local aid = mp.property_get("audio") + + ass:append("\238\132\134" .. osc_styles.smallButtonsLlabel .. " " .. aid) + end + + eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add audio 1") end + eventresponder.mouse_btn2_up = function () mp.send_command("osd-msg add audio -1") end + end + + register_button(posX - pos_offsetX, bigbuttonrowY, 1, 70, 18, osc_styles.smallButtonsL, contentF, eventresponder, metainfo) + + + --cycle sub tracks + + local metainfo = {} + local eventresponder = {} + local contentF + + if not (user_opts.iamaprogrammer) then + metainfo.enabled = (#tracks_osc.sub > 0) + + contentF = function (ass) + local sid = "–" + if not (get_track("sub") == 0) then + sid = get_track("sub") + end + ass:append("\238\132\135" .. osc_styles.smallButtonsLlabel .. " " .. sid .. "/" .. #tracks_osc.sub) + end + + eventresponder.mouse_btn0_up = function () set_track("sub", 1) end + eventresponder.mouse_btn2_up = function () set_track("sub", -1) end + eventresponder["shift+mouse_btn0_down"] = function () + show_message(get_tracklist("sub"), 2) + end + else + metainfo.enabled = true + contentF = function (ass) + local sid = mp.property_get("sub") + + ass:append("\238\132\135" .. osc_styles.smallButtonsLlabel .. " " .. sid) + end + + eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add sub 1") end + eventresponder.mouse_btn2_up = function () mp.send_command("osd-msg add sub -1") end + end + register_button(posX - pos_offsetX, bigbuttonrowY, 7, 70, 18, osc_styles.smallButtonsL, contentF, eventresponder, metainfo) + + + --toggle FS + local contentF = function (ass) + if mp.property_get("fullscreen") == "yes" then + ass:append("\238\132\137") + else + ass:append("\238\132\136") + end + end + local eventresponder = {} + eventresponder.mouse_btn0_up = function () mp.send_command("no-osd cycle fullscreen") end + register_button(posX+pos_offsetX, bigbuttonrowY, 6, 25, 25, osc_styles.smallButtonsR, contentF, eventresponder, nil) + + + -- + -- Seekbar + -- + + local markerF = function () + local duration = 0 + if not (mp.property_get("length") == nil) then + duration = tonumber(mp.property_get("length")) + end + + local chapters = mp.get_chapter_list() + local markers = {} + for n = 1, #chapters do + markers[n] = (chapters[n].time / duration * 100) + end + return markers + end + + local posF = function () + if mp.property_get("length") == nil then + return nil + else + return tonumber(mp.property_get("percent-pos")) + end + end + + local tooltipF = function (pos) + if not (mp.property_get("length") == nil) then + duration = tonumber(mp.property_get("length")) + possec = duration * (pos / 100) + return mp.format_time(possec) + else + return nil + end + end + + local metainfo = {} + + + metainfo.enabled = (not (mp.property_get("length") == nil)) and (tonumber(mp.property_get("length")) > 0) + metainfo.styledown = false + metainfo.slider = {} + metainfo.slider.border = 1 + metainfo.slider.gap = 1 -- >1 will draw triangle markers + metainfo.slider.type = "slider" -- "bar" for old bar-style filling + if (user_opts.seektooltip) and (not (mp.property_get("length") == nil)) then + metainfo.slider.tooltipF = tooltipF + end + + local eventresponder = {} + local sliderF = function (element) + local seek_to = get_slider_value(element) + -- ignore identical seeks + if not(state.last_seek == seek_to) then + mp.send_command(string.format("no-osd seek %f absolute-percent keyframes", seek_to)) + state.last_seek = seek_to + end + end + eventresponder.render = sliderF + eventresponder.mouse_btn0_down = sliderF + register_slider(posX, posY+pos_offsetY-22, 2, pos_offsetX*2, 15, osc_styles.timecodes, 0, 100, markerF, posF, eventresponder, metainfo) + + -- + -- Timecodes + Volume + -- + + local bottomrowY = posY + pos_offsetY - 5 + + -- left (current pos) + local metainfo = {} + local eventresponder = {} + + local contentF = function (ass) + if state.tc_ms then + ass:append(mp.property_get_string("time-pos/full")) + else + ass:append(mp.property_get_string("time-pos")) + end + end + + eventresponder.mouse_btn0_up = function () state.tc_ms = not state.tc_ms end + register_button(posX - pos_offsetX, bottomrowY, 4, 110, 18, osc_styles.timecodes, contentF, eventresponder, metainfo) + + -- center (Cache) + local metainfo = {} + local eventresponder = {} + + local contentF = function (ass) + local cache = mp.property_get("cache") + if not (cache == nil) then + cache = tonumber(mp.property_get("cache")) + if (cache < 45) then + ass:append("Cache: " .. (cache) .."%") + end + end + end + register_button(posX, bottomrowY, 5, 110, 18, osc_styles.timecodes, contentF, eventresponder, metainfo) + + + -- right (total/remaining time) + -- do we have a usuable duration? + local metainfo = {} + metainfo.visible = (not (mp.property_get("length") == nil)) and (tonumber(mp.property_get("length")) > 0) + + local contentF = function (ass) + if state.rightTC_trem == true then + if state.tc_ms then + ass:append("-" .. mp.property_get_string("playtime-remaining/full")) + else + ass:append("-" .. mp.property_get_string("playtime-remaining")) + end + else + if state.tc_ms then + ass:append(mp.property_get_string("length/full")) + else + ass:append(mp.property_get_string("length")) + end + end + end + local eventresponder = {} + eventresponder.mouse_btn0_up = function () state.rightTC_trem = not state.rightTC_trem end + + register_button(posX + pos_offsetX, bottomrowY, 6, 110, 18, osc_styles.timecodes, contentF, eventresponder, metainfo) + +end + +-- +-- Other important stuff +-- + + +function show_osc() + + --remember last time of invocation (mouse move) + state.showtime = mp.get_timer() + + state.osc_visible = true + + if (user_opts.fadeduration > 0) then + state.anitype = nil + end + +end + +function hide_osc() + if (user_opts.fadeduration > 0) then + if not(state.osc_visible == false) then + state.anitype = "out" + end + else + state.osc_visible = false + end +end + +function mouse_leave() + hide_osc() + -- reset mouse position + state.last_mouseX, state.last_mouseY = nil, nil +end + +function request_init() + state.initREQ = true +end + +function render() + local current_screen_sizeX, current_screen_sizeY = mp.get_screen_size() + local mouseX, mouseY = mp.get_mouse_pos() + local now = mp.get_timer() + + -- 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, state.mp_screen_sizeY = current_screen_sizeX, 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 + + -- 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 (mouseX >= osc_param.posX - (osc_param.osc_w / 2) and mouseX <= osc_param.posX + (osc_param.osc_w / 2) + and mouseY >= osc_param.posY - (osc_param.osc_h / 2) and mouseY <= osc_param.posY + (osc_param.osc_h / 2)) then + hide_osc() + 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 + state.osc_visible = true + state.animation = scale_value(state.anistart, (state.anistart + (user_opts.fadeduration/1000)), 255, 0, now) + elseif (state.anitype == "out") then --fade in + state.animation = scale_value(state.anistart, (state.anistart + (user_opts.fadeduration/1000)), 0, 255, now) + end + + else + if (state.anitype == "out") then state.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 + + -- actual rendering + local ass = assdraw.ass_new() + + -- Messages + render_message(ass) + + -- actual OSC + if state.osc_visible then + render_elements(ass) + end + + -- submit + local w, h, aspect = mp.get_screen_size() + mp.set_osd_ass(osc_param.playresy * aspect, osc_param.playresy, ass.text) + + -- set mouse area + local area_y0, area_y1 + if user_opts.valign > 0 then + -- deadzone above OSC + area_y0 = get_align(-1 + (2*user_opts.deadzonesize), osc_param.posY - (osc_param.osc_h / 2), 0, 0) + area_y1 = osc_param.playresy + else + -- deadzone below OSC + area_y0 = 0 + area_y1 = (osc_param.posY + (osc_param.osc_h / 2)) + + get_align(1 - (2*user_opts.deadzonesize), osc_param.playresy - (osc_param.posY + (osc_param.osc_h / 2)), 0, 0) + end + + --mouse show/hide area + mp.set_mouse_area(0, area_y0, osc_param.playresx, area_y1, "showhide") + + --mouse input area + if state.osc_visible then -- activate only when OSC is actually visible + mp.set_mouse_area( + osc_param.posX - (osc_param.osc_w / 2), osc_param.posY - (osc_param.osc_h / 2), + osc_param.posX + (osc_param.osc_w / 2), osc_param.posY + (osc_param.osc_h / 2), + "input") + mp.enable_key_bindings("input") + else + mp.disable_key_bindings("input") + end + +end + +-- +-- Eventhandling +-- + +function process_event(source, what) + + if what == "down" then + + for n = 1, #elements do + + if not (elements[n].eventresponder == nil) then + if not (elements[n].eventresponder[source .. "_up"] == nil) or not (elements[n].eventresponder[source .. "_down"] == nil) then + + if mouse_hit(elements[n]) then + state.active_element = n + state.active_event_source = source + -- fire the down event if the element has one + if not (elements[n].eventresponder[source .. "_" .. what] == nil) then + elements[n].eventresponder[source .. "_" .. what](elements[n]) + end + end + end + + end + end + + elseif what == "up" then + + if not (state.active_element == nil) then + + local n = state.active_element + + if n == 0 then + --click on background (does not work) + elseif n > 0 and not (elements[n].eventresponder[source .. "_" .. what] == nil) then + + if mouse_hit(elements[n]) then + elements[n].eventresponder[source .. "_" .. what](elements[n]) + end + end + end + state.active_element = nil + state.mouse_down_counter = 0 + state.last_seek = nil + + elseif source == "mouse_move" then + local mouseX, mouseY = mp.get_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 + + if not (state.active_element == nil) then + + local n = state.active_element + + if not (elements[n].eventresponder == nil) then + if not (elements[n].eventresponder[source] == nil) then + elements[n].eventresponder[source](elements[n]) + end + end + end + end +end + +-- called by mpv on every frame +function tick() + if (mp.property_get("fullscreen") == "yes" and user_opts.showfullscreen) or (mp.property_get("fullscreen") == "no" and user_opts.showwindowed) then + render() + else + mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "") + end +end + +function mp_event(name, arg) + if name == "tick" then + tick() + elseif name == "start" or name == "track-layout" then + request_init() + elseif name == "end" then + 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") +mp.enable_key_bindings("showhide", "allow-vo-dragging|allow-hide-cursor") + +--mouse input bindings +mp.set_key_bindings({ + {"mouse_btn0", function(e) process_event("mouse_btn0", "up") end, + function(e) process_event("mouse_btn0", "down") end}, + {"shift+mouse_btn0", function(e) process_event("shift+mouse_btn0", "up") end, + function(e) process_event("shift+mouse_btn0", "down") end}, + {"mouse_btn2", function(e) process_event("mouse_btn2", "up") end, + function(e) process_event("mouse_btn2", "down") end}, + {"mouse_btn0_dbl", "ignore"}, + {"shift+mouse_btn0_dbl", "ignore"}, + {"mouse_btn2_dbl", "ignore"}, +}, "input") +mp.enable_key_bindings("input") -- cgit v1.2.3