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

Edit this Snippet