Advertisement
Guest User

pinephone osc.lua

a guest
Dec 13th, 2021
2,593
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 98.96 KB | None | 0 0
  1. local assdraw = require 'mp.assdraw'
  2. local msg = require 'mp.msg'
  3. local opt = require 'mp.options'
  4. local utils = require 'mp.utils'
  5.  
  6. --
  7. -- Parameters
  8. --
  9. -- default user option values
  10. -- do not touch, change them in osc.conf
  11. local user_opts = {
  12. showwindowed = true, -- show OSC when windowed?
  13. showfullscreen = true, -- show OSC when fullscreen?
  14. scalewindowed = 1, -- scaling of the controller when windowed
  15. scalefullscreen = 1, -- scaling of the controller when fullscreen
  16. scaleforcedwindow = 2, -- scaling when rendered on a forced window
  17. vidscale = true, -- scale the controller with the video?
  18. valign = 1, -- vertical alignment, -1 (top) to 1 (bottom)
  19. halign = 0, -- horizontal alignment, -1 (left) to 1 (right)
  20. barmargin = 0, -- vertical margin of top/bottombar
  21. boxalpha = 80, -- alpha of the background box,
  22. -- 0 (opaque) to 255 (fully transparent)
  23. hidetimeout = 1000, -- duration in ms until the OSC hides if no
  24. -- mouse movement. enforced non-negative for the
  25. -- user, but internally negative is "always-on".
  26. fadeduration = 200, -- duration of fade out in ms, 0 = no fade
  27. deadzonesize = 0.5, -- size of deadzone
  28. minmousemove = 0, -- minimum amount of pixels the mouse has to
  29. -- move between ticks to make the OSC show up
  30. iamaprogrammer = false, -- use native mpv values and disable OSC
  31. -- internal track list management (and some
  32. -- functions that depend on it)
  33. layout = "slimbox",
  34. seekbarstyle = "bar", -- bar, diamond or knob
  35. seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle
  36. seekrangestyle = "inverted",-- bar, line, slider, inverted or none
  37. seekrangeseparate = true, -- wether the seekranges overlay on the bar-style seekbar
  38. seekrangealpha = 200, -- transparency of seekranges
  39. seekbarkeyframes = true, -- use keyframes when dragging the seekbar
  40. title = "${media-title}", -- string compatible with property-expansion
  41. -- to be shown as OSC title
  42. tooltipborder = 1, -- border of tooltip in bottom/topbar
  43. timetotal = false, -- display total time instead of remaining time?
  44. timems = false, -- display timecodes with milliseconds?
  45. visibility = "auto", -- only used at init to set visibility_mode(...)
  46. boxmaxchars = 80, -- title crop threshold for box layout
  47. boxvideo = false, -- apply osc_param.video_margins to video
  48. windowcontrols = "auto", -- whether to show window controls
  49. windowcontrols_alignment = "right", -- which side to show window controls on
  50. greenandgrumpy = false, -- disable santa hat
  51. livemarkers = true, -- update seekbar chapter markers on duration change
  52. chapters_osd = true, -- whether to show chapters OSD on next/prev
  53. playlist_osd = true, -- whether to show playlist OSD on next/prev
  54. chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable
  55. }
  56.  
  57. -- read options from config and command-line
  58. opt.read_options(user_opts, "osc", function(list) update_options(list) end)
  59.  
  60. local osc_param = { -- calculated by osc_init()
  61. playresy = 0, -- canvas size Y
  62. playresx = 0, -- canvas size X
  63. display_aspect = 1,
  64. unscaled_y = 0,
  65. areas = {},
  66. video_margins = {
  67. l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom
  68. },
  69. }
  70.  
  71. local osc_styles = {
  72. bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
  73. smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
  74. smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}",
  75. smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
  76. topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
  77.  
  78. elementDown = "{\\1c&H999999}",
  79. timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
  80. vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
  81. box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
  82.  
  83. topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
  84. smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
  85. timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
  86. timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
  87. vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
  88.  
  89. wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}",
  90. wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}",
  91. wcBar = "{\\1c&H000000}",
  92. }
  93.  
  94. -- internal states, do not touch
  95. local state = {
  96. showtime, -- time of last invocation (last mouse move)
  97. osc_visible = false,
  98. anistart, -- time when the animation started
  99. anitype, -- current type of animation
  100. animation, -- current animation alpha
  101. mouse_down_counter = 0, -- used for softrepeat
  102. active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
  103. active_event_source = nil, -- the "button" that issued the current event
  104. rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
  105. tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
  106. mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
  107. initREQ = false, -- is a re-init request pending?
  108. marginsREQ = false, -- is a margins update pending?
  109. last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
  110. mouse_in_window = false,
  111. message_text,
  112. message_hide_timer,
  113. fullscreen = false,
  114. tick_timer = nil,
  115. tick_last_time = 0, -- when the last tick() was run
  116. hide_timer = nil,
  117. cache_state = nil,
  118. idle = false,
  119. enabled = true,
  120. input_enabled = true,
  121. showhide_enabled = false,
  122. dmx_cache = 0,
  123. using_video_margins = false,
  124. border = true,
  125. maximized = false,
  126. osd = mp.create_osd_overlay("ass-events"),
  127. }
  128.  
  129. local window_control_box_width = 80
  130. local tick_delay = 0.03
  131.  
  132. local is_december = os.date("*t").month == 12
  133.  
  134. --
  135. -- Helperfunctions
  136. --
  137.  
  138. function kill_animation()
  139. state.anistart = nil
  140. state.animation = nil
  141. state.anitype = nil
  142. end
  143.  
  144. function set_osd(res_x, res_y, text)
  145. if state.osd.res_x == res_x and
  146. state.osd.res_y == res_y and
  147. state.osd.data == text then
  148. return
  149. end
  150. state.osd.res_x = res_x
  151. state.osd.res_y = res_y
  152. state.osd.data = text
  153. state.osd.z = 1000
  154. state.osd:update()
  155. end
  156.  
  157. local margins_opts = {
  158. {"l", "video-margin-ratio-left"},
  159. {"r", "video-margin-ratio-right"},
  160. {"t", "video-margin-ratio-top"},
  161. {"b", "video-margin-ratio-bottom"},
  162. }
  163.  
  164. -- scale factor for translating between real and virtual ASS coordinates
  165. function get_virt_scale_factor()
  166. local w, h = mp.get_osd_size()
  167. if w <= 0 or h <= 0 then
  168. return 0, 0
  169. end
  170. return osc_param.playresx / w, osc_param.playresy / h
  171. end
  172.  
  173. -- return mouse position in virtual ASS coordinates (playresx/y)
  174. function get_virt_mouse_pos()
  175. if state.mouse_in_window then
  176. local sx, sy = get_virt_scale_factor()
  177. local x, y = mp.get_mouse_pos()
  178. --show_message(x * sx)
  179. return x * sx, y * sy
  180. else
  181. return -1, -1
  182. end
  183. end
  184.  
  185. function set_virt_mouse_area(x0, y0, x1, y1, name)
  186. local sx, sy = get_virt_scale_factor()
  187. mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
  188. end
  189.  
  190. function scale_value(x0, x1, y0, y1, val)
  191. local m = (y1 - y0) / (x1 - x0)
  192. local b = y0 - (m * x0)
  193. return (m * val) + b
  194. end
  195.  
  196. -- returns hitbox spanning coordinates (top left, bottom right corner)
  197. -- according to alignment
  198. function get_hitbox_coords(x, y, an, w, h)
  199.  
  200. local alignments = {
  201. [1] = function () return x, y-h, x+w, y end,
  202. [2] = function () return x-(w/2), y-h, x+(w/2), y end,
  203. [3] = function () return x-w, y-h, x, y end,
  204.  
  205. [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
  206. [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
  207. [6] = function () return x-w, y-(h/2), x, y+(h/2) end,
  208.  
  209. [7] = function () return x, y, x+w, y+h end,
  210. [8] = function () return x-(w/2), y, x+(w/2), y+h end,
  211. [9] = function () return x-w, y, x, y+h end,
  212. }
  213.  
  214. return alignments[an]()
  215. end
  216.  
  217. function get_hitbox_coords_geo(geometry)
  218. return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
  219. geometry.w, geometry.h)
  220. end
  221.  
  222. function get_element_hitbox(element)
  223. return element.hitbox.x1, element.hitbox.y1,
  224. element.hitbox.x2, element.hitbox.y2
  225. end
  226.  
  227. function mouse_hit(element)
  228. return mouse_hit_coords(get_element_hitbox(element))
  229. end
  230.  
  231. function mouse_hit_coords(bX1, bY1, bX2, bY2)
  232. local mX, mY = get_virt_mouse_pos()
  233. return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
  234. end
  235.  
  236. function limit_range(min, max, val)
  237. if val > max then
  238. val = max
  239. elseif val < min then
  240. val = min
  241. end
  242. return val
  243. end
  244.  
  245. -- translate value into element coordinates
  246. function get_slider_ele_pos_for(element, val)
  247.  
  248. local ele_pos = scale_value(
  249. element.slider.min.value, element.slider.max.value,
  250. element.slider.min.ele_pos, element.slider.max.ele_pos,
  251. val)
  252.  
  253. return limit_range(
  254. element.slider.min.ele_pos, element.slider.max.ele_pos,
  255. ele_pos)
  256. end
  257.  
  258. -- translates global (mouse) coordinates to value
  259. function get_slider_value_at(element, glob_pos)
  260.  
  261. local val = scale_value(
  262. element.slider.min.glob_pos, element.slider.max.glob_pos,
  263. element.slider.min.value, element.slider.max.value,
  264. glob_pos)
  265.  
  266. return limit_range(
  267. element.slider.min.value, element.slider.max.value,
  268. val)
  269. end
  270.  
  271. -- get value at current mouse position
  272. function get_slider_value(element)
  273. return get_slider_value_at(element, get_virt_mouse_pos())
  274. end
  275.  
  276. function countone(val)
  277. if not (user_opts.iamaprogrammer) then
  278. val = val + 1
  279. end
  280. return val
  281. end
  282.  
  283. -- align: -1 .. +1
  284. -- frame: size of the containing area
  285. -- obj: size of the object that should be positioned inside the area
  286. -- margin: min. distance from object to frame (as long as -1 <= align <= +1)
  287. function get_align(align, frame, obj, margin)
  288. return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
  289. end
  290.  
  291. -- multiplies two alpha values, formular can probably be improved
  292. function mult_alpha(alphaA, alphaB)
  293. return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
  294. end
  295.  
  296. function add_area(name, x1, y1, x2, y2)
  297. -- create area if needed
  298. if (osc_param.areas[name] == nil) then
  299. osc_param.areas[name] = {}
  300. end
  301. table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
  302. end
  303.  
  304. function ass_append_alpha(ass, alpha, modifier)
  305. local ar = {}
  306.  
  307. for ai, av in pairs(alpha) do
  308. av = mult_alpha(av, modifier)
  309. if state.animation then
  310. av = mult_alpha(av, state.animation)
  311. end
  312. ar[ai] = av
  313. end
  314.  
  315. ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
  316. ar[1], ar[2], ar[3], ar[4]))
  317. end
  318.  
  319. function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
  320. if hexagon then
  321. ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
  322. else
  323. ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
  324. end
  325. end
  326.  
  327. function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
  328. if hexagon then
  329. ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
  330. else
  331. ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
  332. end
  333. end
  334.  
  335.  
  336. --
  337. -- Tracklist Management
  338. --
  339.  
  340. local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
  341.  
  342. -- updates the OSC internal playlists, should be run each time the track-layout changes
  343. function update_tracklist()
  344. local tracktable = mp.get_property_native("track-list", {})
  345.  
  346. -- by osc_id
  347. tracks_osc = {}
  348. tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
  349. -- by mpv_id
  350. tracks_mpv = {}
  351. tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
  352. for n = 1, #tracktable do
  353. if not (tracktable[n].type == "unknown") then
  354. local type = tracktable[n].type
  355. local mpv_id = tonumber(tracktable[n].id)
  356.  
  357. -- by osc_id
  358. table.insert(tracks_osc[type], tracktable[n])
  359.  
  360. -- by mpv_id
  361. tracks_mpv[type][mpv_id] = tracktable[n]
  362. tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
  363. end
  364. end
  365. end
  366.  
  367. -- return a nice list of tracks of the given type (video, audio, sub)
  368. function get_tracklist(type)
  369. local msg = "Available " .. nicetypes[type] .. " Tracks: "
  370. if #tracks_osc[type] == 0 then
  371. msg = msg .. "none"
  372. else
  373. for n = 1, #tracks_osc[type] do
  374. local track = tracks_osc[type][n]
  375. local lang, title, selected = "unknown", "", "○"
  376. if not(track.lang == nil) then lang = track.lang end
  377. if not(track.title == nil) then title = track.title end
  378. if (track.id == tonumber(mp.get_property(type))) then
  379. selected = "●"
  380. end
  381. msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title
  382. end
  383. end
  384. return msg
  385. end
  386.  
  387. -- relatively change the track of given <type> by <next> tracks
  388. --(+1 -> next, -1 -> previous)
  389. function set_track(type, next)
  390. local current_track_mpv, current_track_osc
  391. if (mp.get_property(type) == "no") then
  392. current_track_osc = 0
  393. else
  394. current_track_mpv = tonumber(mp.get_property(type))
  395. current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
  396. end
  397. local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
  398. local new_track_mpv
  399. if new_track_osc == 0 then
  400. new_track_mpv = "no"
  401. else
  402. new_track_mpv = tracks_osc[type][new_track_osc].id
  403. end
  404.  
  405. mp.commandv("set", type, new_track_mpv)
  406.  
  407. if (new_track_osc == 0) then
  408. show_message(nicetypes[type] .. " Track: none")
  409. else
  410. show_message(nicetypes[type] .. " Track: "
  411. .. new_track_osc .. "/" .. #tracks_osc[type]
  412. .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
  413. .. (tracks_osc[type][new_track_osc].title or ""))
  414. end
  415. end
  416.  
  417. -- get the currently selected track of <type>, OSC-style counted
  418. function get_track(type)
  419. local track = mp.get_property(type)
  420. if track ~= "no" and track ~= nil then
  421. local tr = tracks_mpv[type][tonumber(track)]
  422. if tr then
  423. return tr.osc_id
  424. end
  425. end
  426. return 0
  427. end
  428.  
  429. -- WindowControl helpers
  430. function window_controls_enabled()
  431. val = user_opts.windowcontrols
  432. if val == "auto" then
  433. return not state.border
  434. else
  435. return val ~= "no"
  436. end
  437. end
  438.  
  439. function window_controls_alignment()
  440. return user_opts.windowcontrols_alignment
  441. end
  442.  
  443. --
  444. -- Element Management
  445. --
  446.  
  447. local elements = {}
  448.  
  449. function prepare_elements()
  450.  
  451. -- remove elements without layout or invisble
  452. local elements2 = {}
  453. for n, element in pairs(elements) do
  454. if not (element.layout == nil) and (element.visible) then
  455. table.insert(elements2, element)
  456. end
  457. end
  458. elements = elements2
  459.  
  460. function elem_compare (a, b)
  461. return a.layout.layer < b.layout.layer
  462. end
  463.  
  464. table.sort(elements, elem_compare)
  465.  
  466.  
  467. for _,element in pairs(elements) do
  468.  
  469. local elem_geo = element.layout.geometry
  470.  
  471. -- Calculate the hitbox
  472. local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
  473. element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
  474.  
  475. local style_ass = assdraw.ass_new()
  476.  
  477. -- prepare static elements
  478. style_ass:append("{}") -- hack to troll new_event into inserting a \n
  479. style_ass:new_event()
  480. style_ass:pos(elem_geo.x, elem_geo.y)
  481. style_ass:an(elem_geo.an)
  482. style_ass:append(element.layout.style)
  483.  
  484. element.style_ass = style_ass
  485.  
  486. local static_ass = assdraw.ass_new()
  487.  
  488.  
  489. if (element.type == "box") then
  490. --draw box
  491. static_ass:draw_start()
  492. ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
  493. element.layout.box.radius, element.layout.box.hexagon)
  494. static_ass:draw_stop()
  495.  
  496. elseif (element.type == "slider") then
  497. --draw static slider parts
  498.  
  499. local r1 = 0
  500. local r2 = 0
  501. local slider_lo = element.layout.slider
  502. -- offset between element outline and drag-area
  503. local foV = slider_lo.border + slider_lo.gap
  504.  
  505. -- calculate positions of min and max points
  506. if (slider_lo.stype ~= "bar") then
  507. r1 = elem_geo.h / 2
  508. element.slider.min.ele_pos = elem_geo.h / 2
  509. element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
  510. if (slider_lo.stype == "diamond") then
  511. r2 = (elem_geo.h - 2 * slider_lo.border) / 2
  512. elseif (slider_lo.stype == "knob") then
  513. r2 = r1
  514. end
  515. else
  516. element.slider.min.ele_pos =
  517. slider_lo.border + slider_lo.gap
  518. element.slider.max.ele_pos =
  519. elem_geo.w - (slider_lo.border + slider_lo.gap)
  520. end
  521.  
  522. element.slider.min.glob_pos =
  523. element.hitbox.x1 + element.slider.min.ele_pos
  524. element.slider.max.glob_pos =
  525. element.hitbox.x1 + element.slider.max.ele_pos
  526.  
  527. -- -- --
  528.  
  529. static_ass:draw_start()
  530.  
  531. -- the box
  532. ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1, slider_lo.stype == "diamond")
  533.  
  534. -- the "hole"
  535. ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border,
  536. elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border,
  537. r2, slider_lo.stype == "diamond")
  538.  
  539. -- marker nibbles
  540. if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
  541. local markers = element.slider.markerF()
  542. for _,marker in pairs(markers) do
  543. if (marker > element.slider.min.value) and
  544. (marker < element.slider.max.value) then
  545.  
  546. local s = get_slider_ele_pos_for(element, marker)
  547.  
  548. if (slider_lo.gap > 1) then -- draw triangles
  549.  
  550. local a = slider_lo.gap / 0.5 --0.866
  551.  
  552. --top
  553. if (slider_lo.nibbles_top) then
  554. static_ass:move_to(s - (a/2), slider_lo.border)
  555. static_ass:line_to(s + (a/2), slider_lo.border)
  556. static_ass:line_to(s, foV)
  557. end
  558.  
  559. --bottom
  560. if (slider_lo.nibbles_bottom) then
  561. static_ass:move_to(s - (a/2),
  562. elem_geo.h - slider_lo.border)
  563. static_ass:line_to(s,
  564. elem_geo.h - foV)
  565. static_ass:line_to(s + (a/2),
  566. elem_geo.h - slider_lo.border)
  567. end
  568.  
  569. else -- draw 2x1px nibbles
  570.  
  571. --top
  572. if (slider_lo.nibbles_top) then
  573. static_ass:rect_cw(s - 1, slider_lo.border,
  574. s + 1, slider_lo.border + slider_lo.gap);
  575. end
  576.  
  577. --bottom
  578. if (slider_lo.nibbles_bottom) then
  579. static_ass:rect_cw(s - 1,
  580. elem_geo.h -slider_lo.border -slider_lo.gap,
  581. s + 1, elem_geo.h - slider_lo.border);
  582. end
  583. end
  584. end
  585. end
  586. end
  587. end
  588.  
  589. element.static_ass = static_ass
  590.  
  591.  
  592. -- if the element is supposed to be disabled,
  593. -- style it accordingly and kill the eventresponders
  594. if not (element.enabled) then
  595. element.layout.alpha[1] = 136
  596. element.eventresponder = nil
  597. end
  598. end
  599. end
  600.  
  601.  
  602. --
  603. -- Element Rendering
  604. --
  605.  
  606. -- returns nil or a chapter element from the native property chapter-list
  607. function get_chapter(possec)
  608. local cl = mp.get_property_native("chapter-list", {})
  609. local ch = nil
  610.  
  611. -- chapters might not be sorted by time. find nearest-before/at possec
  612. for n=1, #cl do
  613. if possec >= cl[n].time and (not ch or cl[n].time > ch.time) then
  614. ch = cl[n]
  615. end
  616. end
  617. return ch
  618. end
  619.  
  620. function render_elements(master_ass)
  621.  
  622. -- when the slider is dragged or hovered and we have a target chapter name
  623. -- then we use it instead of the normal title. we calculate it before the
  624. -- render iterations because the title may be rendered before the slider.
  625. state.forced_title = nil
  626. local se, ae = state.slider_element, elements[state.active_element]
  627. if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then
  628. local dur = mp.get_property_number("duration", 0)
  629. if dur > 0 then
  630. local possec = get_slider_value(se) * dur / 100 -- of mouse pos
  631. local ch = get_chapter(possec)
  632. if ch and ch.title and ch.title ~= "" then
  633. state.forced_title = string.format(user_opts.chapter_fmt, ch.title)
  634. end
  635. end
  636. end
  637.  
  638. for n=1, #elements do
  639. local element = elements[n]
  640.  
  641. local style_ass = assdraw.ass_new()
  642. style_ass:merge(element.style_ass)
  643. ass_append_alpha(style_ass, element.layout.alpha, 0)
  644.  
  645. if element.eventresponder and (state.active_element == n) then
  646.  
  647. -- run render event functions
  648. if not (element.eventresponder.render == nil) then
  649. element.eventresponder.render(element)
  650. end
  651.  
  652. if mouse_hit(element) then
  653. -- mouse down styling
  654. if (element.styledown) then
  655. style_ass:append(osc_styles.elementDown)
  656. end
  657.  
  658. if (element.softrepeat) and (state.mouse_down_counter >= 15
  659. and state.mouse_down_counter % 5 == 0) then
  660.  
  661. element.eventresponder[state.active_event_source.."_down"](element)
  662. end
  663. state.mouse_down_counter = state.mouse_down_counter + 1
  664. end
  665.  
  666. end
  667.  
  668. local elem_ass = assdraw.ass_new()
  669.  
  670. elem_ass:merge(style_ass)
  671.  
  672. if not (element.type == "button") then
  673. elem_ass:merge(element.static_ass)
  674. end
  675.  
  676. if (element.type == "slider") then
  677.  
  678. local slider_lo = element.layout.slider
  679. local elem_geo = element.layout.geometry
  680. local s_min = element.slider.min.value
  681. local s_max = element.slider.max.value
  682.  
  683. -- draw pos marker
  684. local foH, xp
  685. local pos = element.slider.posF()
  686. local foV = slider_lo.border + slider_lo.gap
  687. local innerH = elem_geo.h - (2 * foV)
  688. local seekRanges = element.slider.seekRangesF()
  689. local seekRangeLineHeight = innerH / 5
  690.  
  691. if slider_lo.stype ~= "bar" then
  692. foH = elem_geo.h / 2
  693. else
  694. foH = slider_lo.border + slider_lo.gap
  695. end
  696.  
  697. if pos then
  698. xp = get_slider_ele_pos_for(element, pos)
  699.  
  700. if slider_lo.stype ~= "bar" then
  701. local r = (user_opts.seekbarhandlesize * innerH) / 2
  702. ass_draw_rr_h_cw(elem_ass, xp - r, foH - r,
  703. xp + r, foH + r,
  704. r, slider_lo.stype == "diamond")
  705. else
  706. local h = 0
  707. if seekRanges and user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
  708. h = seekRangeLineHeight
  709. end
  710. elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h)
  711.  
  712. if seekRanges and not user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
  713. -- Punch holes for the seekRanges to be drawn later
  714. for _,range in pairs(seekRanges) do
  715. if range["start"] < pos then
  716. local pstart = get_slider_ele_pos_for(element, range["start"])
  717. local pend = xp
  718.  
  719. if pos > range["end"] then
  720. pend = get_slider_ele_pos_for(element, range["end"])
  721. end
  722. elem_ass:rect_ccw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
  723. end
  724. end
  725. end
  726. end
  727.  
  728. if slider_lo.rtype == "slider" then
  729. ass_draw_rr_h_cw(elem_ass, foH - innerH / 6, foH - innerH / 6,
  730. xp, foH + innerH / 6,
  731. innerH / 6, slider_lo.stype == "diamond", 0)
  732. ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15,
  733. elem_geo.w - foH + innerH / 15, foH + innerH / 15,
  734. 0, slider_lo.stype == "diamond", innerH / 15)
  735. for _,range in pairs(seekRanges or {}) do
  736. local pstart = get_slider_ele_pos_for(element, range["start"])
  737. local pend = get_slider_ele_pos_for(element, range["end"])
  738. ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21,
  739. pend, foH + innerH / 21,
  740. innerH / 21, slider_lo.stype == "diamond")
  741. end
  742. end
  743. end
  744.  
  745. if seekRanges then
  746. if slider_lo.rtype ~= "inverted" then
  747. elem_ass:draw_stop()
  748. elem_ass:merge(element.style_ass)
  749. ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
  750. elem_ass:merge(element.static_ass)
  751. end
  752.  
  753. for _,range in pairs(seekRanges) do
  754. local pstart = get_slider_ele_pos_for(element, range["start"])
  755. local pend = get_slider_ele_pos_for(element, range["end"])
  756.  
  757. if slider_lo.rtype == "slider" then
  758. ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21,
  759. pend, foH + innerH / 21,
  760. innerH / 21, slider_lo.stype == "diamond")
  761. elseif slider_lo.rtype == "line" then
  762. if slider_lo.stype == "bar" then
  763. elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
  764. else
  765. ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8, foH - innerH / 8,
  766. pend + innerH / 8, foH + innerH / 8,
  767. innerH / 8, slider_lo.stype == "diamond")
  768. end
  769. elseif slider_lo.rtype == "bar" then
  770. if slider_lo.stype ~= "bar" then
  771. ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV,
  772. pend + innerH / 2, foV + innerH,
  773. innerH / 2, slider_lo.stype == "diamond")
  774. elseif range["end"] >= (pos or 0) then
  775. elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV)
  776. else
  777. elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
  778. end
  779. elseif slider_lo.rtype == "inverted" then
  780. if slider_lo.stype ~= "bar" then
  781. ass_draw_rr_h_ccw(elem_ass, pstart, (elem_geo.h / 2) - 1, pend,
  782. (elem_geo.h / 2) + 1,
  783. 1, slider_lo.stype == "diamond")
  784. else
  785. elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1, pend, (elem_geo.h / 2) + 1)
  786. end
  787. end
  788. end
  789. end
  790.  
  791. elem_ass:draw_stop()
  792.  
  793. -- add tooltip
  794. if not (element.slider.tooltipF == nil) then
  795.  
  796. if mouse_hit(element) then
  797. local sliderpos = get_slider_value(element)
  798. local tooltiplabel = element.slider.tooltipF(sliderpos)
  799.  
  800. local an = slider_lo.tooltip_an
  801.  
  802. local ty
  803.  
  804. if (an == 2) then
  805. ty = element.hitbox.y1 - slider_lo.border
  806. else
  807. ty = element.hitbox.y1 + elem_geo.h/2
  808. end
  809.  
  810. local tx = get_virt_mouse_pos()
  811. if (slider_lo.adjust_tooltip) then
  812. if (an == 2) then
  813. if (sliderpos < (s_min + 3)) then
  814. an = an - 1
  815. elseif (sliderpos > (s_max - 3)) then
  816. an = an + 1
  817. end
  818. elseif (sliderpos > (s_max-s_min)/2) then
  819. an = an + 1
  820. tx = tx - 5
  821. else
  822. an = an - 1
  823. tx = tx + 10
  824. end
  825. end
  826.  
  827. -- tooltip label
  828. elem_ass:new_event()
  829. elem_ass:pos(tx, ty)
  830. elem_ass:an(an)
  831. elem_ass:append(slider_lo.tooltip_style)
  832. ass_append_alpha(elem_ass, slider_lo.alpha, 0)
  833. elem_ass:append(tooltiplabel)
  834.  
  835. end
  836. end
  837.  
  838. elseif (element.type == "button") then
  839.  
  840. local buttontext
  841. if type(element.content) == "function" then
  842. buttontext = element.content() -- function objects
  843. elseif not (element.content == nil) then
  844. buttontext = element.content -- text objects
  845. end
  846.  
  847. local maxchars = element.layout.button.maxchars
  848. if not (maxchars == nil) and (#buttontext > maxchars) then
  849. local max_ratio = 1.25 -- up to 25% more chars while shrinking
  850. local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
  851. if (#buttontext > limit) then
  852. while (#buttontext > limit) do
  853. buttontext = buttontext:gsub(".[\128-\191]*$", "")
  854. end
  855. buttontext = buttontext .. "..."
  856. end
  857. local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
  858. local stretch = (maxchars/#buttontext)*100
  859. buttontext = string.format("{\\fscx%f}",
  860. (maxchars/#buttontext)*100) .. buttontext
  861. end
  862.  
  863. elem_ass:append(buttontext)
  864. end
  865.  
  866. master_ass:merge(elem_ass)
  867. end
  868. end
  869.  
  870. --
  871. -- Message display
  872. --
  873.  
  874. -- pos is 1 based
  875. function limited_list(prop, pos)
  876. local proplist = mp.get_property_native(prop, {})
  877. local count = #proplist
  878. if count == 0 then
  879. return count, proplist
  880. end
  881.  
  882. local fs = tonumber(mp.get_property('options/osd-font-size'))
  883. local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
  884. if max % 2 == 0 then
  885. max = max - 1
  886. end
  887. local delta = math.ceil(max / 2) - 1
  888. local begi = math.max(math.min(pos - delta, count - max + 1), 1)
  889. local endi = math.min(begi + max - 1, count)
  890.  
  891. local reslist = {}
  892. for i=begi, endi do
  893. local item = proplist[i]
  894. item.current = (i == pos) and true or nil
  895. table.insert(reslist, item)
  896. end
  897. return count, reslist
  898. end
  899.  
  900. function get_playlist()
  901. local pos = mp.get_property_number('playlist-pos', 0) + 1
  902. local count, limlist = limited_list('playlist', pos)
  903. if count == 0 then
  904. return 'Empty playlist.'
  905. end
  906.  
  907. local message = string.format('Playlist [%d/%d]:\n', pos, count)
  908. for i, v in ipairs(limlist) do
  909. local title = v.title
  910. local _, filename = utils.split_path(v.filename)
  911. if title == nil then
  912. title = filename
  913. end
  914. message = string.format('%s %s %s\n', message,
  915. (v.current and '●' or '○'), title)
  916. end
  917. return message
  918. end
  919.  
  920. function get_chapterlist()
  921. local pos = mp.get_property_number('chapter', 0) + 1
  922. local count, limlist = limited_list('chapter-list', pos)
  923. if count == 0 then
  924. return 'No chapters.'
  925. end
  926.  
  927. local message = string.format('Chapters [%d/%d]:\n', pos, count)
  928. for i, v in ipairs(limlist) do
  929. local time = mp.format_time(v.time)
  930. local title = v.title
  931. if title == nil then
  932. title = string.format('Chapter %02d', i)
  933. end
  934. message = string.format('%s[%s] %s %s\n', message, time,
  935. (v.current and '●' or '○'), title)
  936. end
  937. return message
  938. end
  939.  
  940. function show_message(text, duration)
  941.  
  942. --print("text: "..text.." duration: " .. duration)
  943. if duration == nil then
  944. duration = tonumber(mp.get_property("options/osd-duration")) / 1000
  945. elseif not type(duration) == "number" then
  946. print("duration: " .. duration)
  947. end
  948.  
  949. -- cut the text short, otherwise the following functions
  950. -- may slow down massively on huge input
  951. text = string.sub(text, 0, 4000)
  952.  
  953. -- replace actual linebreaks with ASS linebreaks
  954. text = string.gsub(text, "\n", "\\N")
  955.  
  956. state.message_text = text
  957.  
  958. if not state.message_hide_timer then
  959. state.message_hide_timer = mp.add_timeout(0, request_tick)
  960. end
  961. state.message_hide_timer:kill()
  962. state.message_hide_timer.timeout = duration
  963. state.message_hide_timer:resume()
  964. request_tick()
  965. end
  966.  
  967. function render_message(ass)
  968. if state.message_hide_timer and state.message_hide_timer:is_enabled() and
  969. state.message_text
  970. then
  971. local _, lines = string.gsub(state.message_text, "\\N", "")
  972.  
  973. local fontsize = tonumber(mp.get_property("options/osd-font-size"))
  974. local outline = tonumber(mp.get_property("options/osd-border-size"))
  975. local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
  976. local counterscale = osc_param.playresy / osc_param.unscaled_y
  977.  
  978. fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
  979. outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
  980.  
  981. local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
  982.  
  983.  
  984. ass:new_event()
  985. ass:append(style .. state.message_text)
  986. else
  987. state.message_text = nil
  988. end
  989. end
  990.  
  991. --
  992. -- Initialisation and Layout
  993. --
  994.  
  995. function new_element(name, type)
  996. elements[name] = {}
  997. elements[name].type = type
  998.  
  999. -- add default stuff
  1000. elements[name].eventresponder = {}
  1001. elements[name].visible = true
  1002. elements[name].enabled = true
  1003. elements[name].softrepeat = false
  1004. elements[name].styledown = (type == "button")
  1005. elements[name].state = {}
  1006.  
  1007. if (type == "slider") then
  1008. elements[name].slider = {min = {value = 0}, max = {value = 100}}
  1009. end
  1010.  
  1011.  
  1012. return elements[name]
  1013. end
  1014.  
  1015. function add_layout(name)
  1016. if not (elements[name] == nil) then
  1017. -- new layout
  1018. elements[name].layout = {}
  1019.  
  1020. -- set layout defaults
  1021. elements[name].layout.layer = 50
  1022. elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
  1023.  
  1024. if (elements[name].type == "button") then
  1025. elements[name].layout.button = {
  1026. maxchars = nil,
  1027. }
  1028. elseif (elements[name].type == "slider") then
  1029. -- slider defaults
  1030. elements[name].layout.slider = {
  1031. border = 1,
  1032. gap = 1,
  1033. nibbles_top = true,
  1034. nibbles_bottom = true,
  1035. stype = "slider",
  1036. adjust_tooltip = true,
  1037. tooltip_style = "",
  1038. tooltip_an = 2,
  1039. alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
  1040. }
  1041. elseif (elements[name].type == "box") then
  1042. elements[name].layout.box = {radius = 0, hexagon = false}
  1043. end
  1044.  
  1045. return elements[name].layout
  1046. else
  1047. msg.error("Can't add_layout to element \""..name.."\", doesn't exist.")
  1048. end
  1049. end
  1050.  
  1051. -- Window Controls
  1052. function window_controls(topbar)
  1053. local wc_geo = {
  1054. x = 0,
  1055. y = 30 + user_opts.barmargin,
  1056. an = 1,
  1057. w = osc_param.playresx,
  1058. h = 30,
  1059. }
  1060.  
  1061. local alignment = window_controls_alignment()
  1062. local controlbox_w = window_control_box_width
  1063. local titlebox_w = wc_geo.w - controlbox_w
  1064.  
  1065. -- Default alignment is "right"
  1066. local controlbox_left = wc_geo.w - controlbox_w
  1067. local titlebox_left = wc_geo.x
  1068. local titlebox_right = wc_geo.w - controlbox_w
  1069.  
  1070. if alignment == "left" then
  1071. controlbox_left = wc_geo.x
  1072. titlebox_left = wc_geo.x + controlbox_w
  1073. titlebox_right = wc_geo.w
  1074. end
  1075.  
  1076. add_area("window-controls",
  1077. get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
  1078. controlbox_w, wc_geo.h))
  1079.  
  1080. local lo
  1081.  
  1082. -- Background Bar
  1083. new_element("wcbar", "box")
  1084. lo = add_layout("wcbar")
  1085. lo.geometry = wc_geo
  1086. lo.layer = 10
  1087. lo.style = osc_styles.wcBar
  1088. lo.alpha[1] = user_opts.boxalpha
  1089.  
  1090. local button_y = wc_geo.y - (wc_geo.h / 2)
  1091. local first_geo =
  1092. {x = controlbox_left + 5, y = button_y, an = 4, w = 25, h = 25}
  1093. local second_geo =
  1094. {x = controlbox_left + 30, y = button_y, an = 4, w = 25, h = 25}
  1095. local third_geo =
  1096. {x = controlbox_left + 55, y = button_y, an = 4, w = 25, h = 25}
  1097.  
  1098. -- Window control buttons use symbols in the custom mpv osd font
  1099. -- because the official unicode codepoints are sufficiently
  1100. -- exotic that a system might lack an installed font with them,
  1101. -- and libass will complain that they are not present in the
  1102. -- default font, even if another font with them is available.
  1103.  
  1104. -- Close: 🗙
  1105. ne = new_element("close", "button")
  1106. ne.content = "\238\132\149"
  1107. ne.eventresponder["mbtn_left_up"] =
  1108. function () mp.commandv("quit") end
  1109. lo = add_layout("close")
  1110. lo.geometry = alignment == "left" and first_geo or third_geo
  1111. lo.style = osc_styles.wcButtons
  1112.  
  1113. -- Minimize: 🗕
  1114. ne = new_element("minimize", "button")
  1115. ne.content = "\238\132\146"
  1116. ne.eventresponder["mbtn_left_up"] =
  1117. function () mp.commandv("cycle", "window-minimized") end
  1118. lo = add_layout("minimize")
  1119. lo.geometry = alignment == "left" and second_geo or first_geo
  1120. lo.style = osc_styles.wcButtons
  1121.  
  1122. -- Maximize: 🗖 /🗗
  1123. ne = new_element("maximize", "button")
  1124. if state.maximized or state.fullscreen then
  1125. ne.content = "\238\132\148"
  1126. else
  1127. ne.content = "\238\132\147"
  1128. end
  1129. ne.eventresponder["mbtn_left_up"] =
  1130. function ()
  1131. if state.fullscreen then
  1132. mp.commandv("cycle", "fullscreen")
  1133. else
  1134. mp.commandv("cycle", "window-maximized")
  1135. end
  1136. end
  1137. lo = add_layout("maximize")
  1138. lo.geometry = alignment == "left" and third_geo or second_geo
  1139. lo.style = osc_styles.wcButtons
  1140.  
  1141. -- deadzone below window controls
  1142. local sh_area_y0, sh_area_y1
  1143. sh_area_y0 = user_opts.barmargin
  1144. sh_area_y1 = (wc_geo.y + (wc_geo.h / 2)) +
  1145. get_align(1 - (2 * user_opts.deadzonesize),
  1146. osc_param.playresy - (wc_geo.y + (wc_geo.h / 2)), 0, 0)
  1147. add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1)
  1148.  
  1149. if topbar then
  1150. -- The title is already there as part of the top bar
  1151. return
  1152. else
  1153. -- Apply boxvideo margins to the control bar
  1154. osc_param.video_margins.t = wc_geo.h / osc_param.playresy
  1155. end
  1156.  
  1157. -- Window Title
  1158. ne = new_element("wctitle", "button")
  1159. ne.content = function ()
  1160. local title = mp.command_native({"expand-text", user_opts.title})
  1161. -- escape ASS, and strip newlines and trailing slashes
  1162. title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
  1163. return not (title == "") and title or "mpv"
  1164. end
  1165. local left_pad = 5
  1166. local right_pad = 10
  1167. lo = add_layout("wctitle")
  1168. lo.geometry =
  1169. { x = titlebox_left + left_pad, y = wc_geo.y - 3, an = 1,
  1170. w = titlebox_w, h = wc_geo.h }
  1171. lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
  1172. osc_styles.wcTitle,
  1173. titlebox_left + left_pad, wc_geo.y - wc_geo.h,
  1174. titlebox_right - right_pad , wc_geo.y + wc_geo.h)
  1175.  
  1176. add_area("window-controls-title",
  1177. titlebox_left, 0, titlebox_right, wc_geo.h)
  1178. end
  1179.  
  1180. --
  1181. -- Layouts
  1182. --
  1183.  
  1184. local layouts = {}
  1185.  
  1186. -- Classic box layout
  1187. layouts["box"] = function ()
  1188.  
  1189. local osc_geo = {
  1190. w = 550, -- width
  1191. h = 138, -- height
  1192. r = 10, -- corner-radius
  1193. p = 15, -- padding
  1194. }
  1195.  
  1196. -- make sure the OSC actually fits into the video
  1197. if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then
  1198. osc_param.playresy = (osc_geo.w+(2*osc_geo.p))/osc_param.display_aspect
  1199. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1200. end
  1201.  
  1202. -- position of the controller according to video aspect and valignment
  1203. local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
  1204. osc_geo.w, 0))
  1205. local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
  1206. osc_geo.h, 0))
  1207.  
  1208. -- position offset for contents aligned at the borders of the box
  1209. local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2
  1210. local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2
  1211.  
  1212. osc_param.areas = {} -- delete areas
  1213.  
  1214. -- area for active mouse input
  1215. add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
  1216.  
  1217. -- area for show/hide
  1218. local sh_area_y0, sh_area_y1
  1219. if user_opts.valign > 0 then
  1220. -- deadzone above OSC
  1221. sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
  1222. posY - (osc_geo.h / 2), 0, 0)
  1223. sh_area_y1 = osc_param.playresy
  1224. else
  1225. -- deadzone below OSC
  1226. sh_area_y0 = 0
  1227. sh_area_y1 = (posY + (osc_geo.h / 2)) +
  1228. get_align(1 - (2*user_opts.deadzonesize),
  1229. osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
  1230. end
  1231. add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  1232.  
  1233. -- fetch values
  1234. local osc_w, osc_h, osc_r, osc_p =
  1235. osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p
  1236.  
  1237. local lo
  1238.  
  1239. --
  1240. -- Background box
  1241. --
  1242.  
  1243. new_element("bgbox", "box")
  1244. lo = add_layout("bgbox")
  1245.  
  1246. lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
  1247. lo.layer = 10
  1248. lo.style = osc_styles.box
  1249. lo.alpha[1] = user_opts.boxalpha
  1250. lo.alpha[3] = user_opts.boxalpha
  1251. lo.box.radius = osc_r
  1252.  
  1253. --
  1254. -- Title row
  1255. --
  1256.  
  1257. local titlerowY = posY - pos_offsetY - 10
  1258.  
  1259. lo = add_layout("title")
  1260. lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
  1261. lo.style = osc_styles.vidtitle
  1262. lo.button.maxchars = user_opts.boxmaxchars
  1263.  
  1264. lo = add_layout("pl_prev")
  1265. lo.geometry =
  1266. {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12}
  1267. lo.style = osc_styles.topButtons
  1268.  
  1269. lo = add_layout("pl_next")
  1270. lo.geometry =
  1271. {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12}
  1272. lo.style = osc_styles.topButtons
  1273.  
  1274. --
  1275. -- Big buttons
  1276. --
  1277.  
  1278. local bigbtnrowY = posY - pos_offsetY + 35
  1279. local bigbtndist = 60
  1280.  
  1281. lo = add_layout("playpause")
  1282. lo.geometry =
  1283. {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
  1284. lo.style = osc_styles.bigButtons
  1285.  
  1286. lo = add_layout("skipback")
  1287. lo.geometry =
  1288. {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
  1289. lo.style = osc_styles.bigButtons
  1290.  
  1291. lo = add_layout("skipfrwd")
  1292. lo.geometry =
  1293. {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
  1294. lo.style = osc_styles.bigButtons
  1295.  
  1296. lo = add_layout("ch_prev")
  1297. lo.geometry =
  1298. {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
  1299. lo.style = osc_styles.bigButtons
  1300.  
  1301. lo = add_layout("ch_next")
  1302. lo.geometry =
  1303. {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
  1304. lo.style = osc_styles.bigButtons
  1305.  
  1306. lo = add_layout("cy_audio")
  1307. lo.geometry =
  1308. {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18}
  1309. lo.style = osc_styles.smallButtonsL
  1310.  
  1311. lo = add_layout("cy_sub")
  1312. lo.geometry =
  1313. {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18}
  1314. lo.style = osc_styles.smallButtonsL
  1315.  
  1316. lo = add_layout("tog_fs")
  1317. lo.geometry =
  1318. {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25}
  1319. lo.style = osc_styles.smallButtonsR
  1320.  
  1321. lo = add_layout("volume")
  1322. lo.geometry =
  1323. {x = posX+pos_offsetX - (25 * 2) - osc_geo.p,
  1324. y = bigbtnrowY, an = 4, w = 25, h = 25}
  1325. lo.style = osc_styles.smallButtonsR
  1326.  
  1327. --
  1328. -- Seekbar
  1329. --
  1330.  
  1331. lo = add_layout("seekbar")
  1332. lo.geometry =
  1333. {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15}
  1334. lo.style = osc_styles.timecodes
  1335. lo.slider.tooltip_style = osc_styles.vidtitle
  1336. lo.slider.stype = user_opts["seekbarstyle"]
  1337. lo.slider.rtype = user_opts["seekrangestyle"]
  1338.  
  1339. --
  1340. -- Timecodes + Cache
  1341. --
  1342.  
  1343. local bottomrowY = posY + pos_offsetY - 5
  1344.  
  1345. lo = add_layout("tc_left")
  1346. lo.geometry =
  1347. {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18}
  1348. lo.style = osc_styles.timecodes
  1349.  
  1350. lo = add_layout("tc_right")
  1351. lo.geometry =
  1352. {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18}
  1353. lo.style = osc_styles.timecodes
  1354.  
  1355. lo = add_layout("cache")
  1356. lo.geometry =
  1357. {x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
  1358. lo.style = osc_styles.timecodes
  1359.  
  1360. end
  1361.  
  1362. -- slim box layout
  1363. layouts["slimbox"] = function ()
  1364.  
  1365. local osc_geo = {
  1366. w = 660, -- width
  1367. h = 70, -- height
  1368. r = 10, -- corner-radius
  1369.  
  1370. ew = 80, -- width of p/p button
  1371. eh = 80, -- height of p/p button
  1372. }
  1373.  
  1374. -- make sure the OSC actually fits into the video
  1375. if (osc_param.playresx < (osc_geo.w)) then
  1376. osc_param.playresy = (osc_geo.w)/osc_param.display_aspect
  1377. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1378. end
  1379.  
  1380. -- position of the controller according to video aspect and valignment
  1381. local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
  1382. osc_geo.w, 0))
  1383. local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
  1384. osc_geo.h, 0))
  1385.  
  1386. local eposX = math.floor(get_align(0, osc_param.playresx, osc_geo.ew, 0))
  1387. local eposY = math.floor(get_align(0, osc_param.playresy, osc_geo.eh, 0))
  1388.  
  1389. osc_param.areas = {} -- delete areas
  1390.  
  1391. -- area for active mouse input
  1392. --add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
  1393. --add_area("input", get_hitbox_coords(eposX, eposY, 5, osc_geo.ew, osc_geo.eh))
  1394. add_area("input", get_hitbox_coords(posX, eposY, 5, osc_geo.w, posY))
  1395. --show_message(posY)
  1396.  
  1397. -- area for show/hide
  1398. local sh_area_y0, sh_area_y1
  1399. if user_opts.valign > 0 then
  1400. -- deadzone above OSC
  1401. sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
  1402. posY - (osc_geo.h / 2), 0, 0)
  1403. sh_area_y1 = osc_param.playresy
  1404. else
  1405. -- deadzone below OSC
  1406. sh_area_y0 = 0
  1407. sh_area_y1 = (posY + (osc_geo.h / 2)) +
  1408. get_align(1 - (2*user_opts.deadzonesize),
  1409. osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
  1410. end
  1411. add_area("showhide", posX - 300, sh_area_y0, posX + 300, sh_area_y1)
  1412.  
  1413. local lo
  1414.  
  1415. local tc_w, ele_h, inner_w = 100, 30, osc_geo.w - 100
  1416.  
  1417. -- styles
  1418. local styles = {
  1419. box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
  1420. timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
  1421. tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}",
  1422. }
  1423.  
  1424.  
  1425. new_element("bgbox", "box")
  1426. lo = add_layout("bgbox")
  1427.  
  1428. lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
  1429. lo.layer = 10
  1430. lo.style = osc_styles.box
  1431. lo.alpha[1] = user_opts.boxalpha
  1432. lo.alpha[3] = 0
  1433. if not (user_opts["seekbarstyle"] == "bar") then
  1434. lo.box.radius = osc_geo.r
  1435. lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
  1436. end
  1437.  
  1438.  
  1439. lo = add_layout("seekbar")
  1440. lo.geometry =
  1441. {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
  1442. lo.style = osc_styles.timecodes
  1443. lo.slider.border = 0
  1444. lo.slider.gap = 1.5
  1445. lo.slider.tooltip_style = styles.tooltip
  1446. lo.slider.stype = user_opts["seekbarstyle"]
  1447. lo.slider.rtype = user_opts["seekrangestyle"]
  1448. lo.slider.adjust_tooltip = false
  1449.  
  1450.  
  1451. lo = add_layout("playpause")
  1452. --lo.geometry = {x = posX, y = posY - 100, an = 5, w = 80, h = 80}
  1453. lo.geometry = {x = eposX, y = eposY, an = 5, w = osc_geo.ew, h = osc_geo.eh}
  1454. lo.style = osc_styles.bigButtons
  1455.  
  1456. --
  1457. -- Timecodes
  1458. --
  1459.  
  1460. lo = add_layout("tc_left")
  1461. lo.geometry =
  1462. {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1,
  1463. an = 7, w = tc_w, h = ele_h}
  1464. lo.style = styles.timecodes
  1465. lo.alpha[3] = user_opts.boxalpha
  1466.  
  1467. lo = add_layout("tc_right")
  1468. lo.geometry =
  1469. {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1,
  1470. an = 9, w = tc_w, h = ele_h}
  1471. lo.style = styles.timecodes
  1472. lo.alpha[3] = user_opts.boxalpha
  1473.  
  1474. -- Cache
  1475.  
  1476. lo = add_layout("cache")
  1477. lo.geometry =
  1478. {x = posX, y = posY + 1,
  1479. an = 8, w = tc_w, h = ele_h}
  1480. lo.style = styles.timecodes
  1481. lo.alpha[3] = user_opts.boxalpha
  1482.  
  1483.  
  1484. end
  1485.  
  1486. function bar_layout(direction)
  1487. local osc_geo = {
  1488. x = -2,
  1489. y,
  1490. an = (direction < 0) and 7 or 1,
  1491. w,
  1492. h = 56,
  1493. }
  1494.  
  1495. local padX = 9
  1496. local padY = 3
  1497. local buttonW = 27
  1498. local tcW = (state.tc_ms) and 170 or 110
  1499. local tsW = 90
  1500. local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
  1501.  
  1502. -- Special topbar handling when window controls are present
  1503. local padwc_l
  1504. local padwc_r
  1505. if direction < 0 or not window_controls_enabled() then
  1506. padwc_l = 0
  1507. padwc_r = 0
  1508. elseif window_controls_alignment() == "left" then
  1509. padwc_l = window_control_box_width
  1510. padwc_r = 0
  1511. else
  1512. padwc_l = 0
  1513. padwc_r = window_control_box_width
  1514. end
  1515.  
  1516. if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
  1517. osc_param.playresy = minW / osc_param.display_aspect
  1518. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1519. end
  1520.  
  1521. osc_geo.y = direction * (54 + user_opts.barmargin)
  1522. osc_geo.w = osc_param.playresx + 4
  1523. if direction < 0 then
  1524. osc_geo.y = osc_geo.y + osc_param.playresy
  1525. end
  1526.  
  1527. local line1 = osc_geo.y - direction * (9 + padY)
  1528. local line2 = osc_geo.y - direction * (36 + padY)
  1529.  
  1530. osc_param.areas = {}
  1531.  
  1532. add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
  1533. osc_geo.w, osc_geo.h))
  1534.  
  1535. local sh_area_y0, sh_area_y1
  1536. if direction > 0 then
  1537. -- deadzone below OSC
  1538. sh_area_y0 = user_opts.barmargin
  1539. sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) +
  1540. get_align(1 - (2*user_opts.deadzonesize),
  1541. osc_param.playresy - (osc_geo.y + (osc_geo.h / 2)), 0, 0)
  1542. else
  1543. -- deadzone above OSC
  1544. sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
  1545. osc_geo.y - (osc_geo.h / 2), 0, 0)
  1546. sh_area_y1 = osc_param.playresy - user_opts.barmargin
  1547. end
  1548. add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  1549.  
  1550. local lo, geo
  1551.  
  1552. -- Background bar
  1553. new_element("bgbox", "box")
  1554. lo = add_layout("bgbox")
  1555.  
  1556. lo.geometry = osc_geo
  1557. lo.layer = 10
  1558. lo.style = osc_styles.box
  1559. lo.alpha[1] = user_opts.boxalpha
  1560.  
  1561.  
  1562. -- Playlist prev/next
  1563. geo = { x = osc_geo.x + padX, y = line1,
  1564. an = 4, w = 18, h = 18 - padY }
  1565. lo = add_layout("pl_prev")
  1566. lo.geometry = geo
  1567. lo.style = osc_styles.topButtonsBar
  1568.  
  1569. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1570. lo = add_layout("pl_next")
  1571. lo.geometry = geo
  1572. lo.style = osc_styles.topButtonsBar
  1573.  
  1574. local t_l = geo.x + geo.w + padX
  1575.  
  1576. -- Cache
  1577. geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
  1578. an = 6, w = 150, h = geo.h }
  1579. lo = add_layout("cache")
  1580. lo.geometry = geo
  1581. lo.style = osc_styles.vidtitleBar
  1582.  
  1583. local t_r = geo.x - geo.w - padX*2
  1584.  
  1585. -- Title
  1586. geo = { x = t_l, y = geo.y, an = 4,
  1587. w = t_r - t_l, h = geo.h }
  1588. lo = add_layout("title")
  1589. lo.geometry = geo
  1590. lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
  1591. osc_styles.vidtitleBar,
  1592. geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
  1593.  
  1594.  
  1595. -- Playback control buttons
  1596. geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4,
  1597. w = buttonW, h = 36 - padY*2}
  1598. lo = add_layout("playpause")
  1599. lo.geometry = geo
  1600. lo.style = osc_styles.smallButtonsBar
  1601.  
  1602. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1603. lo = add_layout("ch_prev")
  1604. lo.geometry = geo
  1605. lo.style = osc_styles.smallButtonsBar
  1606.  
  1607. geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1608. lo = add_layout("ch_next")
  1609. lo.geometry = geo
  1610. lo.style = osc_styles.smallButtonsBar
  1611.  
  1612. -- Left timecode
  1613. geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
  1614. w = tcW, h = geo.h }
  1615. lo = add_layout("tc_left")
  1616. lo.geometry = geo
  1617. lo.style = osc_styles.timecodesBar
  1618.  
  1619. local sb_l = geo.x + padX
  1620.  
  1621. -- Fullscreen button
  1622. geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4,
  1623. w = buttonW, h = geo.h }
  1624. lo = add_layout("tog_fs")
  1625. lo.geometry = geo
  1626. lo.style = osc_styles.smallButtonsBar
  1627.  
  1628. -- Volume
  1629. geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1630. lo = add_layout("volume")
  1631. lo.geometry = geo
  1632. lo.style = osc_styles.smallButtonsBar
  1633.  
  1634. -- Track selection buttons
  1635. geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
  1636. lo = add_layout("cy_sub")
  1637. lo.geometry = geo
  1638. lo.style = osc_styles.smallButtonsBar
  1639.  
  1640. geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1641. lo = add_layout("cy_audio")
  1642. lo.geometry = geo
  1643. lo.style = osc_styles.smallButtonsBar
  1644.  
  1645.  
  1646. -- Right timecode
  1647. geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an,
  1648. w = tcW, h = geo.h }
  1649. lo = add_layout("tc_right")
  1650. lo.geometry = geo
  1651. lo.style = osc_styles.timecodesBar
  1652.  
  1653. local sb_r = geo.x - padX
  1654.  
  1655.  
  1656. -- Seekbar
  1657. geo = { x = sb_l, y = geo.y, an = geo.an,
  1658. w = math.max(0, sb_r - sb_l), h = geo.h }
  1659. new_element("bgbar1", "box")
  1660. lo = add_layout("bgbar1")
  1661.  
  1662. lo.geometry = geo
  1663. lo.layer = 15
  1664. lo.style = osc_styles.timecodesBar
  1665. lo.alpha[1] =
  1666. math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
  1667. if not (user_opts["seekbarstyle"] == "bar") then
  1668. lo.box.radius = geo.h / 2
  1669. lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
  1670. end
  1671.  
  1672. lo = add_layout("seekbar")
  1673. lo.geometry = geo
  1674. lo.style = osc_styles.timecodesBar
  1675. lo.slider.border = 0
  1676. lo.slider.gap = 2
  1677. lo.slider.tooltip_style = osc_styles.timePosBar
  1678. lo.slider.tooltip_an = 5
  1679. lo.slider.stype = user_opts["seekbarstyle"]
  1680. lo.slider.rtype = user_opts["seekrangestyle"]
  1681.  
  1682. if direction < 0 then
  1683. osc_param.video_margins.b = osc_geo.h / osc_param.playresy
  1684. else
  1685. osc_param.video_margins.t = osc_geo.h / osc_param.playresy
  1686. end
  1687. end
  1688.  
  1689. layouts["bottombar"] = function()
  1690. bar_layout(-1)
  1691. end
  1692.  
  1693. layouts["topbar"] = function()
  1694. bar_layout(1)
  1695. end
  1696.  
  1697. -- Validate string type user options
  1698. function validate_user_opts()
  1699. if layouts[user_opts.layout] == nil then
  1700. msg.warn("Invalid setting \""..user_opts.layout.."\" for layout")
  1701. user_opts.layout = "bottombar"
  1702. end
  1703.  
  1704. if user_opts.seekbarstyle ~= "bar" and
  1705. user_opts.seekbarstyle ~= "diamond" and
  1706. user_opts.seekbarstyle ~= "knob" then
  1707. msg.warn("Invalid setting \"" .. user_opts.seekbarstyle
  1708. .. "\" for seekbarstyle")
  1709. user_opts.seekbarstyle = "bar"
  1710. end
  1711.  
  1712. if user_opts.seekrangestyle ~= "bar" and
  1713. user_opts.seekrangestyle ~= "line" and
  1714. user_opts.seekrangestyle ~= "slider" and
  1715. user_opts.seekrangestyle ~= "inverted" and
  1716. user_opts.seekrangestyle ~= "none" then
  1717. msg.warn("Invalid setting \"" .. user_opts.seekrangestyle
  1718. .. "\" for seekrangestyle")
  1719. user_opts.seekrangestyle = "inverted"
  1720. end
  1721.  
  1722. if user_opts.seekrangestyle == "slider" and
  1723. user_opts.seekbarstyle == "bar" then
  1724. msg.warn("Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported")
  1725. user_opts.seekrangestyle = "inverted"
  1726. end
  1727.  
  1728. if user_opts.windowcontrols ~= "auto" and
  1729. user_opts.windowcontrols ~= "yes" and
  1730. user_opts.windowcontrols ~= "no" then
  1731. msg.warn("windowcontrols cannot be \"" ..
  1732. user_opts.windowcontrols .. "\". Ignoring.")
  1733. user_opts.windowcontrols = "auto"
  1734. end
  1735. if user_opts.windowcontrols_alignment ~= "right" and
  1736. user_opts.windowcontrols_alignment ~= "left" then
  1737. msg.warn("windowcontrols_alignment cannot be \"" ..
  1738. user_opts.windowcontrols_alignment .. "\". Ignoring.")
  1739. user_opts.windowcontrols_alignment = "right"
  1740. end
  1741. end
  1742.  
  1743. function update_options(list)
  1744. validate_user_opts()
  1745. request_tick()
  1746. visibility_mode(user_opts.visibility, true)
  1747. update_duration_watch()
  1748. request_init()
  1749. end
  1750.  
  1751. -- OSC INIT
  1752. function osc_init()
  1753. msg.debug("osc_init")
  1754.  
  1755. -- set canvas resolution according to display aspect and scaling setting
  1756. local baseResY = 400
  1757. local display_w, display_h, display_aspect = mp.get_osd_size()
  1758. local scale = 1
  1759.  
  1760. if (mp.get_property("video") == "no") then -- dummy/forced window
  1761. scale = user_opts.scaleforcedwindow
  1762. elseif state.fullscreen then
  1763. scale = user_opts.scalefullscreen
  1764. else
  1765. scale = user_opts.scalewindowed
  1766. end
  1767.  
  1768. if user_opts.vidscale then
  1769. osc_param.unscaled_y = baseResY
  1770. else
  1771. osc_param.unscaled_y = display_h
  1772. end
  1773. osc_param.playresy = osc_param.unscaled_y / scale
  1774. if (display_aspect > 0) then
  1775. osc_param.display_aspect = display_aspect
  1776. end
  1777. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1778.  
  1779. -- stop seeking with the slider to prevent skipping files
  1780. state.active_element = nil
  1781.  
  1782. osc_param.video_margins = {l = 0, r = 0, t = 0, b = 0}
  1783.  
  1784. elements = {}
  1785.  
  1786. -- some often needed stuff
  1787. local pl_count = mp.get_property_number("playlist-count", 0)
  1788. local have_pl = (pl_count > 1)
  1789. local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
  1790. local have_ch = (mp.get_property_number("chapters", 0) > 0)
  1791. local loop = mp.get_property("loop-playlist", "no")
  1792.  
  1793. local ne
  1794.  
  1795. -- title
  1796. ne = new_element("title", "button")
  1797.  
  1798. ne.content = function ()
  1799. local title = state.forced_title or
  1800. mp.command_native({"expand-text", user_opts.title})
  1801. -- escape ASS, and strip newlines and trailing slashes
  1802. title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
  1803. return not (title == "") and title or "mpv"
  1804. end
  1805.  
  1806. ne.eventresponder["mbtn_left_up"] = function ()
  1807. local title = mp.get_property_osd("media-title")
  1808. if (have_pl) then
  1809. title = string.format("[%d/%d] %s", countone(pl_pos - 1),
  1810. pl_count, title)
  1811. end
  1812. show_message(title)
  1813. end
  1814.  
  1815. ne.eventresponder["mbtn_right_up"] =
  1816. function () show_message(mp.get_property_osd("filename")) end
  1817.  
  1818. -- playlist buttons
  1819.  
  1820. -- prev
  1821. ne = new_element("pl_prev", "button")
  1822.  
  1823. ne.content = "\238\132\144"
  1824. ne.enabled = (pl_pos > 1) or (loop ~= "no")
  1825. ne.eventresponder["mbtn_left_up"] =
  1826. function ()
  1827. mp.commandv("playlist-prev", "weak")
  1828. if user_opts.playlist_osd then
  1829. show_message(get_playlist(), 3)
  1830. end
  1831. end
  1832. ne.eventresponder["shift+mbtn_left_up"] =
  1833. function () show_message(get_playlist(), 3) end
  1834. ne.eventresponder["mbtn_right_up"] =
  1835. function () show_message(get_playlist(), 3) end
  1836.  
  1837. --next
  1838. ne = new_element("pl_next", "button")
  1839.  
  1840. ne.content = "\238\132\129"
  1841. ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
  1842. ne.eventresponder["mbtn_left_up"] =
  1843. function ()
  1844. mp.commandv("playlist-next", "weak")
  1845. if user_opts.playlist_osd then
  1846. show_message(get_playlist(), 3)
  1847. end
  1848. end
  1849. ne.eventresponder["shift+mbtn_left_up"] =
  1850. function () show_message(get_playlist(), 3) end
  1851. ne.eventresponder["mbtn_right_up"] =
  1852. function () show_message(get_playlist(), 3) end
  1853.  
  1854.  
  1855. -- big buttons
  1856.  
  1857. --playpause
  1858. ne = new_element("playpause", "button")
  1859.  
  1860. ne.content = function ()
  1861. if mp.get_property("pause") == "yes" then
  1862. return ("\238\132\129")
  1863. else
  1864. return ("\238\128\130")
  1865. end
  1866. end
  1867. ne.eventresponder["mbtn_left_up"] =
  1868. function () mp.commandv("cycle", "pause") end
  1869.  
  1870. --skipback
  1871. ne = new_element("skipback", "button")
  1872.  
  1873. ne.softrepeat = true
  1874. ne.content = "\238\128\132"
  1875. ne.eventresponder["mbtn_left_down"] =
  1876. function () mp.commandv("seek", -5, "relative", "keyframes") end
  1877. ne.eventresponder["shift+mbtn_left_down"] =
  1878. function () mp.commandv("frame-back-step") end
  1879. ne.eventresponder["mbtn_right_down"] =
  1880. function () mp.commandv("seek", -30, "relative", "keyframes") end
  1881.  
  1882. --skipfrwd
  1883. ne = new_element("skipfrwd", "button")
  1884.  
  1885. ne.softrepeat = true
  1886. ne.content = "\238\128\133"
  1887. ne.eventresponder["mbtn_left_down"] =
  1888. function () mp.commandv("seek", 10, "relative", "keyframes") end
  1889. ne.eventresponder["shift+mbtn_left_down"] =
  1890. function () mp.commandv("frame-step") end
  1891. ne.eventresponder["mbtn_right_down"] =
  1892. function () mp.commandv("seek", 60, "relative", "keyframes") end
  1893.  
  1894. --ch_prev
  1895. ne = new_element("ch_prev", "button")
  1896.  
  1897. ne.enabled = have_ch
  1898. ne.content = "\238\132\132"
  1899. ne.eventresponder["mbtn_left_up"] =
  1900. function ()
  1901. mp.commandv("add", "chapter", -1)
  1902. if user_opts.chapters_osd then
  1903. show_message(get_chapterlist(), 3)
  1904. end
  1905. end
  1906. ne.eventresponder["shift+mbtn_left_up"] =
  1907. function () show_message(get_chapterlist(), 3) end
  1908. ne.eventresponder["mbtn_right_up"] =
  1909. function () show_message(get_chapterlist(), 3) end
  1910.  
  1911. --ch_next
  1912. ne = new_element("ch_next", "button")
  1913.  
  1914. ne.enabled = have_ch
  1915. ne.content = "\238\132\133"
  1916. ne.eventresponder["mbtn_left_up"] =
  1917. function ()
  1918. mp.commandv("add", "chapter", 1)
  1919. if user_opts.chapters_osd then
  1920. show_message(get_chapterlist(), 3)
  1921. end
  1922. end
  1923. ne.eventresponder["shift+mbtn_left_up"] =
  1924. function () show_message(get_chapterlist(), 3) end
  1925. ne.eventresponder["mbtn_right_up"] =
  1926. function () show_message(get_chapterlist(), 3) end
  1927.  
  1928. --
  1929. update_tracklist()
  1930.  
  1931. --cy_audio
  1932. ne = new_element("cy_audio", "button")
  1933.  
  1934. ne.enabled = (#tracks_osc.audio > 0)
  1935. ne.content = function ()
  1936. local aid = "–"
  1937. if not (get_track("audio") == 0) then
  1938. aid = get_track("audio")
  1939. end
  1940. return ("\238\132\134" .. osc_styles.smallButtonsLlabel
  1941. .. " " .. aid .. "/" .. #tracks_osc.audio)
  1942. end
  1943. ne.eventresponder["mbtn_left_up"] =
  1944. function () set_track("audio", 1) end
  1945. ne.eventresponder["mbtn_right_up"] =
  1946. function () set_track("audio", -1) end
  1947. ne.eventresponder["shift+mbtn_left_down"] =
  1948. function () show_message(get_tracklist("audio"), 2) end
  1949.  
  1950. --cy_sub
  1951. ne = new_element("cy_sub", "button")
  1952.  
  1953. ne.enabled = (#tracks_osc.sub > 0)
  1954. ne.content = function ()
  1955. local sid = "–"
  1956. if not (get_track("sub") == 0) then
  1957. sid = get_track("sub")
  1958. end
  1959. return ("\238\132\135" .. osc_styles.smallButtonsLlabel
  1960. .. " " .. sid .. "/" .. #tracks_osc.sub)
  1961. end
  1962. ne.eventresponder["mbtn_left_up"] =
  1963. function () set_track("sub", 1) end
  1964. ne.eventresponder["mbtn_right_up"] =
  1965. function () set_track("sub", -1) end
  1966. ne.eventresponder["shift+mbtn_left_down"] =
  1967. function () show_message(get_tracklist("sub"), 2) end
  1968.  
  1969. --tog_fs
  1970. ne = new_element("tog_fs", "button")
  1971. ne.content = function ()
  1972. if (state.fullscreen) then
  1973. return ("\238\132\137")
  1974. else
  1975. return ("\238\132\136")
  1976. end
  1977. end
  1978. ne.eventresponder["mbtn_left_up"] =
  1979. function () mp.commandv("cycle", "fullscreen") end
  1980.  
  1981. --seekbar
  1982. ne = new_element("seekbar", "slider")
  1983.  
  1984. ne.enabled = not (mp.get_property("percent-pos") == nil)
  1985. state.slider_element = ne.enabled and ne or nil -- used for forced_title
  1986. ne.slider.markerF = function ()
  1987. local duration = mp.get_property_number("duration", nil)
  1988. if not (duration == nil) then
  1989. local chapters = mp.get_property_native("chapter-list", {})
  1990. local markers = {}
  1991. for n = 1, #chapters do
  1992. markers[n] = (chapters[n].time / duration * 100)
  1993. end
  1994. return markers
  1995. else
  1996. return {}
  1997. end
  1998. end
  1999. ne.slider.posF =
  2000. function () return mp.get_property_number("percent-pos", nil) end
  2001. ne.slider.tooltipF = function (pos)
  2002. local duration = mp.get_property_number("duration", nil)
  2003. if not ((duration == nil) or (pos == nil)) then
  2004. possec = duration * (pos / 100)
  2005. return mp.format_time(possec)
  2006. else
  2007. return ""
  2008. end
  2009. end
  2010. ne.slider.seekRangesF = function()
  2011. if user_opts.seekrangestyle == "none" then
  2012. return nil
  2013. end
  2014. local cache_state = state.cache_state
  2015. if not cache_state then
  2016. return nil
  2017. end
  2018. local duration = mp.get_property_number("duration", nil)
  2019. if (duration == nil) or duration <= 0 then
  2020. return nil
  2021. end
  2022. local ranges = cache_state["seekable-ranges"]
  2023. if #ranges == 0 then
  2024. return nil
  2025. end
  2026. local nranges = {}
  2027. for _, range in pairs(ranges) do
  2028. nranges[#nranges + 1] = {
  2029. ["start"] = 100 * range["start"] / duration,
  2030. ["end"] = 100 * range["end"] / duration,
  2031. }
  2032. end
  2033. return nranges
  2034. end
  2035. ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
  2036. function (element)
  2037. -- mouse move events may pile up during seeking and may still get
  2038. -- sent when the user is done seeking, so we need to throw away
  2039. -- identical seeks
  2040. local seekto = get_slider_value(element)
  2041. if (element.state.lastseek == nil) or
  2042. (not (element.state.lastseek == seekto)) then
  2043. local flags = "absolute-percent"
  2044. if not user_opts.seekbarkeyframes then
  2045. flags = flags .. "+exact"
  2046. end
  2047. mp.commandv("seek", seekto, flags)
  2048. element.state.lastseek = seekto
  2049. end
  2050.  
  2051. end
  2052. ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks
  2053. function (element) mp.commandv("seek", get_slider_value(element),
  2054. "absolute-percent", "exact") end
  2055. ne.eventresponder["reset"] =
  2056. function (element) element.state.lastseek = nil end
  2057.  
  2058.  
  2059. -- tc_left (current pos)
  2060. ne = new_element("tc_left", "button")
  2061.  
  2062. ne.content = function ()
  2063. if (state.tc_ms) then
  2064. return (mp.get_property_osd("playback-time/full"))
  2065. else
  2066. return (mp.get_property_osd("playback-time"))
  2067. end
  2068. end
  2069. ne.eventresponder["mbtn_left_up"] = function ()
  2070. state.tc_ms = not state.tc_ms
  2071. request_init()
  2072. end
  2073.  
  2074. -- tc_right (total/remaining time)
  2075. ne = new_element("tc_right", "button")
  2076.  
  2077. ne.visible = (mp.get_property_number("duration", 0) > 0)
  2078. ne.content = function ()
  2079. if (state.rightTC_trem) then
  2080. if state.tc_ms then
  2081. return ("-"..mp.get_property_osd("playtime-remaining/full"))
  2082. else
  2083. return ("-"..mp.get_property_osd("playtime-remaining"))
  2084. end
  2085. else
  2086. if state.tc_ms then
  2087. return (mp.get_property_osd("duration/full"))
  2088. else
  2089. return (mp.get_property_osd("duration"))
  2090. end
  2091. end
  2092. end
  2093. ne.eventresponder["mbtn_left_up"] =
  2094. function () state.rightTC_trem = not state.rightTC_trem end
  2095.  
  2096. -- cache
  2097. ne = new_element("cache", "button")
  2098.  
  2099. ne.content = function ()
  2100. local cache_state = state.cache_state
  2101. if not (cache_state and cache_state["seekable-ranges"] and
  2102. #cache_state["seekable-ranges"] > 0) then
  2103. -- probably not a network stream
  2104. return ""
  2105. end
  2106. local dmx_cache = cache_state and cache_state["cache-duration"]
  2107. local thresh = math.min(state.dmx_cache * 0.05, 5) -- 5% or 5s
  2108. if dmx_cache and math.abs(dmx_cache - state.dmx_cache) >= thresh then
  2109. state.dmx_cache = dmx_cache
  2110. else
  2111. dmx_cache = state.dmx_cache
  2112. end
  2113. local min = math.floor(dmx_cache / 60)
  2114. local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60
  2115. return "Cache: " .. (min > 0 and
  2116. string.format("%sm%02.0fs", min, sec) or
  2117. string.format("%3.0fs", sec))
  2118. end
  2119.  
  2120. -- volume
  2121. ne = new_element("volume", "button")
  2122.  
  2123. ne.content = function()
  2124. local volume = mp.get_property_number("volume", 0)
  2125. local mute = mp.get_property_native("mute")
  2126. local volicon = {"\238\132\139", "\238\132\140",
  2127. "\238\132\141", "\238\132\142"}
  2128. if volume == 0 or mute then
  2129. return "\238\132\138"
  2130. else
  2131. return volicon[math.min(4,math.ceil(volume / (100/3)))]
  2132. end
  2133. end
  2134. ne.eventresponder["mbtn_left_up"] =
  2135. function () mp.commandv("cycle", "mute") end
  2136.  
  2137. ne.eventresponder["wheel_up_press"] =
  2138. function () mp.commandv("osd-auto", "add", "volume", 5) end
  2139. ne.eventresponder["wheel_down_press"] =
  2140. function () mp.commandv("osd-auto", "add", "volume", -5) end
  2141.  
  2142.  
  2143. -- load layout
  2144. layouts[user_opts.layout]()
  2145.  
  2146. -- load window controls
  2147. if window_controls_enabled() then
  2148. window_controls(user_opts.layout == "topbar")
  2149. end
  2150.  
  2151. --do something with the elements
  2152. prepare_elements()
  2153.  
  2154. update_margins()
  2155. end
  2156.  
  2157. function reset_margins()
  2158. if state.using_video_margins then
  2159. for _, opt in ipairs(margins_opts) do
  2160. mp.set_property_number(opt[2], 0.0)
  2161. end
  2162. state.using_video_margins = false
  2163. end
  2164. end
  2165.  
  2166. function update_margins()
  2167. local margins = osc_param.video_margins
  2168.  
  2169. -- Don't use margins if it's visible only temporarily.
  2170. if (not state.osc_visible) or (get_hidetimeout() >= 0) or
  2171. (state.fullscreen and not user_opts.showfullscreen) or
  2172. (not state.fullscreen and not user_opts.showwindowed)
  2173. then
  2174. margins = {l = 0, r = 0, t = 0, b = 0}
  2175. end
  2176.  
  2177. if user_opts.boxvideo then
  2178. -- check whether any margin option has a non-default value
  2179. local margins_used = false
  2180.  
  2181. if not state.using_video_margins then
  2182. for _, opt in ipairs(margins_opts) do
  2183. if mp.get_property_number(opt[2], 0.0) ~= 0.0 then
  2184. margins_used = true
  2185. end
  2186. end
  2187. end
  2188.  
  2189. if not margins_used then
  2190. for _, opt in ipairs(margins_opts) do
  2191. local v = margins[opt[1]]
  2192. if (v ~= 0) or state.using_video_margins then
  2193. mp.set_property_number(opt[2], v)
  2194. state.using_video_margins = true
  2195. end
  2196. end
  2197. end
  2198. else
  2199. reset_margins()
  2200. end
  2201.  
  2202. utils.shared_script_property_set("osc-margins",
  2203. string.format("%f,%f,%f,%f", margins.l, margins.r, margins.t, margins.b))
  2204. end
  2205.  
  2206. function shutdown()
  2207. reset_margins()
  2208. utils.shared_script_property_set("osc-margins", nil)
  2209. end
  2210.  
  2211. --
  2212. -- Other important stuff
  2213. --
  2214.  
  2215.  
  2216. function show_osc()
  2217. -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
  2218. if not state.enabled then return end
  2219.  
  2220. msg.trace("show_osc")
  2221. --remember last time of invocation (mouse move)
  2222. state.showtime = mp.get_time()
  2223.  
  2224. osc_visible(true)
  2225.  
  2226. if (user_opts.fadeduration > 0) then
  2227. state.anitype = nil
  2228. end
  2229. end
  2230.  
  2231. function hide_osc()
  2232. msg.trace("hide_osc")
  2233. if not state.enabled then
  2234. -- typically hide happens at render() from tick(), but now tick() is
  2235. -- no-op and won't render again to remove the osc, so do that manually.
  2236. state.osc_visible = false
  2237. render_wipe()
  2238. elseif (user_opts.fadeduration > 0) then
  2239. if not(state.osc_visible == false) then
  2240. state.anitype = "out"
  2241. request_tick()
  2242. end
  2243. else
  2244. osc_visible(false)
  2245. end
  2246. end
  2247.  
  2248. function osc_visible(visible)
  2249. if state.osc_visible ~= visible then
  2250. state.osc_visible = visible
  2251. update_margins()
  2252. end
  2253. request_tick()
  2254. end
  2255.  
  2256. function pause_state(name, enabled)
  2257. state.paused = enabled
  2258. request_tick()
  2259. end
  2260.  
  2261. function cache_state(name, st)
  2262. state.cache_state = st
  2263. request_tick()
  2264. end
  2265.  
  2266. -- Request that tick() is called (which typically re-renders the OSC).
  2267. -- The tick is then either executed immediately, or rate-limited if it was
  2268. -- called a small time ago.
  2269. function request_tick()
  2270. if state.tick_timer == nil then
  2271. state.tick_timer = mp.add_timeout(0, tick)
  2272. end
  2273.  
  2274. if not state.tick_timer:is_enabled() then
  2275. local now = mp.get_time()
  2276. local timeout = tick_delay - (now - state.tick_last_time)
  2277. if timeout < 0 then
  2278. timeout = 0
  2279. end
  2280. state.tick_timer.timeout = timeout
  2281. state.tick_timer:resume()
  2282. end
  2283. end
  2284.  
  2285. function mouse_leave()
  2286. if get_hidetimeout() >= 0 then
  2287. hide_osc()
  2288. end
  2289. -- reset mouse position
  2290. state.last_mouseX, state.last_mouseY = nil, nil
  2291. state.mouse_in_window = false
  2292. end
  2293.  
  2294. function request_init()
  2295. state.initREQ = true
  2296. request_tick()
  2297. end
  2298.  
  2299. -- Like request_init(), but also request an immediate update
  2300. function request_init_resize()
  2301. request_init()
  2302. -- ensure immediate update
  2303. state.tick_timer:kill()
  2304. state.tick_timer.timeout = 0
  2305. state.tick_timer:resume()
  2306. end
  2307.  
  2308. function render_wipe()
  2309. msg.trace("render_wipe()")
  2310. state.osd.data = "" -- allows set_osd to immediately update on enable
  2311. state.osd:remove()
  2312. end
  2313.  
  2314. function render()
  2315. msg.trace("rendering")
  2316. local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
  2317. local mouseX, mouseY = get_virt_mouse_pos()
  2318. local now = mp.get_time()
  2319.  
  2320. -- check if display changed, if so request reinit
  2321. if not (state.mp_screen_sizeX == current_screen_sizeX
  2322. and state.mp_screen_sizeY == current_screen_sizeY) then
  2323.  
  2324. request_init_resize()
  2325.  
  2326. state.mp_screen_sizeX = current_screen_sizeX
  2327. state.mp_screen_sizeY = current_screen_sizeY
  2328. end
  2329.  
  2330. -- init management
  2331. if state.active_element then
  2332. -- mouse is held down on some element - keep ticking and igore initReq
  2333. -- till it's released, or else the mouse-up (click) will misbehave or
  2334. -- get ignored. that's because osc_init() recreates the osc elements,
  2335. -- but mouse handling depends on the elements staying unmodified
  2336. -- between mouse-down and mouse-up (using the index active_element).
  2337. request_tick()
  2338. elseif state.initREQ then
  2339. osc_init()
  2340. state.initREQ = false
  2341.  
  2342. -- store initial mouse position
  2343. if (state.last_mouseX == nil or state.last_mouseY == nil)
  2344. and not (mouseX == nil or mouseY == nil) then
  2345.  
  2346. state.last_mouseX, state.last_mouseY = mouseX, mouseY
  2347. end
  2348. end
  2349.  
  2350.  
  2351. -- fade animation
  2352. if not(state.anitype == nil) then
  2353.  
  2354. if (state.anistart == nil) then
  2355. state.anistart = now
  2356. end
  2357.  
  2358. if (now < state.anistart + (user_opts.fadeduration/1000)) then
  2359.  
  2360. if (state.anitype == "in") then --fade in
  2361. osc_visible(true)
  2362. state.animation = scale_value(state.anistart,
  2363. (state.anistart + (user_opts.fadeduration/1000)),
  2364. 255, 0, now)
  2365. elseif (state.anitype == "out") then --fade out
  2366. state.animation = scale_value(state.anistart,
  2367. (state.anistart + (user_opts.fadeduration/1000)),
  2368. 0, 255, now)
  2369. end
  2370.  
  2371. else
  2372. if (state.anitype == "out") then
  2373. osc_visible(false)
  2374. end
  2375. kill_animation()
  2376. end
  2377. else
  2378. kill_animation()
  2379. end
  2380.  
  2381. --mouse show/hide area
  2382. for k,cords in pairs(osc_param.areas["showhide"]) do
  2383. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
  2384. end
  2385. if osc_param.areas["showhide_wc"] then
  2386. for k,cords in pairs(osc_param.areas["showhide_wc"]) do
  2387. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc")
  2388. end
  2389. else
  2390. set_virt_mouse_area(0, 0, 0, 0, "showhide_wc")
  2391. end
  2392. do_enable_keybindings()
  2393.  
  2394. --mouse input area
  2395. local mouse_over_osc = false
  2396.  
  2397. for _,cords in ipairs(osc_param.areas["input"]) do
  2398. if state.osc_visible then -- activate only when OSC is actually visible
  2399. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
  2400. end
  2401. if state.osc_visible ~= state.input_enabled then
  2402. if state.osc_visible then
  2403. mp.enable_key_bindings("input")
  2404. else
  2405. mp.disable_key_bindings("input")
  2406. end
  2407. state.input_enabled = state.osc_visible
  2408. end
  2409.  
  2410. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  2411. mouse_over_osc = true
  2412. end
  2413. end
  2414.  
  2415. if osc_param.areas["window-controls"] then
  2416. for _,cords in ipairs(osc_param.areas["window-controls"]) do
  2417. if state.osc_visible then -- activate only when OSC is actually visible
  2418. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls")
  2419. mp.enable_key_bindings("window-controls")
  2420. else
  2421. mp.disable_key_bindings("window-controls")
  2422. end
  2423.  
  2424. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  2425. mouse_over_osc = true
  2426. end
  2427. end
  2428. end
  2429.  
  2430. if osc_param.areas["window-controls-title"] then
  2431. for _,cords in ipairs(osc_param.areas["window-controls-title"]) do
  2432. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  2433. mouse_over_osc = true
  2434. end
  2435. end
  2436. end
  2437.  
  2438. -- autohide
  2439. if not (state.showtime == nil) and (get_hidetimeout() >= 0) then
  2440. local timeout = state.showtime + (get_hidetimeout()/1000) - now
  2441. if timeout <= 0 then
  2442. if (state.active_element == nil) and not (mouse_over_osc) then
  2443. hide_osc()
  2444. end
  2445. else
  2446. -- the timer is only used to recheck the state and to possibly run
  2447. -- the code above again
  2448. if not state.hide_timer then
  2449. state.hide_timer = mp.add_timeout(0, tick)
  2450. end
  2451. state.hide_timer.timeout = timeout
  2452. -- re-arm
  2453. state.hide_timer:kill()
  2454. state.hide_timer:resume()
  2455. end
  2456. end
  2457.  
  2458.  
  2459. -- actual rendering
  2460. local ass = assdraw.ass_new()
  2461.  
  2462. -- Messages
  2463. render_message(ass)
  2464.  
  2465. -- actual OSC
  2466. if state.osc_visible then
  2467. render_elements(ass)
  2468. end
  2469.  
  2470. -- submit
  2471. set_osd(osc_param.playresy * osc_param.display_aspect,
  2472. osc_param.playresy, ass.text)
  2473. end
  2474.  
  2475. --
  2476. -- Eventhandling
  2477. --
  2478.  
  2479. local function element_has_action(element, action)
  2480. return element and element.eventresponder and
  2481. element.eventresponder[action]
  2482. end
  2483.  
  2484. function process_event(source, what)
  2485. local action = string.format("%s%s", source,
  2486. what and ("_" .. what) or "")
  2487.  
  2488. if what == "down" or what == "press" then
  2489.  
  2490. for n = 1, #elements do
  2491.  
  2492. if mouse_hit(elements[n]) and
  2493. elements[n].eventresponder and
  2494. (elements[n].eventresponder[source .. "_up"] or
  2495. elements[n].eventresponder[action]) then
  2496.  
  2497. if what == "down" then
  2498. state.active_element = n
  2499. state.active_event_source = source
  2500. end
  2501. -- fire the down or press event if the element has one
  2502. if element_has_action(elements[n], action) then
  2503. elements[n].eventresponder[action](elements[n])
  2504. end
  2505.  
  2506. end
  2507. end
  2508.  
  2509. elseif what == "up" then
  2510.  
  2511. if elements[state.active_element] then
  2512. local n = state.active_element
  2513.  
  2514. if n == 0 then
  2515. --click on background (does not work)
  2516. elseif element_has_action(elements[n], action) and
  2517. mouse_hit(elements[n]) then
  2518.  
  2519. elements[n].eventresponder[action](elements[n])
  2520. end
  2521.  
  2522. --reset active element
  2523. if element_has_action(elements[n], "reset") then
  2524. elements[n].eventresponder["reset"](elements[n])
  2525. end
  2526.  
  2527. end
  2528. state.active_element = nil
  2529. state.mouse_down_counter = 0
  2530.  
  2531. elseif source == "mouse_move" then
  2532.  
  2533. state.mouse_in_window = true
  2534.  
  2535. local mouseX, mouseY = get_virt_mouse_pos()
  2536. if (user_opts.minmousemove == 0) or
  2537. (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
  2538. ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
  2539. or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
  2540. )
  2541. ) then
  2542. show_osc()
  2543. end
  2544. state.last_mouseX, state.last_mouseY = mouseX, mouseY
  2545.  
  2546. local n = state.active_element
  2547. if element_has_action(elements[n], action) then
  2548. elements[n].eventresponder[action](elements[n])
  2549. end
  2550. end
  2551.  
  2552. -- ensure rendering after any (mouse) event - icons could change etc
  2553. request_tick()
  2554. end
  2555.  
  2556.  
  2557. local logo_lines = {
  2558. -- White border
  2559. "{\\c&HE5E5E5&\\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 {\\p0}",
  2560. -- Purple fill
  2561. "{\\c&H682167&\\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{\\p0}",
  2562. -- Darker fill
  2563. "{\\c&H430142&\\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}",
  2564. -- White fill
  2565. "{\\c&HDDDBDD&\\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}",
  2566. -- Triangle
  2567. "{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}",
  2568. }
  2569.  
  2570. local santa_hat_lines = {
  2571. -- Pompoms
  2572. "{\\c&HC0C0C0&\\p6}m 500 -323 b 491 -322 481 -318 475 -311 465 -312 456 -319 446 -318 434 -314 427 -304 417 -297 410 -290 404 -282 395 -278 390 -274 387 -267 381 -265 377 -261 379 -254 384 -253 397 -244 409 -232 425 -228 437 -228 446 -218 457 -217 462 -216 466 -213 468 -209 471 -205 477 -203 482 -206 491 -211 499 -217 508 -222 532 -235 556 -249 576 -267 584 -272 584 -284 578 -290 569 -305 550 -312 533 -309 523 -310 515 -316 507 -321 505 -323 503 -323 500 -323{\\p0}",
  2573. "{\\c&HE0E0E0&\\p6}m 315 -260 b 286 -258 259 -240 246 -215 235 -210 222 -215 211 -211 204 -188 177 -176 172 -151 170 -139 163 -128 154 -121 143 -103 141 -81 143 -60 139 -46 125 -34 129 -17 132 -1 134 16 142 30 145 56 161 80 181 96 196 114 210 133 231 144 266 153 303 138 328 115 373 79 401 28 423 -24 446 -73 465 -123 483 -174 487 -199 467 -225 442 -227 421 -232 402 -242 384 -254 364 -259 342 -250 322 -260 320 -260 317 -261 315 -260{\\p0}",
  2574. -- Main cap
  2575. "{\\c&H0000F0&\\p6}m 1151 -523 b 1016 -516 891 -458 769 -406 693 -369 624 -319 561 -262 526 -252 465 -235 479 -187 502 -147 551 -135 588 -111 1115 165 1379 232 1909 761 1926 800 1952 834 1987 858 2020 883 2053 912 2065 952 2088 1000 2146 962 2139 919 2162 836 2156 747 2143 662 2131 615 2116 567 2122 517 2120 410 2090 306 2089 199 2092 147 2071 99 2034 64 1987 5 1928 -41 1869 -86 1777 -157 1712 -256 1629 -337 1578 -389 1521 -436 1461 -476 1407 -509 1343 -507 1284 -515 1240 -519 1195 -521 1151 -523{\\p0}",
  2576. -- Cap shadow
  2577. "{\\c&H0000AA&\\p6}m 1657 248 b 1658 254 1659 261 1660 267 1669 276 1680 284 1689 293 1695 302 1700 311 1707 320 1716 325 1726 330 1735 335 1744 347 1752 360 1761 371 1753 352 1754 331 1753 311 1751 237 1751 163 1751 90 1752 64 1752 37 1767 14 1778 -3 1785 -24 1786 -45 1786 -60 1786 -77 1774 -87 1760 -96 1750 -78 1751 -65 1748 -37 1750 -8 1750 20 1734 78 1715 134 1699 192 1694 211 1689 231 1676 246 1671 251 1661 255 1657 248 m 1909 541 b 1914 542 1922 549 1917 539 1919 520 1921 502 1919 483 1918 458 1917 433 1915 407 1930 373 1942 338 1947 301 1952 270 1954 238 1951 207 1946 214 1947 229 1945 239 1939 278 1936 318 1924 356 1923 362 1913 382 1912 364 1906 301 1904 237 1891 175 1887 150 1892 126 1892 101 1892 68 1893 35 1888 2 1884 -9 1871 -20 1859 -14 1851 -6 1854 9 1854 20 1855 58 1864 95 1873 132 1883 179 1894 225 1899 273 1908 362 1910 451 1909 541{\\p0}",
  2578. -- Brim and tip pompom
  2579. "{\\c&HF8F8F8&\\p6}m 626 -191 b 565 -155 486 -196 428 -151 387 -115 327 -101 304 -47 273 2 267 59 249 113 219 157 217 213 215 265 217 309 260 302 285 283 373 264 465 264 555 257 608 252 655 292 709 287 759 294 816 276 863 298 903 340 972 324 1012 367 1061 394 1125 382 1167 424 1213 462 1268 482 1322 506 1385 546 1427 610 1479 662 1510 690 1534 725 1566 752 1611 796 1664 830 1703 880 1740 918 1747 986 1805 1005 1863 991 1897 932 1916 880 1914 823 1945 777 1961 725 1979 673 1957 622 1938 575 1912 534 1862 515 1836 473 1790 417 1755 351 1697 305 1658 266 1633 216 1593 176 1574 138 1539 116 1497 110 1448 101 1402 77 1371 37 1346 -16 1295 15 1254 6 1211 -27 1170 -62 1121 -86 1072 -104 1027 -128 976 -133 914 -130 851 -137 794 -162 740 -181 679 -168 626 -191 m 2051 917 b 1971 932 1929 1017 1919 1091 1912 1149 1923 1214 1970 1254 2000 1279 2027 1314 2066 1325 2139 1338 2212 1295 2254 1238 2281 1203 2287 1158 2282 1116 2292 1061 2273 1006 2229 970 2206 941 2167 938 2138 918{\\p0}",
  2580. }
  2581.  
  2582. -- called by mpv on every frame
  2583. function tick()
  2584. if state.marginsREQ == true then
  2585. update_margins()
  2586. state.marginsREQ = false
  2587. end
  2588.  
  2589. if (not state.enabled) then return end
  2590.  
  2591. if (state.idle) then
  2592.  
  2593. -- render idle message
  2594. msg.trace("idle message")
  2595. local icon_x, icon_y = 320 - 26, 140
  2596. local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y)
  2597.  
  2598. local ass = assdraw.ass_new()
  2599. -- mpv logo
  2600. for i, line in ipairs(logo_lines) do
  2601. ass:new_event()
  2602. ass:append(line_prefix .. line)
  2603. end
  2604.  
  2605. -- Santa hat
  2606. if is_december and not user_opts.greenandgrumpy then
  2607. for i, line in ipairs(santa_hat_lines) do
  2608. ass:new_event()
  2609. ass:append(line_prefix .. line)
  2610. end
  2611. end
  2612.  
  2613. ass:new_event()
  2614. ass:pos(320, icon_y+65)
  2615. ass:an(8)
  2616. ass:append("Drop files or URLs to play here.")
  2617. set_osd(640, 360, ass.text)
  2618.  
  2619. if state.showhide_enabled then
  2620. mp.disable_key_bindings("showhide")
  2621. mp.disable_key_bindings("showhide_wc")
  2622. state.showhide_enabled = false
  2623. end
  2624.  
  2625.  
  2626. elseif (state.fullscreen and user_opts.showfullscreen)
  2627. or (not state.fullscreen and user_opts.showwindowed) then
  2628.  
  2629. -- render the OSC
  2630. render()
  2631. else
  2632. -- Flush OSD
  2633. render_wipe()
  2634. end
  2635.  
  2636. state.tick_last_time = mp.get_time()
  2637.  
  2638. if state.anitype ~= nil then
  2639. -- state.anistart can be nil - animation should now start, or it can
  2640. -- be a timestamp when it started. state.idle has no animation.
  2641. if not state.idle and
  2642. (not state.anistart or
  2643. mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000)
  2644. then
  2645. -- animating or starting, or still within 1s past the deadline
  2646. request_tick()
  2647. else
  2648. kill_animation()
  2649. end
  2650. end
  2651. end
  2652.  
  2653. function do_enable_keybindings()
  2654. if state.enabled then
  2655. if not state.showhide_enabled then
  2656. mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
  2657. mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor")
  2658. end
  2659. state.showhide_enabled = true
  2660. end
  2661. end
  2662.  
  2663. function enable_osc(enable)
  2664. state.enabled = enable
  2665. if enable then
  2666. do_enable_keybindings()
  2667. else
  2668. hide_osc() -- acts immediately when state.enabled == false
  2669. if state.showhide_enabled then
  2670. mp.disable_key_bindings("showhide")
  2671. mp.disable_key_bindings("showhide_wc")
  2672. end
  2673. state.showhide_enabled = false
  2674. end
  2675. end
  2676.  
  2677. -- duration is observed for the sole purpose of updating chapter markers
  2678. -- positions. live streams with chapters are very rare, and the update is also
  2679. -- expensive (with request_init), so it's only observed when we have chapters
  2680. -- and the user didn't disable the livemarkers option (update_duration_watch).
  2681. function on_duration() request_init() end
  2682.  
  2683. local duration_watched = false
  2684. function update_duration_watch()
  2685. local want_watch = user_opts.livemarkers and
  2686. (mp.get_property_number("chapters", 0) or 0) > 0 and
  2687. true or false -- ensure it's a boolean
  2688.  
  2689. if (want_watch ~= duration_watched) then
  2690. if want_watch then
  2691. mp.observe_property("duration", nil, on_duration)
  2692. else
  2693. mp.unobserve_property(on_duration)
  2694. end
  2695. duration_watched = want_watch
  2696. end
  2697. end
  2698.  
  2699. validate_user_opts()
  2700. update_duration_watch()
  2701.  
  2702. mp.register_event("shutdown", shutdown)
  2703. mp.register_event("start-file", request_init)
  2704. mp.observe_property("track-list", nil, request_init)
  2705. mp.observe_property("playlist", nil, request_init)
  2706. mp.observe_property("chapter-list", nil, function()
  2707. update_duration_watch()
  2708. request_init()
  2709. end)
  2710.  
  2711. mp.register_script_message("osc-message", show_message)
  2712. mp.register_script_message("osc-chapterlist", function(dur)
  2713. show_message(get_chapterlist(), dur)
  2714. end)
  2715. mp.register_script_message("osc-playlist", function(dur)
  2716. show_message(get_playlist(), dur)
  2717. end)
  2718. mp.register_script_message("osc-tracklist", function(dur)
  2719. local msg = {}
  2720. for k,v in pairs(nicetypes) do
  2721. table.insert(msg, get_tracklist(k))
  2722. end
  2723. show_message(table.concat(msg, '\n\n'), dur)
  2724. end)
  2725.  
  2726. mp.observe_property("fullscreen", "bool",
  2727. function(name, val)
  2728. state.fullscreen = val
  2729. state.marginsREQ = true
  2730. request_init_resize()
  2731. end
  2732. )
  2733. mp.observe_property("border", "bool",
  2734. function(name, val)
  2735. state.border = val
  2736. request_init_resize()
  2737. end
  2738. )
  2739. mp.observe_property("window-maximized", "bool",
  2740. function(name, val)
  2741. state.maximized = val
  2742. request_init_resize()
  2743. end
  2744. )
  2745. mp.observe_property("idle-active", "bool",
  2746. function(name, val)
  2747. state.idle = val
  2748. request_tick()
  2749. end
  2750. )
  2751. mp.observe_property("pause", "bool", pause_state)
  2752. mp.observe_property("demuxer-cache-state", "native", cache_state)
  2753. mp.observe_property("vo-configured", "bool", function(name, val)
  2754. request_tick()
  2755. end)
  2756. mp.observe_property("playback-time", "number", function(name, val)
  2757. request_tick()
  2758. end)
  2759. mp.observe_property("osd-dimensions", "native", function(name, val)
  2760. -- (we could use the value instead of re-querying it all the time, but then
  2761. -- we might have to worry about property update ordering)
  2762. request_init_resize()
  2763. end)
  2764.  
  2765. -- mouse show/hide bindings
  2766. mp.set_key_bindings({
  2767. {"mouse_move", function(e) process_event("mouse_move", nil) end},
  2768. {"mouse_leave", mouse_leave},
  2769. }, "showhide", "force")
  2770. mp.set_key_bindings({
  2771. {"mouse_move", function(e) process_event("mouse_move", nil) end},
  2772. {"mouse_leave", mouse_leave},
  2773. }, "showhide_wc", "force")
  2774. do_enable_keybindings()
  2775.  
  2776. --mouse input bindings
  2777. mp.set_key_bindings({
  2778. {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
  2779. function(e) process_event("mbtn_left", "down") end},
  2780. {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
  2781. function(e) process_event("shift+mbtn_left", "down") end},
  2782. {"mbtn_right", function(e) process_event("mbtn_right", "up") end,
  2783. function(e) process_event("mbtn_right", "down") end},
  2784. -- alias to shift_mbtn_left for single-handed mouse use
  2785. {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end,
  2786. function(e) process_event("shift+mbtn_left", "down") end},
  2787. {"wheel_up", function(e) process_event("wheel_up", "press") end},
  2788. {"wheel_down", function(e) process_event("wheel_down", "press") end},
  2789. {"mbtn_left_dbl", "ignore"},
  2790. {"shift+mbtn_left_dbl", "ignore"},
  2791. {"mbtn_right_dbl", "ignore"},
  2792. }, "input", "force")
  2793. mp.enable_key_bindings("input")
  2794.  
  2795. mp.set_key_bindings({
  2796. {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
  2797. function(e) process_event("mbtn_left", "down") end},
  2798. }, "window-controls", "force")
  2799. mp.enable_key_bindings("window-controls")
  2800.  
  2801. function get_hidetimeout()
  2802. if user_opts.visibility == "always" then
  2803. return -1 -- disable autohide
  2804. end
  2805. return user_opts.hidetimeout
  2806. end
  2807.  
  2808. function always_on(val)
  2809. if state.enabled then
  2810. if val then
  2811. show_osc()
  2812. else
  2813. hide_osc()
  2814. end
  2815. end
  2816. end
  2817.  
  2818. -- mode can be auto/always/never/cycle
  2819. -- the modes only affect internal variables and not stored on its own.
  2820. function visibility_mode(mode, no_osd)
  2821. if mode == "cycle" then
  2822. if not state.enabled then
  2823. mode = "auto"
  2824. elseif user_opts.visibility ~= "always" then
  2825. mode = "always"
  2826. else
  2827. mode = "never"
  2828. end
  2829. end
  2830.  
  2831. if mode == "auto" then
  2832. always_on(false)
  2833. enable_osc(true)
  2834. elseif mode == "always" then
  2835. enable_osc(true)
  2836. always_on(true)
  2837. elseif mode == "never" then
  2838. enable_osc(false)
  2839. else
  2840. msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
  2841. return
  2842. end
  2843.  
  2844. user_opts.visibility = mode
  2845. utils.shared_script_property_set("osc-visibility", mode)
  2846.  
  2847. if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
  2848. mp.osd_message("OSC visibility: " .. mode)
  2849. end
  2850.  
  2851. -- Reset the input state on a mode change. The input state will be
  2852. -- recalcuated on the next render cycle, except in 'never' mode where it
  2853. -- will just stay disabled.
  2854. mp.disable_key_bindings("input")
  2855. mp.disable_key_bindings("window-controls")
  2856. state.input_enabled = false
  2857.  
  2858. update_margins()
  2859. request_tick()
  2860. end
  2861.  
  2862. visibility_mode(user_opts.visibility, true)
  2863. mp.register_script_message("osc-visibility", visibility_mode)
  2864. mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
  2865.  
  2866. set_virt_mouse_area(0, 0, 0, 0, "input")
  2867. set_virt_mouse_area(0, 0, 0, 0, "window-controls")
  2868.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement