osd.lua 97 KB

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