mordenx.lua 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506
  1. -- mpv-osc-morden by maoiscat
  2. -- email:valarmor@163.com
  3. -- https://github.com/maoiscat/mpv-osc-morden
  4. -- fork by cyl0
  5. -- https://github.com/cyl0/mpv-osc-morden-x
  6. local assdraw = require 'mp.assdraw'
  7. local msg = require 'mp.msg'
  8. local opt = require 'mp.options'
  9. local utils = require 'mp.utils'
  10. --
  11. -- Parameters
  12. --
  13. -- default user option values
  14. -- may change them in osc.conf
  15. local user_opts = {
  16. showwindowed = true, -- show OSC when windowed?
  17. showfullscreen = true, -- show OSC when fullscreen?
  18. scalewindowed = 1.0, -- scaling of the controller when windowed
  19. scalefullscreen = 1.0, -- scaling of the controller when fullscreen
  20. scaleforcedwindow = 1.0, -- scaling when rendered on a forced window
  21. vidscale = false, -- scale the controller with the video?
  22. hidetimeout = 1500, -- duration in ms until the OSC hides if no
  23. -- mouse movement. enforced non-negative for the
  24. -- user, but internally negative is 'always-on'.
  25. fadeduration = 250, -- duration of fade out in ms, 0 = no fade
  26. minmousemove = 1, -- minimum amount of pixels the mouse has to
  27. -- move between ticks to make the OSC show up
  28. iamaprogrammer = false, -- use native mpv values and disable OSC
  29. -- internal track list management (and some
  30. -- functions that depend on it)
  31. font = 'mpv-osd-symbols', -- default osc font
  32. seekbarhandlesize = 1.0, -- size ratio of the slider handle, range 0 ~ 1
  33. seekrange = true, -- show seekrange overlay
  34. seekrangealpha = 64, -- transparency of seekranges
  35. seekbarkeyframes = true, -- use keyframes when dragging the seekbar
  36. showjump = true, -- show "jump forward/backward 5 seconds" buttons
  37. -- shift+left-click to step 1 frame and
  38. -- right-click to jump 1 minute
  39. jumpamount = 5, -- change the jump amount (in seconds by default)
  40. jumpiconnumber = true, -- show different icon when jumpamount is 5, 10, or 30
  41. jumpmode = 'exact', -- seek mode for jump buttons. e.g.
  42. -- 'exact', 'relative+keyframes', etc.
  43. title = '${media-title}', -- string compatible with property-expansion
  44. -- to be shown as OSC title
  45. showtitle = true, -- show title in OSC
  46. showonpause = true, -- whether to disable the hide timeout on pause
  47. timetotal = true, -- display total time instead of remaining time?
  48. timems = false, -- Display time down to millliseconds by default
  49. visibility = 'auto', -- only used at init to set visibility_mode(...)
  50. windowcontrols = 'no', -- whether to show window controls
  51. language = 'eng', -- eng=English, chs=Chinese
  52. keyboardnavigation = false, -- enable directional keyboard navigation
  53. chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable
  54. }
  55. -- Icons for jump button depending on jumpamount
  56. local jumpicons = {
  57. [5] = {'\xEF\x8E\xB1', '\xEF\x8E\xA3'},
  58. [10] = {'\xEF\x8E\xAF', '\xEF\x8E\xA1'},
  59. [30] = {'\xEF\x8E\xB0', '\xEF\x8E\xA2'},
  60. default = {'\xEF\x8E\xB2', '\xEF\x8E\xB2'}, -- second icon is mirrored in layout()
  61. }
  62. -- Localization
  63. local language = {
  64. ['eng'] = {
  65. welcome = '{\\fs24\\1c&H0&\\1c&HFFFFFF&}Drop files or URLs to play here.', -- this text appears when mpv starts
  66. off = 'OFF',
  67. na = 'n/a',
  68. none = 'none',
  69. video = 'Video',
  70. audio = 'Audio',
  71. subtitle = 'Subtitle',
  72. available = 'Available ',
  73. track = ' Tracks:',
  74. playlist = 'Playlist',
  75. nolist = 'Empty playlist.',
  76. chapter = 'Chapter',
  77. nochapter = 'No chapters.',
  78. },
  79. ['chs'] = {
  80. welcome = '{\\1c&H00\\bord0\\fs30\\fn微软雅黑 light\\fscx125}MPV{\\fscx100} 播放器', -- this text appears when mpv starts
  81. off = '关闭',
  82. na = 'n/a',
  83. none = '无',
  84. video = '视频',
  85. audio = '音频',
  86. subtitle = '字幕',
  87. available = '可选',
  88. track = ':',
  89. playlist = '播放列表',
  90. nolist = '无列表信息',
  91. chapter = '章节',
  92. nochapter = '无章节信息',
  93. }
  94. }
  95. -- read options from config and command-line
  96. opt.read_options(user_opts, 'osc', function(list) update_options(list) end)
  97. -- apply lang opts
  98. local texts = language[user_opts.language]
  99. local osc_param = { -- calculated by osc_init()
  100. playresy = 0, -- canvas size Y
  101. playresx = 0, -- canvas size X
  102. display_aspect = 1,
  103. unscaled_y = 0,
  104. areas = {},
  105. }
  106. local osc_styles = {
  107. TransBg = '{\\blur100\\bord150\\1c&H000000&\\3c&H000000&}',
  108. SeekbarBg = '{\\blur0\\bord0\\1c&HFFFFFF&}',
  109. SeekbarFg = '{\\blur1\\bord1\\1c&HE39C42&}',
  110. Ctrl1 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs36\\fnmaterial-design-iconic-font}',
  111. Ctrl2 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs30\\fnmaterial-design-iconic-font}',
  112. Ctrl2Flip = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs30\\fnmaterial-design-iconic-font\\fry180',
  113. Ctrl3 = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs30\\fnmaterial-design-iconic-font}',
  114. Time = '{\\blur0\\bord0\\1c&HFFFFFF&\\3c&H000000&\\fs20\\fn' .. user_opts.font .. '}',
  115. Tooltip = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H000000&\\fs20\\fn' .. user_opts.font .. '}',
  116. Title = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs32\\q2\\fn' .. user_opts.font .. '}',
  117. WinCtrl = '{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs20\\fnmpv-osd-symbols}',
  118. elementDown = '{\\1c&H999999&}',
  119. elementHighlight = '{\\blur1\\bord1\\1c&HFFC033&}',
  120. }
  121. -- internal states, do not touch
  122. local state = {
  123. showtime, -- time of last invocation (last mouse move)
  124. osc_visible = false,
  125. anistart, -- time when the animation started
  126. anitype, -- current type of animation
  127. animation, -- current animation alpha
  128. mouse_down_counter = 0, -- used for softrepeat
  129. active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
  130. active_event_source = nil, -- the 'button' that issued the current event
  131. rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
  132. mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
  133. initREQ = false, -- is a re-init request pending?
  134. last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
  135. mouse_in_window = false,
  136. message_text,
  137. message_hide_timer,
  138. fullscreen = false,
  139. tick_timer = nil,
  140. tick_last_time = 0, -- when the last tick() was run
  141. hide_timer = nil,
  142. cache_state = nil,
  143. idle = false,
  144. enabled = true,
  145. input_enabled = true,
  146. showhide_enabled = false,
  147. dmx_cache = 0,
  148. border = true,
  149. maximized = false,
  150. osd = mp.create_osd_overlay('ass-events'),
  151. lastvisibility = user_opts.visibility, -- save last visibility on pause if showonpause
  152. fulltime = user_opts.timems,
  153. highlight_element = 'cy_audio',
  154. chapter_list = {}, -- sorted by time
  155. }
  156. local window_control_box_width = 138
  157. local tick_delay = 0.03
  158. --- Automatically disable OSC
  159. local builtin_osc_enabled = mp.get_property_native('osc')
  160. if builtin_osc_enabled then
  161. mp.set_property_native('osc', false)
  162. end
  163. --
  164. -- WindowControl helpers
  165. function window_controls_enabled()
  166. val = user_opts.windowcontrols
  167. if val == 'auto' then
  168. return (not state.border) or state.fullscreen
  169. else
  170. return val ~= 'no'
  171. end
  172. end
  173. function build_keyboard_controls()
  174. -- prepare the main button row
  175. local bottom_button_line = {}
  176. table.insert(bottom_button_line, 'cy_audio')
  177. table.insert(bottom_button_line, 'cy_sub')
  178. table.insert(bottom_button_line, 'pl_prev')
  179. table.insert(bottom_button_line, 'skipback')
  180. if user_opts.showjump then
  181. table.insert(bottom_button_line, 'jumpback')
  182. end
  183. table.insert(bottom_button_line, 'playpause')
  184. if user_opts.showjump then
  185. table.insert(bottom_button_line, 'jumpfrwd')
  186. end
  187. table.insert(bottom_button_line, 'skipfrwd')
  188. table.insert(bottom_button_line, 'pl_next')
  189. table.insert(bottom_button_line, 'tog_info')
  190. table.insert(bottom_button_line, 'tog_fs')
  191. -- build up the main mapping object
  192. local mapping = {}
  193. if window_controls_enabled() then
  194. table.insert(mapping, {
  195. 'minimize',
  196. 'maximize',
  197. 'close'
  198. })
  199. end
  200. table.insert(mapping, {
  201. 'seekbar'
  202. })
  203. table.insert(mapping, bottom_button_line)
  204. return mapping
  205. end
  206. --
  207. -- Helperfunctions
  208. --
  209. function set_osd(res_x, res_y, text)
  210. if state.osd.res_x == res_x and
  211. state.osd.res_y == res_y and
  212. state.osd.data == text then
  213. return
  214. end
  215. state.osd.res_x = res_x
  216. state.osd.res_y = res_y
  217. state.osd.data = text
  218. state.osd.z = 1000
  219. state.osd:update()
  220. end
  221. -- scale factor for translating between real and virtual ASS coordinates
  222. function get_virt_scale_factor()
  223. local w, h = mp.get_osd_size()
  224. if w <= 0 or h <= 0 then
  225. return 0, 0
  226. end
  227. return osc_param.playresx / w, osc_param.playresy / h
  228. end
  229. -- return mouse position in virtual ASS coordinates (playresx/y)
  230. function get_virt_mouse_pos()
  231. if state.mouse_in_window then
  232. local sx, sy = get_virt_scale_factor()
  233. local x, y = mp.get_mouse_pos()
  234. return x * sx, y * sy
  235. else
  236. return -1, -1
  237. end
  238. end
  239. function set_virt_mouse_area(x0, y0, x1, y1, name)
  240. local sx, sy = get_virt_scale_factor()
  241. mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
  242. end
  243. function scale_value(x0, x1, y0, y1, val)
  244. local m = (y1 - y0) / (x1 - x0)
  245. local b = y0 - (m * x0)
  246. return (m * val) + b
  247. end
  248. -- returns hitbox spanning coordinates (top left, bottom right corner)
  249. -- according to alignment
  250. function get_hitbox_coords(x, y, an, w, h)
  251. local alignments = {
  252. [1] = function () return x, y-h, x+w, y end,
  253. [2] = function () return x-(w/2), y-h, x+(w/2), y end,
  254. [3] = function () return x-w, y-h, x, y end,
  255. [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
  256. [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
  257. [6] = function () return x-w, y-(h/2), x, y+(h/2) end,
  258. [7] = function () return x, y, x+w, y+h end,
  259. [8] = function () return x-(w/2), y, x+(w/2), y+h end,
  260. [9] = function () return x-w, y, x, y+h end,
  261. }
  262. return alignments[an]()
  263. end
  264. function get_hitbox_coords_geo(geometry)
  265. return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
  266. geometry.w, geometry.h)
  267. end
  268. function get_element_hitbox(element)
  269. return element.hitbox.x1, element.hitbox.y1,
  270. element.hitbox.x2, element.hitbox.y2
  271. end
  272. function mouse_hit(element)
  273. return mouse_hit_coords(get_element_hitbox(element))
  274. end
  275. function mouse_hit_coords(bX1, bY1, bX2, bY2)
  276. local mX, mY = get_virt_mouse_pos()
  277. return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
  278. end
  279. function limit_range(min, max, val)
  280. if val > max then
  281. val = max
  282. elseif val < min then
  283. val = min
  284. end
  285. return val
  286. end
  287. -- translate value into element coordinates
  288. function get_slider_ele_pos_for(element, val)
  289. local ele_pos = scale_value(
  290. element.slider.min.value, element.slider.max.value,
  291. element.slider.min.ele_pos, element.slider.max.ele_pos,
  292. val)
  293. return limit_range(
  294. element.slider.min.ele_pos, element.slider.max.ele_pos,
  295. ele_pos)
  296. end
  297. -- translates global (mouse) coordinates to value
  298. function get_slider_value_at(element, glob_pos)
  299. local val = scale_value(
  300. element.slider.min.glob_pos, element.slider.max.glob_pos,
  301. element.slider.min.value, element.slider.max.value,
  302. glob_pos)
  303. return limit_range(
  304. element.slider.min.value, element.slider.max.value,
  305. val)
  306. end
  307. -- get value at current mouse position
  308. function get_slider_value(element)
  309. return get_slider_value_at(element, get_virt_mouse_pos())
  310. end
  311. function countone(val)
  312. if not (user_opts.iamaprogrammer) then
  313. val = val + 1
  314. end
  315. return val
  316. end
  317. -- multiplies two alpha values, formular can probably be improved
  318. function mult_alpha(alphaA, alphaB)
  319. return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
  320. end
  321. function add_area(name, x1, y1, x2, y2)
  322. -- create area if needed
  323. if (osc_param.areas[name] == nil) then
  324. osc_param.areas[name] = {}
  325. end
  326. table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
  327. end
  328. function ass_append_alpha(ass, alpha, modifier)
  329. local ar = {}
  330. for ai, av in pairs(alpha) do
  331. av = mult_alpha(av, modifier)
  332. if state.animation then
  333. av = mult_alpha(av, state.animation)
  334. end
  335. ar[ai] = av
  336. end
  337. ass:append(string.format('{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
  338. ar[1], ar[2], ar[3], ar[4]))
  339. end
  340. function ass_draw_cir_cw(ass, x, y, r)
  341. ass:round_rect_cw(x-r, y-r, x+r, y+r, r)
  342. end
  343. function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
  344. if hexagon then
  345. ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
  346. else
  347. ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
  348. end
  349. end
  350. function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
  351. if hexagon then
  352. ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
  353. else
  354. ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
  355. end
  356. end
  357. --
  358. -- Tracklist Management
  359. --
  360. local nicetypes = {video = texts.video, audio = texts.audio, sub = texts.subtitle}
  361. -- updates the OSC internal playlists, should be run each time the track-layout changes
  362. function update_tracklist()
  363. local tracktable = mp.get_property_native('track-list', {})
  364. -- by osc_id
  365. tracks_osc = {}
  366. tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
  367. -- by mpv_id
  368. tracks_mpv = {}
  369. tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
  370. for n = 1, #tracktable do
  371. if not (tracktable[n].type == 'unknown') then
  372. local type = tracktable[n].type
  373. local mpv_id = tonumber(tracktable[n].id)
  374. -- by osc_id
  375. table.insert(tracks_osc[type], tracktable[n])
  376. -- by mpv_id
  377. tracks_mpv[type][mpv_id] = tracktable[n]
  378. tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
  379. end
  380. end
  381. end
  382. -- return a nice list of tracks of the given type (video, audio, sub)
  383. function get_tracklist(type)
  384. local msg = texts.available .. nicetypes[type] .. texts.track
  385. if #tracks_osc[type] == 0 then
  386. msg = msg .. texts.none
  387. else
  388. for n = 1, #tracks_osc[type] do
  389. local track = tracks_osc[type][n]
  390. local lang, title, selected = 'unknown', '', '○'
  391. if not(track.lang == nil) then lang = track.lang end
  392. if not(track.title == nil) then title = track.title end
  393. if (track.id == tonumber(mp.get_property(type))) then
  394. selected = '●'
  395. end
  396. msg = msg..'\n'..selected..' '..n..': ['..lang..'] '..title
  397. end
  398. end
  399. return msg
  400. end
  401. -- relatively change the track of given <type> by <next> tracks
  402. --(+1 -> next, -1 -> previous)
  403. function set_track(type, next)
  404. local current_track_mpv, current_track_osc
  405. if (mp.get_property(type) == 'no') then
  406. current_track_osc = 0
  407. else
  408. current_track_mpv = tonumber(mp.get_property(type))
  409. current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
  410. end
  411. local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
  412. local new_track_mpv
  413. if new_track_osc == 0 then
  414. new_track_mpv = 'no'
  415. else
  416. new_track_mpv = tracks_osc[type][new_track_osc].id
  417. end
  418. mp.commandv('set', type, new_track_mpv)
  419. -- if (new_track_osc == 0) then
  420. -- show_message(nicetypes[type] .. ' Track: none')
  421. -- else
  422. -- show_message(nicetypes[type] .. ' Track: '
  423. -- .. new_track_osc .. '/' .. #tracks_osc[type]
  424. -- .. ' ['.. (tracks_osc[type][new_track_osc].lang or 'unknown') ..'] '
  425. -- .. (tracks_osc[type][new_track_osc].title or ''))
  426. -- end
  427. end
  428. -- get the currently selected track of <type>, OSC-style counted
  429. function get_track(type)
  430. local track = mp.get_property(type)
  431. if track ~= 'no' and track ~= nil then
  432. local tr = tracks_mpv[type][tonumber(track)]
  433. if tr then
  434. return tr.osc_id
  435. end
  436. end
  437. return 0
  438. end
  439. --
  440. -- Element Management
  441. --
  442. local elements = {}
  443. function prepare_elements()
  444. -- remove elements without layout or invisble
  445. local elements2 = {}
  446. for n, element in pairs(elements) do
  447. if not (element.layout == nil) and (element.visible) then
  448. table.insert(elements2, element)
  449. end
  450. end
  451. elements = elements2
  452. function elem_compare (a, b)
  453. return a.layout.layer < b.layout.layer
  454. end
  455. table.sort(elements, elem_compare)
  456. for _,element in pairs(elements) do
  457. local elem_geo = element.layout.geometry
  458. -- Calculate the hitbox
  459. local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
  460. element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
  461. local style_ass = assdraw.ass_new()
  462. -- prepare static elements
  463. style_ass:append('{}') -- hack to troll new_event into inserting a \n
  464. style_ass:new_event()
  465. style_ass:pos(elem_geo.x, elem_geo.y)
  466. style_ass:an(elem_geo.an)
  467. style_ass:append(element.layout.style)
  468. element.style_ass = style_ass
  469. local static_ass = assdraw.ass_new()
  470. if (element.type == 'box') then
  471. --draw box
  472. static_ass:draw_start()
  473. ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
  474. element.layout.box.radius, element.layout.box.hexagon)
  475. static_ass:draw_stop()
  476. elseif (element.type == 'slider') then
  477. --draw static slider parts
  478. local slider_lo = element.layout.slider
  479. -- calculate positions of min and max points
  480. element.slider.min.ele_pos = user_opts.seekbarhandlesize * elem_geo.h / 2
  481. element.slider.max.ele_pos = elem_geo.w - element.slider.min.ele_pos
  482. element.slider.min.glob_pos = element.hitbox.x1 + element.slider.min.ele_pos
  483. element.slider.max.glob_pos = element.hitbox.x1 + element.slider.max.ele_pos
  484. static_ass:draw_start()
  485. -- a hack which prepares the whole slider area to allow center placements such like an=5
  486. static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h)
  487. static_ass:rect_ccw(0, 0, elem_geo.w, elem_geo.h)
  488. -- marker nibbles
  489. if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
  490. local markers = element.slider.markerF()
  491. for _,marker in pairs(markers) do
  492. if (marker >= element.slider.min.value) and (marker <= element.slider.max.value) then
  493. local s = get_slider_ele_pos_for(element, marker)
  494. if (slider_lo.gap > 5) then -- draw triangles
  495. --top
  496. if (slider_lo.nibbles_top) then
  497. static_ass:move_to(s - 3, slider_lo.gap - 5)
  498. static_ass:line_to(s + 3, slider_lo.gap - 5)
  499. static_ass:line_to(s, slider_lo.gap - 1)
  500. end
  501. --bottom
  502. if (slider_lo.nibbles_bottom) then
  503. static_ass:move_to(s - 3, elem_geo.h - slider_lo.gap + 5)
  504. static_ass:line_to(s, elem_geo.h - slider_lo.gap + 1)
  505. static_ass:line_to(s + 3, elem_geo.h - slider_lo.gap + 5)
  506. end
  507. else -- draw 2x1px nibbles
  508. --top
  509. if (slider_lo.nibbles_top) then
  510. static_ass:rect_cw(s - 1, 0, s + 1, slider_lo.gap);
  511. end
  512. --bottom
  513. if (slider_lo.nibbles_bottom) then
  514. static_ass:rect_cw(s - 1, elem_geo.h-slider_lo.gap, s + 1, elem_geo.h);
  515. end
  516. end
  517. end
  518. end
  519. end
  520. end
  521. element.static_ass = static_ass
  522. -- if the element is supposed to be disabled,
  523. -- style it accordingly and kill the eventresponders
  524. if not (element.enabled) then
  525. element.layout.alpha[1] = 136
  526. element.eventresponder = nil
  527. end
  528. -- gray out the element if it is toggled off
  529. if (element.off) then
  530. element.layout.alpha[1] = 136
  531. end
  532. end
  533. end
  534. --
  535. -- Element Rendering
  536. --
  537. -- returns nil or a chapter element from the native property chapter-list
  538. function get_chapter(possec)
  539. local cl = state.chapter_list -- sorted, get latest before possec, if any
  540. for n=#cl,1,-1 do
  541. if possec >= cl[n].time then
  542. return cl[n]
  543. end
  544. end
  545. end
  546. function render_elements(master_ass)
  547. -- when the slider is dragged or hovered and we have a target chapter name
  548. -- then we use it instead of the normal title. we calculate it before the
  549. -- render iterations because the title may be rendered before the slider.
  550. state.forced_title = nil
  551. local se, ae = state.slider_element, elements[state.active_element]
  552. if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then
  553. local dur = mp.get_property_number("duration", 0)
  554. if dur > 0 then
  555. local possec = get_slider_value(se) * dur / 100 -- of mouse pos
  556. local ch = get_chapter(possec)
  557. if ch and ch.title and ch.title ~= "" then
  558. state.forced_title = string.format(user_opts.chapter_fmt, ch.title)
  559. end
  560. end
  561. end
  562. for n=1, #elements do
  563. local element = elements[n]
  564. local style_ass = assdraw.ass_new()
  565. style_ass:merge(element.style_ass)
  566. ass_append_alpha(style_ass, element.layout.alpha, 0)
  567. if element.eventresponder and (state.active_element == n) then
  568. -- run render event functions
  569. if not (element.eventresponder.render == nil) then
  570. element.eventresponder.render(element)
  571. end
  572. if mouse_hit(element) then
  573. -- mouse down styling
  574. if (element.styledown) then
  575. style_ass:append(osc_styles.elementDown)
  576. end
  577. if (element.softrepeat) and (state.mouse_down_counter >= 15
  578. and state.mouse_down_counter % 5 == 0) then
  579. element.eventresponder[state.active_event_source..'_down'](element)
  580. end
  581. state.mouse_down_counter = state.mouse_down_counter + 1
  582. end
  583. end
  584. if user_opts.keyboardnavigation and state.highlight_element == element.name then
  585. style_ass:append(osc_styles.elementHighlight)
  586. end
  587. local elem_ass = assdraw.ass_new()
  588. elem_ass:merge(style_ass)
  589. if not (element.type == 'button') then
  590. elem_ass:merge(element.static_ass)
  591. end
  592. if (element.type == 'slider') then
  593. local slider_lo = element.layout.slider
  594. local elem_geo = element.layout.geometry
  595. local s_min = element.slider.min.value
  596. local s_max = element.slider.max.value
  597. -- draw pos marker
  598. local pos = element.slider.posF()
  599. local seekRanges = element.slider.seekRangesF()
  600. local rh = user_opts.seekbarhandlesize * elem_geo.h / 2 -- Handle radius
  601. local xp
  602. if pos then
  603. xp = get_slider_ele_pos_for(element, pos)
  604. ass_draw_cir_cw(elem_ass, xp, elem_geo.h/2, rh)
  605. elem_ass:rect_cw(0, slider_lo.gap, xp, elem_geo.h - slider_lo.gap)
  606. end
  607. if seekRanges then
  608. elem_ass:draw_stop()
  609. elem_ass:merge(element.style_ass)
  610. ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
  611. elem_ass:merge(element.static_ass)
  612. for _,range in pairs(seekRanges) do
  613. local pstart = get_slider_ele_pos_for(element, range['start'])
  614. local pend = get_slider_ele_pos_for(element, range['end'])
  615. elem_ass:rect_cw(pstart - rh, slider_lo.gap, pend + rh, elem_geo.h - slider_lo.gap)
  616. end
  617. end
  618. elem_ass:draw_stop()
  619. -- add tooltip
  620. if not (element.slider.tooltipF == nil) then
  621. if mouse_hit(element) then
  622. local sliderpos = get_slider_value(element)
  623. local tooltiplabel = element.slider.tooltipF(sliderpos)
  624. local an = slider_lo.tooltip_an
  625. local ty
  626. if (an == 2) then
  627. ty = element.hitbox.y1
  628. else
  629. ty = element.hitbox.y1 + elem_geo.h/2
  630. end
  631. local tx = get_virt_mouse_pos()
  632. if (slider_lo.adjust_tooltip) then
  633. if (an == 2) then
  634. if (sliderpos < (s_min + 3)) then
  635. an = an - 1
  636. elseif (sliderpos > (s_max - 3)) then
  637. an = an + 1
  638. end
  639. elseif (sliderpos > (s_max-s_min)/2) then
  640. an = an + 1
  641. tx = tx - 5
  642. else
  643. an = an - 1
  644. tx = tx + 10
  645. end
  646. end
  647. -- tooltip label
  648. elem_ass:new_event()
  649. elem_ass:pos(tx, ty)
  650. elem_ass:an(an)
  651. elem_ass:append(slider_lo.tooltip_style)
  652. ass_append_alpha(elem_ass, slider_lo.alpha, 0)
  653. elem_ass:append(tooltiplabel)
  654. end
  655. end
  656. elseif (element.type == 'button') then
  657. local buttontext
  658. if type(element.content) == 'function' then
  659. buttontext = element.content() -- function objects
  660. elseif not (element.content == nil) then
  661. buttontext = element.content -- text objects
  662. end
  663. buttontext = buttontext:gsub(':%((.?.?.?)%) unknown ', ':%(%1%)') --gsub('%) unknown %(\'', '')
  664. local maxchars = element.layout.button.maxchars
  665. -- 认为1个中文字符约等于1.5个英文字符
  666. -- local charcount = buttontext:len()- (buttontext:len()-select(2, buttontext:gsub('[^\128-\193]', '')))/1.5
  667. local charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3
  668. if not (maxchars == nil) and (charcount > maxchars) then
  669. local limit = math.max(0, maxchars - 3)
  670. if (charcount > limit) then
  671. while (charcount > limit) do
  672. buttontext = buttontext:gsub('.[\128-\191]*$', '')
  673. charcount = (buttontext:len() + select(2, buttontext:gsub('[^\128-\193]', ''))*2) / 3
  674. end
  675. buttontext = buttontext .. '...'
  676. end
  677. end
  678. elem_ass:append(buttontext)
  679. -- add tooltip
  680. if not (element.tooltipF == nil) and element.enabled then
  681. if mouse_hit(element) then
  682. local tooltiplabel = element.tooltipF
  683. local an = 1
  684. local ty = element.hitbox.y1
  685. local tx = get_virt_mouse_pos()
  686. if ty < osc_param.playresy / 2 then
  687. ty = element.hitbox.y2
  688. an = 7
  689. end
  690. -- tooltip label
  691. if type(element.tooltipF) == 'function' then
  692. tooltiplabel = element.tooltipF()
  693. else
  694. tooltiplabel = element.tooltipF
  695. end
  696. elem_ass:new_event()
  697. elem_ass:pos(tx, ty)
  698. elem_ass:an(an)
  699. elem_ass:append(element.tooltip_style)
  700. elem_ass:append(tooltiplabel)
  701. end
  702. end
  703. end
  704. master_ass:merge(elem_ass)
  705. end
  706. end
  707. --
  708. -- Message display
  709. --
  710. -- pos is 1 based
  711. function limited_list(prop, pos)
  712. local proplist = mp.get_property_native(prop, {})
  713. local count = #proplist
  714. if count == 0 then
  715. return count, proplist
  716. end
  717. local fs = tonumber(mp.get_property('options/osd-font-size'))
  718. local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
  719. if max % 2 == 0 then
  720. max = max - 1
  721. end
  722. local delta = math.ceil(max / 2) - 1
  723. local begi = math.max(math.min(pos - delta, count - max + 1), 1)
  724. local endi = math.min(begi + max - 1, count)
  725. local reslist = {}
  726. for i=begi, endi do
  727. local item = proplist[i]
  728. item.current = (i == pos) and true or nil
  729. table.insert(reslist, item)
  730. end
  731. return count, reslist
  732. end
  733. function get_playlist()
  734. local pos = mp.get_property_number('playlist-pos', 0) + 1
  735. local count, limlist = limited_list('playlist', pos)
  736. if count == 0 then
  737. return texts.nolist
  738. end
  739. local message = string.format(texts.playlist .. ' [%d/%d]:\n', pos, count)
  740. for i, v in ipairs(limlist) do
  741. local title = v.title
  742. local _, filename = utils.split_path(v.filename)
  743. if title == nil then
  744. title = filename
  745. end
  746. message = string.format('%s %s %s\n', message,
  747. (v.current and '●' or '○'), title)
  748. end
  749. return message
  750. end
  751. function get_chapterlist()
  752. local pos = mp.get_property_number('chapter', 0) + 1
  753. local count, limlist = limited_list('chapter-list', pos)
  754. if count == 0 then
  755. return texts.nochapter
  756. end
  757. local message = string.format(texts.chapter.. ' [%d/%d]:\n', pos, count)
  758. for i, v in ipairs(limlist) do
  759. local time = mp.format_time(v.time)
  760. local title = v.title
  761. if title == nil then
  762. title = string.format(texts.chapter .. ' %02d', i)
  763. end
  764. message = string.format('%s[%s] %s %s\n', message, time,
  765. (v.current and '●' or '○'), title)
  766. end
  767. return message
  768. end
  769. function show_message(text, duration)
  770. --print('text: '..text..' duration: ' .. duration)
  771. if duration == nil then
  772. duration = tonumber(mp.get_property('options/osd-duration')) / 1000
  773. elseif not type(duration) == 'number' then
  774. print('duration: ' .. duration)
  775. end
  776. -- cut the text short, otherwise the following functions
  777. -- may slow down massively on huge input
  778. text = string.sub(text, 0, 4000)
  779. -- replace actual linebreaks with ASS linebreaks
  780. text = string.gsub(text, '\n', '\\N')
  781. state.message_text = text
  782. if not state.message_hide_timer then
  783. state.message_hide_timer = mp.add_timeout(0, request_tick)
  784. end
  785. state.message_hide_timer:kill()
  786. state.message_hide_timer.timeout = duration
  787. state.message_hide_timer:resume()
  788. request_tick()
  789. end
  790. function render_message(ass)
  791. if state.message_hide_timer and state.message_hide_timer:is_enabled() and
  792. state.message_text
  793. then
  794. local _, lines = string.gsub(state.message_text, '\\N', '')
  795. local fontsize = tonumber(mp.get_property('options/osd-font-size'))
  796. local outline = tonumber(mp.get_property('options/osd-border-size'))
  797. local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
  798. local counterscale = osc_param.playresy / osc_param.unscaled_y
  799. fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
  800. outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
  801. local style = '{\\bord' .. outline .. '\\fs' .. fontsize .. '}'
  802. ass:new_event()
  803. ass:append(style .. state.message_text)
  804. else
  805. state.message_text = nil
  806. end
  807. end
  808. --
  809. -- Initialisation and Layout
  810. --
  811. function new_element(name, type)
  812. elements[name] = {}
  813. elements[name].type = type
  814. elements[name].name = name
  815. -- add default stuff
  816. elements[name].eventresponder = {}
  817. elements[name].visible = true
  818. elements[name].enabled = true
  819. elements[name].softrepeat = false
  820. elements[name].styledown = (type == 'button')
  821. elements[name].state = {}
  822. if (type == 'slider') then
  823. elements[name].slider = {min = {value = 0}, max = {value = 100}}
  824. end
  825. return elements[name]
  826. end
  827. function add_layout(name)
  828. if not (elements[name] == nil) then
  829. -- new layout
  830. elements[name].layout = {}
  831. -- set layout defaults
  832. elements[name].layout.layer = 50
  833. elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
  834. if (elements[name].type == 'button') then
  835. elements[name].layout.button = {
  836. maxchars = nil,
  837. }
  838. elseif (elements[name].type == 'slider') then
  839. -- slider defaults
  840. elements[name].layout.slider = {
  841. border = 1,
  842. gap = 1,
  843. nibbles_top = true,
  844. nibbles_bottom = true,
  845. adjust_tooltip = true,
  846. tooltip_style = '',
  847. tooltip_an = 2,
  848. alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
  849. }
  850. elseif (elements[name].type == 'box') then
  851. elements[name].layout.box = {radius = 0, hexagon = false}
  852. end
  853. return elements[name].layout
  854. else
  855. msg.error('Can\'t add_layout to element \''..name..'\', doesn\'t exist.')
  856. end
  857. end
  858. -- Window Controls
  859. function window_controls()
  860. local wc_geo = {
  861. x = 0,
  862. y = 32,
  863. an = 1,
  864. w = osc_param.playresx,
  865. h = 32,
  866. }
  867. local controlbox_w = window_control_box_width
  868. local titlebox_w = wc_geo.w - controlbox_w
  869. -- Default alignment is 'right'
  870. local controlbox_left = wc_geo.w - controlbox_w
  871. local titlebox_left = wc_geo.x
  872. local titlebox_right = wc_geo.w - controlbox_w
  873. add_area('window-controls',
  874. get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
  875. controlbox_w, wc_geo.h))
  876. local lo
  877. local button_y = wc_geo.y - (wc_geo.h / 2)
  878. local first_geo =
  879. {x = controlbox_left + 27, y = button_y, an = 5, w = 40, h = wc_geo.h}
  880. local second_geo =
  881. {x = controlbox_left + 69, y = button_y, an = 5, w = 40, h = wc_geo.h}
  882. local third_geo =
  883. {x = controlbox_left + 115, y = button_y, an = 5, w = 40, h = wc_geo.h}
  884. -- Window control buttons use symbols in the custom mpv osd font
  885. -- because the official unicode codepoints are sufficiently
  886. -- exotic that a system might lack an installed font with them,
  887. -- and libass will complain that they are not present in the
  888. -- default font, even if another font with them is available.
  889. -- Close: ??
  890. ne = new_element('close', 'button')
  891. ne.content = '\238\132\149'
  892. ne.eventresponder['mbtn_left_up'] =
  893. function () mp.commandv('quit') end
  894. lo = add_layout('close')
  895. lo.geometry = third_geo
  896. lo.style = osc_styles.WinCtrl
  897. lo.alpha[3] = 0
  898. -- Minimize: ??
  899. ne = new_element('minimize', 'button')
  900. ne.content = '\\n\238\132\146'
  901. ne.eventresponder['mbtn_left_up'] =
  902. function () mp.commandv('cycle', 'window-minimized') end
  903. lo = add_layout('minimize')
  904. lo.geometry = first_geo
  905. lo.style = osc_styles.WinCtrl
  906. lo.alpha[3] = 0
  907. -- Maximize: ?? /??
  908. ne = new_element('maximize', 'button')
  909. if state.maximized or state.fullscreen then
  910. ne.content = '\238\132\148'
  911. else
  912. ne.content = '\238\132\147'
  913. end
  914. ne.eventresponder['mbtn_left_up'] =
  915. function ()
  916. if state.fullscreen then
  917. mp.commandv('cycle', 'fullscreen')
  918. else
  919. mp.commandv('cycle', 'window-maximized')
  920. end
  921. end
  922. lo = add_layout('maximize')
  923. lo.geometry = second_geo
  924. lo.style = osc_styles.WinCtrl
  925. lo.alpha[3] = 0
  926. end
  927. --
  928. -- Layouts
  929. --
  930. local layouts = {}
  931. -- Default layout
  932. layouts = function ()
  933. local osc_geo = {w, h}
  934. osc_geo.w = osc_param.playresx
  935. osc_geo.h = 180
  936. -- origin of the controllers, left/bottom corner
  937. local posX = 0
  938. local posY = osc_param.playresy
  939. osc_param.areas = {} -- delete areas
  940. -- area for active mouse input
  941. add_area('input', get_hitbox_coords(posX, posY, 1, osc_geo.w, 113))
  942. -- area for show/hide
  943. add_area('showhide', 0, 0, osc_param.playresx, osc_param.playresy)
  944. -- fetch values
  945. local osc_w, osc_h=
  946. osc_geo.w, osc_geo.h
  947. --
  948. -- Controller Background
  949. --
  950. local lo
  951. new_element('TransBg', 'box')
  952. lo = add_layout('TransBg')
  953. lo.geometry = {x = posX, y = posY, an = 7, w = osc_w, h = 1}
  954. lo.style = osc_styles.TransBg
  955. lo.layer = 10
  956. lo.alpha[3] = 0
  957. --
  958. -- Alignment
  959. --
  960. local refX = osc_w / 2
  961. local refY = posY
  962. local geo
  963. --
  964. -- Seekbar
  965. --
  966. new_element('bgbar1', 'box')
  967. lo = add_layout('bgbar1')
  968. lo.geometry = {x = refX , y = refY - 105 , an = 5, w = osc_geo.w - 50, h = 4}
  969. lo.layer = 13
  970. lo.style = osc_styles.SeekbarBg
  971. lo.alpha[1] = 128
  972. lo.alpha[3] = 128
  973. lo = add_layout('seekbar')
  974. lo.geometry = {x = refX, y = refY - 105 , an = 5, w = osc_geo.w - 50, h = 20}
  975. lo.style = osc_styles.SeekbarFg
  976. lo.slider.gap = 7
  977. lo.slider.tooltip_style = osc_styles.Tooltip
  978. lo.slider.tooltip_an = 2
  979. local showjump = user_opts.showjump
  980. local offset = showjump and 60 or 0
  981. -- buttons
  982. lo = add_layout('pl_prev')
  983. lo.geometry = {x = refX - 120 - offset, y = refY - 40 , an = 5, w = 30, h = 24}
  984. lo.style = osc_styles.Ctrl2
  985. lo = add_layout('skipback')
  986. lo.geometry = {x = refX - 60 - offset, y = refY - 40 , an = 5, w = 30, h = 24}
  987. lo.style = osc_styles.Ctrl2
  988. if showjump then
  989. lo = add_layout('jumpback')
  990. lo.geometry = {x = refX - 60, y = refY - 40 , an = 5, w = 30, h = 24}
  991. lo.style = osc_styles.Ctrl2
  992. end
  993. lo = add_layout('playpause')
  994. lo.geometry = {x = refX, y = refY - 40 , an = 5, w = 45, h = 45}
  995. lo.style = osc_styles.Ctrl1
  996. if showjump then
  997. lo = add_layout('jumpfrwd')
  998. lo.geometry = {x = refX + 60, y = refY - 40 , an = 5, w = 30, h = 24}
  999. -- HACK: jumpfrwd's icon must be mirrored for nonstandard # of seconds
  1000. -- as the font only has an icon without a number for rewinding
  1001. lo.style = (user_opts.jumpiconnumber and jumpicons[user_opts.jumpamount] ~= nil) and osc_styles.Ctrl2 or osc_styles.Ctrl2Flip
  1002. end
  1003. lo = add_layout('skipfrwd')
  1004. lo.geometry = {x = refX + 60 + offset, y = refY - 40 , an = 5, w = 30, h = 24}
  1005. lo.style = osc_styles.Ctrl2
  1006. lo = add_layout('pl_next')
  1007. lo.geometry = {x = refX + 120 + offset, y = refY - 40 , an = 5, w = 30, h = 24}
  1008. lo.style = osc_styles.Ctrl2
  1009. -- Time
  1010. lo = add_layout('tc_left')
  1011. lo.geometry = {x = 25, y = refY - 84, an = 7, w = 64, h = 20}
  1012. lo.style = osc_styles.Time
  1013. lo = add_layout('tc_right')
  1014. lo.geometry = {x = osc_geo.w - 25 , y = refY -84, an = 9, w = 64, h = 20}
  1015. lo.style = osc_styles.Time
  1016. lo = add_layout('cy_audio')
  1017. lo.geometry = {x = 37, y = refY - 40, an = 5, w = 24, h = 24}
  1018. lo.style = osc_styles.Ctrl3
  1019. lo = add_layout('cy_sub')
  1020. lo.geometry = {x = 87, y = refY - 40, an = 5, w = 24, h = 24}
  1021. lo.style = osc_styles.Ctrl3
  1022. lo = add_layout('tog_fs')
  1023. lo.geometry = {x = osc_geo.w - 37, y = refY - 40, an = 5, w = 24, h = 24}
  1024. lo.style = osc_styles.Ctrl3
  1025. lo = add_layout('tog_info')
  1026. lo.geometry = {x = osc_geo.w - 87, y = refY - 40, an = 5, w = 24, h = 24}
  1027. lo.style = osc_styles.Ctrl3
  1028. geo = { x = 25, y = refY - 132, an = 1, w = osc_geo.w - 50, h = 48 }
  1029. lo = add_layout('title')
  1030. lo.geometry = geo
  1031. lo.style = string.format('%s{\\clip(%f,%f,%f,%f)}', osc_styles.Title,
  1032. geo.x, geo.y - geo.h, geo.x + geo.w , geo.y)
  1033. lo.alpha[3] = 0
  1034. end
  1035. -- Validate string type user options
  1036. function validate_user_opts()
  1037. if user_opts.windowcontrols ~= 'auto' and
  1038. user_opts.windowcontrols ~= 'yes' and
  1039. user_opts.windowcontrols ~= 'no' then
  1040. msg.warn('windowcontrols cannot be \'' ..
  1041. user_opts.windowcontrols .. '\'. Ignoring.')
  1042. user_opts.windowcontrols = 'auto'
  1043. end
  1044. end
  1045. function update_options(list)
  1046. validate_user_opts()
  1047. request_tick()
  1048. visibility_mode(user_opts.visibility, true)
  1049. update_duration_watch()
  1050. request_init()
  1051. end
  1052. -- OSC INIT
  1053. function osc_init()
  1054. msg.debug('osc_init')
  1055. -- set canvas resolution according to display aspect and scaling setting
  1056. local baseResY = 720
  1057. local display_w, display_h, display_aspect = mp.get_osd_size()
  1058. local scale = 1
  1059. if (mp.get_property('video') == 'no') then -- dummy/forced window
  1060. scale = user_opts.scaleforcedwindow
  1061. elseif state.fullscreen then
  1062. scale = user_opts.scalefullscreen
  1063. else
  1064. scale = user_opts.scalewindowed
  1065. end
  1066. if user_opts.vidscale then
  1067. osc_param.unscaled_y = baseResY
  1068. else
  1069. osc_param.unscaled_y = display_h
  1070. end
  1071. osc_param.playresy = osc_param.unscaled_y / scale
  1072. if (display_aspect > 0) then
  1073. osc_param.display_aspect = display_aspect
  1074. end
  1075. osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1076. -- stop seeking with the slider to prevent skipping files
  1077. state.active_element = nil
  1078. elements = {}
  1079. -- some often needed stuff
  1080. local pl_count = mp.get_property_number('playlist-count', 0)
  1081. local have_pl = (pl_count > 1)
  1082. local pl_pos = mp.get_property_number('playlist-pos', 0) + 1
  1083. local have_ch = (mp.get_property_number('chapters', 0) > 0)
  1084. local loop = mp.get_property('loop-playlist', 'no')
  1085. local ne
  1086. -- playlist buttons
  1087. -- prev
  1088. ne = new_element('pl_prev', 'button')
  1089. ne.content = '\xEF\x8E\xB5'
  1090. ne.enabled = (pl_pos > 1) or (loop ~= 'no')
  1091. ne.eventresponder['mbtn_left_up'] =
  1092. function ()
  1093. mp.commandv('playlist-prev', 'weak')
  1094. end
  1095. ne.eventresponder['mbtn_right_up'] =
  1096. function () show_message(get_playlist()) end
  1097. --next
  1098. ne = new_element('pl_next', 'button')
  1099. ne.content = '\xEF\x8E\xB4'
  1100. ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= 'no')
  1101. ne.eventresponder['mbtn_left_up'] =
  1102. function ()
  1103. mp.commandv('playlist-next', 'weak')
  1104. end
  1105. ne.eventresponder['mbtn_right_up'] =
  1106. function () show_message(get_playlist()) end
  1107. --play control buttons
  1108. --playpause
  1109. ne = new_element('playpause', 'button')
  1110. ne.content = function ()
  1111. if mp.get_property('pause') == 'yes' then
  1112. return ('\xEF\x8E\xAA')
  1113. else
  1114. return ('\xEF\x8E\xA7')
  1115. end
  1116. end
  1117. ne.eventresponder['mbtn_left_up'] =
  1118. function () mp.commandv('cycle', 'pause') end
  1119. --ne.eventresponder['mbtn_right_up'] =
  1120. -- function () mp.commandv('script-binding', 'open-file-dialog') end
  1121. if user_opts.showjump then
  1122. local jumpamount = user_opts.jumpamount
  1123. local jumpmode = user_opts.jumpmode
  1124. local icons = jumpicons.default
  1125. if user_opts.jumpiconnumber then
  1126. icons = jumpicons[jumpamount] or jumpicons.default
  1127. end
  1128. --jumpback
  1129. ne = new_element('jumpback', 'button')
  1130. ne.softrepeat = true
  1131. ne.content = icons[1]
  1132. ne.eventresponder['mbtn_left_down'] =
  1133. --function () mp.command('seek -5') end
  1134. function () mp.commandv('seek', -jumpamount, jumpmode) end
  1135. ne.eventresponder['shift+mbtn_left_down'] =
  1136. function () mp.commandv('frame-back-step') end
  1137. ne.eventresponder['mbtn_right_down'] =
  1138. --function () mp.command('seek -60') end
  1139. function () mp.commandv('seek', -60, jumpmode) end
  1140. ne.eventresponder['enter'] =
  1141. --function () mp.command('seek -5') end
  1142. function () mp.commandv('seek', -jumpamount, jumpmode) end
  1143. --jumpfrwd
  1144. ne = new_element('jumpfrwd', 'button')
  1145. ne.softrepeat = true
  1146. ne.content = icons[2]
  1147. ne.eventresponder['mbtn_left_down'] =
  1148. --function () mp.command('seek +5') end
  1149. function () mp.commandv('seek', jumpamount, jumpmode) end
  1150. ne.eventresponder['shift+mbtn_left_down'] =
  1151. function () mp.commandv('frame-step') end
  1152. ne.eventresponder['mbtn_right_down'] =
  1153. --function () mp.command('seek +60') end
  1154. function () mp.commandv('seek', 60, jumpmode) end
  1155. ne.eventresponder['enter'] =
  1156. --function () mp.command('seek +5') end
  1157. function () mp.commandv('seek', jumpamount, jumpmode) end
  1158. end
  1159. --skipback
  1160. ne = new_element('skipback', 'button')
  1161. ne.softrepeat = true
  1162. ne.content = '\xEF\x8E\xA0'
  1163. ne.enabled = (have_ch) -- disables button when no chapters available.
  1164. ne.eventresponder['mbtn_left_down'] =
  1165. --function () mp.command('seek -5') end
  1166. --function () mp.commandv('seek', -5, 'relative', 'keyframes') end
  1167. function () mp.commandv("add", "chapter", -1) end
  1168. --ne.eventresponder['shift+mbtn_left_down'] =
  1169. --function () mp.commandv('frame-back-step') end
  1170. ne.eventresponder['mbtn_right_down'] =
  1171. function () show_message(get_chapterlist()) end
  1172. --function () mp.command('seek -60') end
  1173. --function () mp.commandv('seek', -60, 'relative', 'keyframes') end
  1174. ne.eventresponder['enter'] =
  1175. --function () mp.command('seek -5') end
  1176. --function () mp.commandv('seek', -5, 'relative', 'keyframes') end
  1177. function () mp.commandv("add", "chapter", -1) end
  1178. --skipfrwd
  1179. ne = new_element('skipfrwd', 'button')
  1180. ne.softrepeat = true
  1181. ne.content = '\xEF\x8E\x9F'
  1182. ne.enabled = (have_ch) -- disables button when no chapters available.
  1183. ne.eventresponder['mbtn_left_down'] =
  1184. --function () mp.command('seek +5') end
  1185. --function () mp.commandv('seek', 5, 'relative', 'keyframes') end
  1186. function () mp.commandv("add", "chapter", 1) end
  1187. --ne.eventresponder['shift+mbtn_left_down'] =
  1188. --function () mp.commandv('frame-step') end
  1189. ne.eventresponder['mbtn_right_down'] =
  1190. function () show_message(get_chapterlist()) end
  1191. --function () mp.command('seek +60') end
  1192. --function () mp.commandv('seek', 60, 'relative', 'keyframes') end
  1193. ne.eventresponder['enter'] =
  1194. --function () mp.command('seek +5') end
  1195. --function () mp.commandv('seek', 5, 'relative', 'keyframes') end
  1196. function () mp.commandv("add", "chapter", 1) end
  1197. --
  1198. update_tracklist()
  1199. --cy_audio
  1200. ne = new_element('cy_audio', 'button')
  1201. ne.enabled = (#tracks_osc.audio > 0)
  1202. ne.off = (get_track('audio') == 0)
  1203. ne.visible = (osc_param.playresx >= 540)
  1204. ne.content = '\xEF\x8E\xB7'
  1205. ne.tooltip_style = osc_styles.Tooltip
  1206. ne.tooltipF = function ()
  1207. local msg = texts.off
  1208. if not (get_track('audio') == 0) then
  1209. msg = (texts.audio .. ' [' .. get_track('audio') .. ' ∕ ' .. #tracks_osc.audio .. '] ')
  1210. local prop = mp.get_property('current-tracks/audio/title') --('current-tracks/audio/lang')
  1211. if not prop then
  1212. prop = texts.na
  1213. end
  1214. msg = msg .. '[' .. prop .. ']'
  1215. prop = mp.get_property('current-tracks/audio/lang') --('current-tracks/audio/title')
  1216. if prop then
  1217. msg = msg .. ' ' .. prop
  1218. end
  1219. return msg
  1220. end
  1221. return msg
  1222. end
  1223. ne.eventresponder['mbtn_left_up'] =
  1224. function () set_track('audio', 1) end
  1225. ne.eventresponder['mbtn_right_up'] =
  1226. function () set_track('audio', -1) end
  1227. ne.eventresponder['mbtn_mid_up'] =
  1228. function () show_message(get_tracklist('audio')) end
  1229. ne.eventresponder['enter'] =
  1230. function () set_track('audio', 1); show_message(get_tracklist('audio')) end
  1231. --cy_sub
  1232. ne = new_element('cy_sub', 'button')
  1233. ne.enabled = (#tracks_osc.sub > 0)
  1234. ne.off = (get_track('sub') == 0)
  1235. ne.visible = (osc_param.playresx >= 600)
  1236. ne.content = '\xEF\x8F\x93'
  1237. ne.tooltip_style = osc_styles.Tooltip
  1238. ne.tooltipF = function ()
  1239. local msg = texts.off
  1240. if not (get_track('sub') == 0) then
  1241. msg = (texts.subtitle .. ' [' .. get_track('sub') .. ' ∕ ' .. #tracks_osc.sub .. '] ')
  1242. local prop = mp.get_property('current-tracks/sub/lang')
  1243. if not prop then
  1244. prop = texts.na
  1245. end
  1246. msg = msg .. '[' .. prop .. ']'
  1247. prop = mp.get_property('current-tracks/sub/title')
  1248. if prop then
  1249. msg = msg .. ' ' .. prop
  1250. end
  1251. return msg
  1252. end
  1253. return msg
  1254. end
  1255. ne.eventresponder['mbtn_left_up'] =
  1256. function () set_track('sub', 1) end
  1257. ne.eventresponder['mbtn_right_up'] =
  1258. function () set_track('sub', -1) end
  1259. ne.eventresponder['mbtn_mid_up'] =
  1260. function () show_message(get_tracklist('sub')) end
  1261. ne.eventresponder['enter'] =
  1262. function () set_track('sub', 1); show_message(get_tracklist('sub')) end
  1263. --tog_fs
  1264. ne = new_element('tog_fs', 'button')
  1265. ne.content = function ()
  1266. if (state.fullscreen) then
  1267. return ('\xEF\x85\xAC')
  1268. else
  1269. return ('\xEF\x85\xAD')
  1270. end
  1271. end
  1272. ne.visible = (osc_param.playresx >= 540)
  1273. ne.eventresponder['mbtn_left_up'] =
  1274. function () mp.commandv("cycle", "fullscreen") end
  1275. --tog_info
  1276. ne = new_element('tog_info', 'button')
  1277. ne.content = ''
  1278. ne.visible = (osc_param.playresx >= 600)
  1279. ne.eventresponder['mbtn_left_up'] =
  1280. function () mp.commandv('script-binding', 'stats/display-stats-toggle') end
  1281. -- title
  1282. ne = new_element('title', 'button')
  1283. ne.content = function ()
  1284. local title = state.forced_title or
  1285. mp.command_native({"expand-text", user_opts.title})
  1286. if state.paused then
  1287. title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{')
  1288. else
  1289. title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{') --title = ' '
  1290. end
  1291. return not (title == '') and title or ' '
  1292. end
  1293. ne.visible = osc_param.playresy >= 320 and user_opts.showtitle
  1294. --seekbar
  1295. ne = new_element('seekbar', 'slider')
  1296. ne.enabled = not (mp.get_property('percent-pos') == nil)
  1297. state.slider_element = ne.enabled and ne or nil -- used for forced_title
  1298. ne.slider.markerF = function ()
  1299. local duration = mp.get_property_number('duration', nil)
  1300. if not (duration == nil) then
  1301. local chapters = mp.get_property_native('chapter-list', {})
  1302. local markers = {}
  1303. for n = 1, #chapters do
  1304. markers[n] = (chapters[n].time / duration * 100)
  1305. end
  1306. return markers
  1307. else
  1308. return {}
  1309. end
  1310. end
  1311. ne.slider.posF =
  1312. function () return mp.get_property_number('percent-pos', nil) end
  1313. ne.slider.tooltipF = function (pos)
  1314. local duration = mp.get_property_number('duration', nil)
  1315. if not ((duration == nil) or (pos == nil)) then
  1316. possec = duration * (pos / 100)
  1317. return mp.format_time(possec)
  1318. else
  1319. return ''
  1320. end
  1321. end
  1322. ne.slider.seekRangesF = function()
  1323. if not user_opts.seekrange then
  1324. return nil
  1325. end
  1326. local cache_state = state.cache_state
  1327. if not cache_state then
  1328. return nil
  1329. end
  1330. local duration = mp.get_property_number('duration', nil)
  1331. if (duration == nil) or duration <= 0 then
  1332. return nil
  1333. end
  1334. local ranges = cache_state['seekable-ranges']
  1335. if #ranges == 0 then
  1336. return nil
  1337. end
  1338. local nranges = {}
  1339. for _, range in pairs(ranges) do
  1340. nranges[#nranges + 1] = {
  1341. ['start'] = 100 * range['start'] / duration,
  1342. ['end'] = 100 * range['end'] / duration,
  1343. }
  1344. end
  1345. return nranges
  1346. end
  1347. ne.eventresponder['mouse_move'] = --keyframe seeking when mouse is dragged
  1348. function (element)
  1349. if not element.state.mbtnleft then return end -- allow drag for mbtnleft only!
  1350. -- mouse move events may pile up during seeking and may still get
  1351. -- sent when the user is done seeking, so we need to throw away
  1352. -- identical seeks
  1353. local seekto = get_slider_value(element)
  1354. if (element.state.lastseek == nil) or
  1355. (not (element.state.lastseek == seekto)) then
  1356. local flags = 'absolute-percent'
  1357. if not user_opts.seekbarkeyframes then
  1358. flags = flags .. '+exact'
  1359. end
  1360. mp.commandv('seek', seekto, flags)
  1361. element.state.lastseek = seekto
  1362. end
  1363. end
  1364. ne.eventresponder['mbtn_left_down'] = --exact seeks on single clicks
  1365. function (element)
  1366. mp.commandv('seek', get_slider_value(element), 'absolute-percent', 'exact')
  1367. element.state.mbtnleft = true
  1368. end
  1369. ne.eventresponder['mbtn_left_up'] =
  1370. function (element) element.state.mbtnleft = false end
  1371. ne.eventresponder['mbtn_right_down'] = --seeks to chapter start
  1372. function (element)
  1373. local duration = mp.get_property_number('duration', nil)
  1374. if not (duration == nil) then
  1375. local chapters = mp.get_property_native('chapter-list', {})
  1376. if #chapters > 0 then
  1377. local pos = get_slider_value(element)
  1378. local ch = #chapters
  1379. for n = 1, ch do
  1380. if chapters[n].time / duration * 100 >= pos then
  1381. ch = n - 1
  1382. break
  1383. end
  1384. end
  1385. mp.commandv('set', 'chapter', ch - 1)
  1386. --if chapters[ch].title then show_message(chapters[ch].time) end
  1387. end
  1388. end
  1389. end
  1390. ne.eventresponder['reset'] =
  1391. function (element) element.state.lastseek = nil end
  1392. -- tc_left (current pos)
  1393. ne = new_element('tc_left', 'button')
  1394. ne.content = function ()
  1395. if (state.fulltime) then
  1396. return (mp.get_property_osd('playback-time/full'))
  1397. else
  1398. return (mp.get_property_osd('playback-time'))
  1399. end
  1400. end
  1401. ne.eventresponder["mbtn_left_up"] = function ()
  1402. state.fulltime = not state.fulltime
  1403. request_init()
  1404. end
  1405. -- tc_right (total/remaining time)
  1406. ne = new_element('tc_right', 'button')
  1407. ne.content = function ()
  1408. if (mp.get_property_number('duration', 0) <= 0) then return '--:--:--' end
  1409. if (state.rightTC_trem) then
  1410. if (state.fulltime) then
  1411. return ('-'..mp.get_property_osd('playtime-remaining/full'))
  1412. else
  1413. return ('-'..mp.get_property_osd('playtime-remaining'))
  1414. end
  1415. else
  1416. if (state.fulltime) then
  1417. return (mp.get_property_osd('duration/full'))
  1418. else
  1419. return (mp.get_property_osd('duration'))
  1420. end
  1421. end
  1422. end
  1423. ne.eventresponder['mbtn_left_up'] =
  1424. function () state.rightTC_trem = not state.rightTC_trem end
  1425. -- load layout
  1426. layouts()
  1427. -- load window controls
  1428. if window_controls_enabled() then
  1429. window_controls()
  1430. end
  1431. --do something with the elements
  1432. prepare_elements()
  1433. end
  1434. function shutdown()
  1435. end
  1436. --
  1437. -- Other important stuff
  1438. --
  1439. function show_osc()
  1440. -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
  1441. if not state.enabled then return end
  1442. msg.trace('show_osc')
  1443. --remember last time of invocation (mouse move)
  1444. state.showtime = mp.get_time()
  1445. osc_visible(true)
  1446. if user_opts.keyboardnavigation == true then
  1447. osc_enable_key_bindings()
  1448. end
  1449. if (user_opts.fadeduration > 0) then
  1450. state.anitype = nil
  1451. end
  1452. end
  1453. function hide_osc()
  1454. msg.trace('hide_osc')
  1455. if not state.enabled then
  1456. -- typically hide happens at render() from tick(), but now tick() is
  1457. -- no-op and won't render again to remove the osc, so do that manually.
  1458. state.osc_visible = false
  1459. render_wipe()
  1460. if user_opts.keyboardnavigation == true then
  1461. osc_disable_key_bindings()
  1462. end
  1463. elseif (user_opts.fadeduration > 0) then
  1464. if not(state.osc_visible == false) then
  1465. state.anitype = 'out'
  1466. request_tick()
  1467. end
  1468. else
  1469. osc_visible(false)
  1470. end
  1471. end
  1472. function osc_visible(visible)
  1473. if state.osc_visible ~= visible then
  1474. state.osc_visible = visible
  1475. end
  1476. request_tick()
  1477. end
  1478. function pause_state(name, enabled)
  1479. state.paused = enabled
  1480. mp.add_timeout(0.1, function() state.osd:update() end)
  1481. if user_opts.showonpause then
  1482. if enabled then
  1483. state.lastvisibility = user_opts.visibility
  1484. visibility_mode("always", true)
  1485. show_osc()
  1486. else
  1487. visibility_mode(state.lastvisibility, true)
  1488. end
  1489. end
  1490. request_tick()
  1491. end
  1492. function cache_state(name, st)
  1493. state.cache_state = st
  1494. request_tick()
  1495. end
  1496. -- Request that tick() is called (which typically re-renders the OSC).
  1497. -- The tick is then either executed immediately, or rate-limited if it was
  1498. -- called a small time ago.
  1499. function request_tick()
  1500. if state.tick_timer == nil then
  1501. state.tick_timer = mp.add_timeout(0, tick)
  1502. end
  1503. if not state.tick_timer:is_enabled() then
  1504. local now = mp.get_time()
  1505. local timeout = tick_delay - (now - state.tick_last_time)
  1506. if timeout < 0 then
  1507. timeout = 0
  1508. end
  1509. state.tick_timer.timeout = timeout
  1510. state.tick_timer:resume()
  1511. end
  1512. end
  1513. function mouse_leave()
  1514. if get_hidetimeout() >= 0 then
  1515. hide_osc()
  1516. end
  1517. -- reset mouse position
  1518. state.last_mouseX, state.last_mouseY = nil, nil
  1519. state.mouse_in_window = false
  1520. end
  1521. function request_init()
  1522. state.initREQ = true
  1523. request_tick()
  1524. end
  1525. -- Like request_init(), but also request an immediate update
  1526. function request_init_resize()
  1527. request_init()
  1528. -- ensure immediate update
  1529. state.tick_timer:kill()
  1530. state.tick_timer.timeout = 0
  1531. state.tick_timer:resume()
  1532. end
  1533. function render_wipe()
  1534. msg.trace('render_wipe()')
  1535. state.osd:remove()
  1536. end
  1537. function render()
  1538. msg.trace('rendering')
  1539. local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
  1540. local mouseX, mouseY = get_virt_mouse_pos()
  1541. local now = mp.get_time()
  1542. -- check if display changed, if so request reinit
  1543. if not (state.mp_screen_sizeX == current_screen_sizeX
  1544. and state.mp_screen_sizeY == current_screen_sizeY) then
  1545. request_init_resize()
  1546. state.mp_screen_sizeX = current_screen_sizeX
  1547. state.mp_screen_sizeY = current_screen_sizeY
  1548. end
  1549. -- init management
  1550. if state.initREQ then
  1551. osc_init()
  1552. state.initREQ = false
  1553. -- store initial mouse position
  1554. if (state.last_mouseX == nil or state.last_mouseY == nil)
  1555. and not (mouseX == nil or mouseY == nil) then
  1556. state.last_mouseX, state.last_mouseY = mouseX, mouseY
  1557. end
  1558. end
  1559. -- fade animation
  1560. if not(state.anitype == nil) then
  1561. if (state.anistart == nil) then
  1562. state.anistart = now
  1563. end
  1564. if (now < state.anistart + (user_opts.fadeduration/1000)) then
  1565. if (state.anitype == 'in') then --fade in
  1566. osc_visible(true)
  1567. state.animation = scale_value(state.anistart,
  1568. (state.anistart + (user_opts.fadeduration/1000)),
  1569. 255, 0, now)
  1570. elseif (state.anitype == 'out') then --fade out
  1571. state.animation = scale_value(state.anistart,
  1572. (state.anistart + (user_opts.fadeduration/1000)),
  1573. 0, 255, now)
  1574. end
  1575. else
  1576. if (state.anitype == 'out') then
  1577. osc_visible(false)
  1578. end
  1579. state.anistart = nil
  1580. state.animation = nil
  1581. state.anitype = nil
  1582. end
  1583. else
  1584. state.anistart = nil
  1585. state.animation = nil
  1586. state.anitype = nil
  1587. end
  1588. --mouse show/hide area
  1589. for k,cords in pairs(osc_param.areas['showhide']) do
  1590. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'showhide')
  1591. end
  1592. if osc_param.areas['showhide_wc'] then
  1593. for k,cords in pairs(osc_param.areas['showhide_wc']) do
  1594. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'showhide_wc')
  1595. end
  1596. else
  1597. set_virt_mouse_area(0, 0, 0, 0, 'showhide_wc')
  1598. end
  1599. do_enable_keybindings()
  1600. --mouse input area
  1601. local mouse_over_osc = false
  1602. for _,cords in ipairs(osc_param.areas['input']) do
  1603. if state.osc_visible then -- activate only when OSC is actually visible
  1604. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'input')
  1605. end
  1606. if state.osc_visible ~= state.input_enabled then
  1607. if state.osc_visible then
  1608. mp.enable_key_bindings('input')
  1609. else
  1610. mp.disable_key_bindings('input')
  1611. end
  1612. state.input_enabled = state.osc_visible
  1613. end
  1614. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  1615. mouse_over_osc = true
  1616. end
  1617. end
  1618. if osc_param.areas['window-controls'] then
  1619. for _,cords in ipairs(osc_param.areas['window-controls']) do
  1620. if state.osc_visible then -- activate only when OSC is actually visible
  1621. set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, 'window-controls')
  1622. mp.enable_key_bindings('window-controls')
  1623. else
  1624. mp.disable_key_bindings('window-controls')
  1625. end
  1626. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  1627. mouse_over_osc = true
  1628. end
  1629. end
  1630. end
  1631. if osc_param.areas['window-controls-title'] then
  1632. for _,cords in ipairs(osc_param.areas['window-controls-title']) do
  1633. if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  1634. mouse_over_osc = true
  1635. end
  1636. end
  1637. end
  1638. -- autohide
  1639. if not (state.showtime == nil) and (get_hidetimeout() >= 0) then
  1640. local timeout = state.showtime + (get_hidetimeout()/1000) - now
  1641. if timeout <= 0 then
  1642. if (state.active_element == nil) and not (mouse_over_osc) then
  1643. hide_osc()
  1644. end
  1645. else
  1646. -- the timer is only used to recheck the state and to possibly run
  1647. -- the code above again
  1648. if not state.hide_timer then
  1649. state.hide_timer = mp.add_timeout(0, tick)
  1650. end
  1651. state.hide_timer.timeout = timeout
  1652. -- re-arm
  1653. state.hide_timer:kill()
  1654. state.hide_timer:resume()
  1655. end
  1656. end
  1657. -- actual rendering
  1658. local ass = assdraw.ass_new()
  1659. -- Messages
  1660. render_message(ass)
  1661. -- actual OSC
  1662. if state.osc_visible then
  1663. render_elements(ass)
  1664. end
  1665. -- submit
  1666. set_osd(osc_param.playresy * osc_param.display_aspect,
  1667. osc_param.playresy, ass.text)
  1668. end
  1669. --
  1670. -- Eventhandling
  1671. --
  1672. local function element_has_action(element, action)
  1673. return element and element.eventresponder and
  1674. element.eventresponder[action]
  1675. end
  1676. function process_event(source, what)
  1677. local action = string.format('%s%s', source,
  1678. what and ('_' .. what) or '')
  1679. if what == 'down' or what == 'press' then
  1680. for n = 1, #elements do
  1681. if mouse_hit(elements[n]) and
  1682. elements[n].eventresponder and
  1683. (elements[n].eventresponder[source .. '_up'] or
  1684. elements[n].eventresponder[action]) then
  1685. if what == 'down' then
  1686. state.active_element = n
  1687. state.active_event_source = source
  1688. end
  1689. -- fire the down or press event if the element has one
  1690. if element_has_action(elements[n], action) then
  1691. elements[n].eventresponder[action](elements[n])
  1692. end
  1693. end
  1694. end
  1695. elseif what == 'up' then
  1696. if elements[state.active_element] then
  1697. local n = state.active_element
  1698. if n == 0 then
  1699. --click on background (does not work)
  1700. elseif element_has_action(elements[n], action) and
  1701. mouse_hit(elements[n]) then
  1702. elements[n].eventresponder[action](elements[n])
  1703. end
  1704. --reset active element
  1705. if element_has_action(elements[n], 'reset') then
  1706. elements[n].eventresponder['reset'](elements[n])
  1707. end
  1708. end
  1709. state.active_element = nil
  1710. state.mouse_down_counter = 0
  1711. elseif source == 'mouse_move' then
  1712. state.mouse_in_window = true
  1713. local mouseX, mouseY = get_virt_mouse_pos()
  1714. if (user_opts.minmousemove == 0) or
  1715. (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
  1716. ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
  1717. or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
  1718. )
  1719. ) then
  1720. show_osc()
  1721. end
  1722. state.last_mouseX, state.last_mouseY = mouseX, mouseY
  1723. local n = state.active_element
  1724. if element_has_action(elements[n], action) then
  1725. elements[n].eventresponder[action](elements[n])
  1726. end
  1727. request_tick()
  1728. end
  1729. end
  1730. function show_logo()
  1731. local osd_w, osd_h = 640, 360
  1732. local logo_x, logo_y = osd_w/2, osd_h/2-20
  1733. local ass = assdraw.ass_new()
  1734. ass:new_event()
  1735. ass:pos(logo_x, logo_y)
  1736. ass:append('{\\1c&H8E348D&\\3c&H0&\\3a&H60&\\blur1\\bord0.5}')
  1737. ass:draw_start()
  1738. ass_draw_cir_cw(ass, 0, 0, 100)
  1739. ass:draw_stop()
  1740. ass:new_event()
  1741. ass:pos(logo_x, logo_y)
  1742. ass:append('{\\1c&H632462&\\bord0}')
  1743. ass:draw_start()
  1744. ass_draw_cir_cw(ass, 6, -6, 75)
  1745. ass:draw_stop()
  1746. ass:new_event()
  1747. ass:pos(logo_x, logo_y)
  1748. ass:append('{\\1c&HFFFFFF&\\bord0}')
  1749. ass:draw_start()
  1750. ass_draw_cir_cw(ass, -4, 4, 50)
  1751. ass:draw_stop()
  1752. ass:new_event()
  1753. ass:pos(logo_x, logo_y)
  1754. ass:append('{\\1c&H632462&\\bord&}')
  1755. ass:draw_start()
  1756. ass:move_to(-20, -20)
  1757. ass:line_to(23.3, 5)
  1758. ass:line_to(-20, 30)
  1759. ass:draw_stop()
  1760. ass:new_event()
  1761. ass:pos(logo_x, logo_y+110)
  1762. ass:an(8)
  1763. ass:append(texts.welcome)
  1764. set_osd(osd_w, osd_h, ass.text)
  1765. end
  1766. -- called by mpv on every frame
  1767. function tick()
  1768. if (not state.enabled) then return end
  1769. if (state.idle) then
  1770. --show_logo()
  1771. -- render idle message
  1772. msg.trace('idle message')
  1773. if state.showhide_enabled then
  1774. mp.disable_key_bindings('showhide')
  1775. mp.disable_key_bindings('showhide_wc')
  1776. state.showhide_enabled = false
  1777. end
  1778. elseif (state.fullscreen and user_opts.showfullscreen)
  1779. or (not state.fullscreen and user_opts.showwindowed) then
  1780. -- render the OSC
  1781. render()
  1782. else
  1783. -- Flush OSD
  1784. set_osd(osc_param.playresy, osc_param.playresy, '')
  1785. end
  1786. state.tick_last_time = mp.get_time()
  1787. if state.anitype ~= nil then
  1788. -- state.anistart can be nil - animation should now start, or it can
  1789. -- be a timestamp when it started. state.idle has no animation.
  1790. if not state.idle and
  1791. (not state.anistart or
  1792. mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000)
  1793. then
  1794. -- animating or starting, or still within 1s past the deadline
  1795. request_tick()
  1796. else
  1797. kill_animation()
  1798. end
  1799. end
  1800. end
  1801. function do_enable_keybindings()
  1802. if state.enabled then
  1803. if not state.showhide_enabled then
  1804. mp.enable_key_bindings('showhide', 'allow-vo-dragging+allow-hide-cursor')
  1805. mp.enable_key_bindings('showhide_wc', 'allow-vo-dragging+allow-hide-cursor')
  1806. end
  1807. state.showhide_enabled = true
  1808. end
  1809. end
  1810. function enable_osc(enable)
  1811. state.enabled = enable
  1812. if enable then
  1813. do_enable_keybindings()
  1814. else
  1815. hide_osc() -- acts immediately when state.enabled == false
  1816. if state.showhide_enabled then
  1817. mp.disable_key_bindings('showhide')
  1818. mp.disable_key_bindings('showhide_wc')
  1819. end
  1820. state.showhide_enabled = false
  1821. end
  1822. end
  1823. -- duration is observed for the sole purpose of updating chapter markers
  1824. -- positions. live streams with chapters are very rare, and the update is also
  1825. -- expensive (with request_init), so it's only observed when we have chapters
  1826. -- and the user didn't disable the livemarkers option (update_duration_watch).
  1827. function on_duration() request_init() end
  1828. local duration_watched = false
  1829. function update_duration_watch()
  1830. local want_watch = user_opts.livemarkers and
  1831. (mp.get_property_number("chapters", 0) or 0) > 0 and
  1832. true or false -- ensure it's a boolean
  1833. if (want_watch ~= duration_watched) then
  1834. if want_watch then
  1835. mp.observe_property("duration", nil, on_duration)
  1836. else
  1837. mp.unobserve_property(on_duration)
  1838. end
  1839. duration_watched = want_watch
  1840. end
  1841. end
  1842. validate_user_opts()
  1843. update_duration_watch()
  1844. mp.register_event('shutdown', shutdown)
  1845. mp.register_event('start-file', request_init)
  1846. mp.observe_property('track-list', nil, request_init)
  1847. mp.observe_property('playlist', nil, request_init)
  1848. mp.observe_property("chapter-list", "native", function(_, list)
  1849. list = list or {} -- safety, shouldn't return nil
  1850. table.sort(list, function(a, b) return a.time < b.time end)
  1851. state.chapter_list = list
  1852. update_duration_watch()
  1853. request_init()
  1854. end)
  1855. mp.register_script_message('osc-message', show_message)
  1856. mp.register_script_message('osc-chapterlist', function(dur)
  1857. show_message(get_chapterlist(), dur)
  1858. end)
  1859. mp.register_script_message('osc-playlist', function(dur)
  1860. show_message(get_playlist(), dur)
  1861. end)
  1862. mp.register_script_message('osc-tracklist', function(dur)
  1863. local msg = {}
  1864. for k,v in pairs(nicetypes) do
  1865. table.insert(msg, get_tracklist(k))
  1866. end
  1867. show_message(table.concat(msg, '\n\n'), dur)
  1868. end)
  1869. mp.observe_property('fullscreen', 'bool',
  1870. function(name, val)
  1871. state.fullscreen = val
  1872. request_init_resize()
  1873. end
  1874. )
  1875. mp.observe_property('border', 'bool',
  1876. function(name, val)
  1877. state.border = val
  1878. request_init_resize()
  1879. end
  1880. )
  1881. mp.observe_property('window-maximized', 'bool',
  1882. function(name, val)
  1883. state.maximized = val
  1884. request_init_resize()
  1885. end
  1886. )
  1887. mp.observe_property('idle-active', 'bool',
  1888. function(name, val)
  1889. state.idle = val
  1890. request_tick()
  1891. end
  1892. )
  1893. mp.observe_property('pause', 'bool', pause_state)
  1894. mp.observe_property('demuxer-cache-state', 'native', cache_state)
  1895. mp.observe_property('vo-configured', 'bool', function(name, val)
  1896. request_tick()
  1897. end)
  1898. mp.observe_property('playback-time', 'number', function(name, val)
  1899. request_tick()
  1900. end)
  1901. mp.observe_property('osd-dimensions', 'native', function(name, val)
  1902. -- (we could use the value instead of re-querying it all the time, but then
  1903. -- we might have to worry about property update ordering)
  1904. request_init_resize()
  1905. end)
  1906. -- mouse show/hide bindings
  1907. mp.set_key_bindings({
  1908. {'mouse_move', function(e) process_event('mouse_move', nil) end},
  1909. {'mouse_leave', mouse_leave},
  1910. }, 'showhide', 'force')
  1911. mp.set_key_bindings({
  1912. {'mouse_move', function(e) process_event('mouse_move', nil) end},
  1913. {'mouse_leave', mouse_leave},
  1914. }, 'showhide_wc', 'force')
  1915. do_enable_keybindings()
  1916. --mouse input bindings
  1917. mp.set_key_bindings({
  1918. {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
  1919. function(e) process_event("mbtn_left", "down") end},
  1920. {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
  1921. function(e) process_event("shift+mbtn_left", "down") end},
  1922. {"mbtn_right", function(e) process_event("mbtn_right", "up") end,
  1923. function(e) process_event("mbtn_right", "down") end},
  1924. -- alias to shift_mbtn_left for single-handed mouse use
  1925. {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end,
  1926. function(e) process_event("shift+mbtn_left", "down") end},
  1927. {"wheel_up", function(e) process_event("wheel_up", "press") end},
  1928. {"wheel_down", function(e) process_event("wheel_down", "press") end},
  1929. {"mbtn_left_dbl", "ignore"},
  1930. {"shift+mbtn_left_dbl", "ignore"},
  1931. {"mbtn_right_dbl", "ignore"},
  1932. }, "input", "force")
  1933. mp.enable_key_bindings('input')
  1934. mp.set_key_bindings({
  1935. {'mbtn_left', function(e) process_event('mbtn_left', 'up') end,
  1936. function(e) process_event('mbtn_left', 'down') end},
  1937. }, 'window-controls', 'force')
  1938. mp.enable_key_bindings('window-controls')
  1939. function get_hidetimeout()
  1940. if user_opts.visibility == 'always' then
  1941. return -1 -- disable autohide
  1942. end
  1943. return user_opts.hidetimeout
  1944. end
  1945. function always_on(val)
  1946. if state.enabled then
  1947. if val then
  1948. show_osc()
  1949. else
  1950. hide_osc()
  1951. end
  1952. end
  1953. end
  1954. -- mode can be auto/always/never/cycle
  1955. -- the modes only affect internal variables and not stored on its own.
  1956. function visibility_mode(mode, no_osd)
  1957. if mode == 'auto' then
  1958. always_on(false)
  1959. enable_osc(true)
  1960. elseif mode == 'always' then
  1961. enable_osc(true)
  1962. always_on(true)
  1963. elseif mode == 'never' then
  1964. enable_osc(false)
  1965. else
  1966. msg.warn('Ignoring unknown visibility mode \"' .. mode .. '\"')
  1967. return
  1968. end
  1969. user_opts.visibility = mode
  1970. if not no_osd and tonumber(mp.get_property('osd-level')) >= 1 then
  1971. mp.osd_message('OSC visibility: ' .. mode)
  1972. end
  1973. -- Reset the input state on a mode change. The input state will be
  1974. -- recalcuated on the next render cycle, except in 'never' mode where it
  1975. -- will just stay disabled.
  1976. mp.disable_key_bindings('input')
  1977. mp.disable_key_bindings('window-controls')
  1978. state.input_enabled = false
  1979. request_tick()
  1980. end
  1981. -- KeyboardControl
  1982. --
  1983. local osc_key_bindings = {}
  1984. function osc_kb_control_up()
  1985. visibility_mode('always', true)
  1986. local keyboard_controls = build_keyboard_controls()
  1987. local rows = {}
  1988. local active_row_index = 0
  1989. local active_row_name = nil
  1990. local row_index = -1
  1991. for row_name, row_controls in pairs(keyboard_controls) do
  1992. row_index = row_index + 1
  1993. rows[row_index] = row_name
  1994. for i, control in pairs(row_controls) do
  1995. if control == state.highlight_element then
  1996. active_row_index = row_index
  1997. active_row_name = row_name
  1998. end
  1999. end
  2000. end
  2001. if active_row_index - 1 < 0 then
  2002. return
  2003. end
  2004. local next_row_index = active_row_index - 1
  2005. local new_active_row_name = rows[next_row_index]
  2006. local new_active_row = keyboard_controls[new_active_row_name]
  2007. for i, control in pairs(new_active_row) do
  2008. state.highlight_element = control
  2009. return
  2010. end
  2011. end
  2012. function osc_kb_control_down()
  2013. visibility_mode('always', true)
  2014. local keyboard_controls = build_keyboard_controls()
  2015. local rows = {}
  2016. local active_row_index = 0
  2017. local active_row_name = nil
  2018. local row_index = -1
  2019. for row_name, row_controls in pairs(keyboard_controls) do
  2020. row_index = row_index + 1
  2021. rows[row_index] = row_name
  2022. for i, control in pairs(row_controls) do
  2023. if control == state.highlight_element then
  2024. active_row_index = row_index
  2025. active_row_name = row_name
  2026. end
  2027. end
  2028. end
  2029. if active_row_index + 1 > #rows then
  2030. return
  2031. end
  2032. local next_row_index = active_row_index + 1
  2033. local new_active_row_name = rows[next_row_index]
  2034. local new_active_row = keyboard_controls[new_active_row_name]
  2035. for i, control in pairs(new_active_row) do
  2036. state.highlight_element = control
  2037. return
  2038. end
  2039. end
  2040. function osc_kb_control_left()
  2041. visibility_mode('always', true)
  2042. local keyboard_controls = build_keyboard_controls()
  2043. local active_control_name = nil
  2044. for row_name, row_controls in pairs(keyboard_controls) do
  2045. local controls = {}
  2046. local controls_index = -1
  2047. for i, control in pairs(row_controls) do
  2048. controls_index = controls_index + 1
  2049. controls[controls_index] = control
  2050. if control == state.highlight_element then
  2051. active_control_index = controls_index
  2052. active_control_name = control
  2053. end
  2054. end
  2055. if active_control_name == 'seekbar' then
  2056. mp.commandv('seek', -5, 'exact', 'keyframes')
  2057. return
  2058. end
  2059. if active_control_name then
  2060. if active_control_index - 1 < 0 then
  2061. return
  2062. end
  2063. local next_control_index = active_control_index - 1
  2064. state.highlight_element = controls[next_control_index]
  2065. return
  2066. end
  2067. end
  2068. end
  2069. function osc_kb_control_right()
  2070. visibility_mode('always', true)
  2071. local keyboard_controls = build_keyboard_controls()
  2072. local active_control_name = nil
  2073. for row_name, row_controls in pairs(keyboard_controls) do
  2074. local controls = {}
  2075. local controls_index = -1
  2076. for i, control in pairs(row_controls) do
  2077. controls_index = controls_index + 1
  2078. controls[controls_index] = control
  2079. if control == state.highlight_element then
  2080. active_control_index = controls_index
  2081. active_control_name = control
  2082. end
  2083. end
  2084. if active_control_name == 'seekbar' then
  2085. mp.commandv('seek', 5, 'exact', 'keyframes')
  2086. return
  2087. end
  2088. if active_control_name then
  2089. if active_control_index + 1 > #controls then
  2090. return
  2091. end
  2092. local next_control_index = active_control_index + 1
  2093. state.highlight_element = controls[next_control_index]
  2094. return
  2095. end
  2096. end
  2097. end
  2098. function osc_kb_control_back()
  2099. visibility_mode('auto', true)
  2100. end
  2101. function osc_kb_control_enter()
  2102. visibility_mode('always', true)
  2103. for n = 1, #elements do
  2104. if elements[n].name == state.highlight_element then
  2105. local action = 'enter'
  2106. if element_has_action(elements[n], action) then
  2107. elements[n].eventresponder[action](elements[n])
  2108. return
  2109. end
  2110. local action = 'mbtn_left_up'
  2111. if element_has_action(elements[n], action) then
  2112. elements[n].eventresponder[action](elements[n])
  2113. return
  2114. end
  2115. end
  2116. end
  2117. end
  2118. function osc_add_key_binding(key, name, fn, flags)
  2119. osc_key_bindings[#osc_key_bindings + 1] = name
  2120. mp.add_forced_key_binding(key, name, fn, flags)
  2121. end
  2122. -- This is based on code from https://github.com/darsain/uosc
  2123. function osc_enable_key_bindings()
  2124. osc_key_bindings = {}
  2125. -- The `mp.set_key_bindings()` method would be easier here, but that
  2126. -- doesn't support 'repeatable' flag, so we are stuck with this monster.
  2127. osc_add_key_binding('up', 'osc-kb-control-prev1', osc_kb_control_up, 'repeatable')
  2128. osc_add_key_binding('down', 'osc-kb-control-next1', osc_kb_control_down, 'repeatable')
  2129. osc_add_key_binding('left', 'osc-kb-control-left1', osc_kb_control_left, 'repeatable')
  2130. osc_add_key_binding('right', 'osc-kb-control-right1', osc_kb_control_right, 'repeatable')
  2131. osc_add_key_binding('enter', 'osc-kb-control-select-alt3', osc_kb_control_enter, 'repeatable')
  2132. osc_add_key_binding('esc', 'osc-kb-control-close', osc_kb_control_back, 'repeatable')
  2133. end
  2134. function osc_disable_key_bindings()
  2135. for _, name in ipairs(osc_key_bindings) do mp.remove_key_binding(name) end
  2136. osc_key_bindings = {}
  2137. end
  2138. visibility_mode(user_opts.visibility, true)
  2139. mp.register_script_message('osc-visibility', visibility_mode)
  2140. mp.add_key_binding(nil, 'visibility', function() visibility_mode('cycle') end)
  2141. set_virt_mouse_area(0, 0, 0, 0, 'input')
  2142. set_virt_mouse_area(0, 0, 0, 0, 'window-controls')