-
Notifications
You must be signed in to change notification settings - Fork 2
/
oscf.lua
471 lines (444 loc) · 13.6 KB
/
oscf.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
-- osc framework
-- ver 1.5
-- # variables
-- player status for public access, generated by program
-- changing their values do not interfere the framework
player = {
now = 0, -- a copy of local now for public access
geo = {width = 0, height = 0, aspect = 0},
idle = true, -- idle status
}
-- user options, altering them may change osc behavior
opts = {
scale = 1, -- osc render scale
fixedHeight = false, -- true means osc y resolution is fixed 480, all elements scale with player window height
hideTimeout = 1, -- seconds untile osc hides, negative means never
fadeDuration = 0.5, -- seconds during fade out, negative means never
}
-- local variables, users do not touch them
local elements = {} -- all available elements
local elementsInUse = {} -- an element added to a layout will be marked here
local layouts = {idle = {}, play = {}} -- tow layouts: idle, play
local oscLayout = 'idle' -- selected layout will be rendered
local activeAreas = { -- mouse in these rectangle areas avtivate osc and key bindings
idle = {}, play = {}} -- example: idle['name'] = {x1=n1, y1=n2, x2=n3, y2=n4, prop=''}
local active = false -- true if mouse is in activeArea
local osd = mp.create_osd_overlay('ass-events')
local tickTimer = nil -- osc timer objects
local tickDelay = 0.03 -- 33 fps limit
local now = 0 -- now time, updated by tick()
local mouseScale = 1 -- mouse positon scale
local visible = false -- osc visiblility
local visualMode = 'out' -- visual mode: in, out, always, hide
local fadeLastTime = 0 -- fade effect time base
local fadeFactor = 0 -- fade factor as transparency modifier
-- # basic osc functions
-- set osd display
-- text: in ASS format
local function setOsd(text)
if text == osd.data then return end
osd.data = text
osd:update()
end
-- update visual effects, which deal with the fade out effect
local function updateVisual()
if visualMode == 'in' then -- osc is shown
visible = true
fadeFactor = 0
if opts.hideTimeout < 0 then return end
if now - fadeLastTime >= opts.hideTimeout then
visualMode = 'out'
fadeLastTime = now
end
elseif visualMode == 'out' then -- fading out
if opts.fadeDuration < 0 then return end
local time = now - fadeLastTime
local factor = time / opts.fadeDuration
if factor > 1 then factor = 1 end
fadeFactor = factor
if time >= opts.fadeDuration then visible = false end
elseif visualMode == 'always' then
fadeFactor = 0
visible = true
elseif visualMode == 'hide' then
visible = false
end
end
function getVisibility()
if visualMode == 'in' or visualMode == 'out' then
return 'normal'
elseif visualMode == 'always' then
return 'always'
elseif visualMode == 'hide' then
return 'hide'
else return 'unknown' end
end
-- set osc visibility
-- mode: 'normal', 'always', 'hide'
function setVisibility(mode)
if mode == 'normal' then
visualMode = 'in'
elseif mode == 'always' then
visualMode = 'always'
elseif mode == 'hide' then
visualMode = 'hide'
end
end
-- show osc if it's faded out
function showOsc()
if visualMode == 'in' or visualMode == 'out' then
visualMode = 'in'
fadeLastTime = now
end
end
-- render a selected layout
local renderLayout = {
idle = function()
local text = {}
for i, e in ipairs(layouts.idle) do
text[i] = e.tick(e)
end
setOsd(table.concat(text, '\n'))
end,
play = function()
local text = {}
updateVisual()
if visible then
for i, e in ipairs(layouts.play) do
e.setAlpha(e, fadeFactor) -- remix fade effect
text[i] = e.tick(e)
end
end
setOsd(table.concat(text, '\n'))
end
}
-- called by mpv timer periodically
local function tick()
now = mp.get_time()
player.now = now
if active then showOsc() end
renderLayout[oscLayout]()
end
-- called on player resize
local function oscResize()
local baseWidth, baseHeight = 720, 480
local dispWidth, dispHeight, dispAspect = mp.get_osd_size()
if dispAspect > 0 then -- in some cases osd size could be zero, need to check
if opts.fixedHeight then -- if true, baseWidth is calculated according to baseHeight
baseWidth = baseHeight * dispAspect
else -- or else, use real window size
baseWidth, baseHeight = dispWidth, dispHeight
end
end
local x = baseWidth / opts.scale
local y = baseHeight / opts.scale
player.geo = {
width = x,
height = y,
aspect = dispAspect
}
-- set osd resolution
osd.res_x = x
osd.res_y = y
-- display positon may not be actual mouse position. need scale
if dispHeight > 0 then
mouseScale = y / dispHeight
else
mouseScale = 0
end
end
-- initialize
local function init()
-- init osd params
osd.res_x = 0
osd.res_y = 0
osd.z = 10
-- init timer
tickTimer = mp.add_periodic_timer(tickDelay, tick)
-- disable internal osc
mp.commandv('set', 'osc', 'no')
end
-- osc starts here
init()
-- # element management
local elements = {}
-- creat a default element as a template
elements['default'] = {
-- z order
layer = 0,
-- geometry, left, right, width, height, alignmet
geo = {x = 0, y = 0, w = 0, h = 0, an = 7},
-- global transparency modifier
trans = 0,
-- render style, each property can be optional
style = {
-- primary, secondary, outline, back colors in BGR format
-- back color only works when "osd-back-color" is set in mpv
color = {'ffffff', 'ffffff', 'ffffff', 'ffffff'},
-- transparency, 0~255, 255 is invisible
alpha = {0, 0, 0, 255},
-- border size, decimal
border = 0,
-- blur size, decimal
blur = 0,
-- shadow size, decimal
shadow = 0,
-- font name, string
font = '',
-- fontsize, decimal
fontsize = 10,
-- 0-auto, 1-end wrap, 2-no wrap, 3-auto2
wrap = 2,
},
visible = true,
-- pack[1] - position codes
-- pack[2] - alpha codes
-- pack[3] - other style codes
-- pack[4] - other contents
-- pack[>4] also works if you like
pack = {'', '', '', ''},
-- initialize the element
init = function(self)
self:setPos()
self:setStyle()
self:render()
end,
-- set positon codes
setPos = function(self)
if not self.geo then return end
self.pack[1] = string.format('{\\pos(%f,%f)\\an%d}', self.geo.x, self.geo.y, self.geo.an)
end,
-- set alpha codes, usually called by oscf
-- trans: transparency modifier, 0~1, 1 for invisible
setAlpha = function(self, trans)
local sta = self.style.alpha or elements['default'].style.alpha
local alpha = {0, 0, 0, 0}
self.trans = trans
for i = 1, 4 do
alpha[i] = 255 - (255-sta[i])*(1-trans)
end
self.pack[2] = string.format('{\\1a&H%x&\\2a&H%x&\\3a&H%x&\\4a&H%x&}',
alpha[1], alpha[2], alpha[3], alpha[4])
end,
-- set style codes, including alpha codes
setStyle = function(self)
local st, std = self.style, elements['default'].style
local fmt = {'{', '', '', '', '', '', '', '', '}'}
if st.color then
fmt[2] = string.format('\\1c&H%s&\\2c&H%s&\\3c&H%s&\\4c&H%s&',
st.color[1], st.color[2], st.color[3], st.color[4])
else
fmt[2] = string.format('\\1c&H%s&\\2c&H%s&\\3c&H%s&\\4c&H%s&',
std.color[1], std.color[2], std.color[3], std.color[4])
end
fmt[3] = string.format('\\bord%.2f', st.border or std.border)
fmt[4] = string.format('\\blur%.2f', st.blur or std.blur)
fmt[5] = string.format('\\shad%.2f', st.shadow or std.shadow)
fmt[6] = '\\fn' .. (st.font or std.font)
fmt[7] = string.format('\\fs%.2f', st.fontsize or std.fontsize)
fmt[8] = string.format('\\q%d', st.wrap or std.wrap)
self.pack[3] = table.concat(fmt)
self:setAlpha(self.trans)
end,
-- update other contents
render = function(self) end,
-- called by function tick(), return the pack as a string for further render
-- it MUST return a string, or the osc will halt
tick = function(self)
if self.visible then return table.concat(self.pack)
else return ''
end
end,
--[[responders are functions called by dispatchEvents(), the format is like
responder['event'] = function(self, arg)
...
return true/false
end
return true will prevent this event from other responders, useful in overlaped elements that only one is allowed to respond.]]--
responder = {},
}
-- create a new element, either from 'default', or from an existing source
-- name: name string of the new element
-- source: OPTIONAL name string of an element as a template
-- return: table of the new element
function newElement(name, source)
local ne, lookup = {}, {}
if source == nil then source = 'default' end
local function clone(e) -- deep clone
if type(e) ~= 'table' then return e end
if lookup[e] then return lookup[e] end --keep reference relations
local copy = {}
lookup[e] = copy
for k, v in pairs(e) do
copy[k] = clone(v)
end
return setmetatable(copy, getmetatable(e))
end
ne = clone(elements[source])
elements[name] = ne
return ne
end
-- get the table of an element
-- name: name string of the element
function getElement(name)
return elements[name]
end
-- sort element order
local function lowerFirst (a, b)
return a.layer < b.layer
end
local function higherFirst (a, b)
return a.layer > b.layer
end
-- add an element to idle layout
-- name: name string of the element
-- return: element table
function addToIdleLayout(name)
return addToLayout('idle', name)
end
-- add an element to play layout
-- name: name string of the element
-- return: element table
function addToPlayLayout(name)
return addToLayout('play', name)
end
-- add an element ot a layout
-- layout: 'idle', 'play', or something you like
-- name: name string
-- return: element table
function addToLayout(layout, name)
local e = elements[name]
if e then
table.insert(layouts[layout], e)
table.sort(layouts[layout], lowerFirst)
-- elementsInUse require unique values
local check = true
local n = #elementsInUse
for i = 1, n do
if e == elementsInUse[i] then
check = false
break
end
end
if check then
elementsInUse[n+1] = e
end
table.sort(elementsInUse, higherFirst)
end
return e
end
-- # event management
-- dispatch event to elements in current layout
-- event: string of event name
-- arg: OPTIONAL arguments
function dispatchEvent(event, arg)
for _, v in ipairs(elementsInUse) do
if v.responder[event] and
v.responder[event](v, arg) then
-- a responder return true can terminate this event
break end
end
end
-- property observers to generate events
-- these are minimum events for osc framework
mp.observe_property('osd-dimensions', 'native',
function(name, val)
oscResize()
dispatchEvent('resize')
end)
mp.observe_property('idle-active', 'bool',
function(name, val)
player.idle = val
if not player.idle then oscLayout = 'play'
else oscLayout = 'idle'
end
dispatchEvent('idle')
end)
-- set an active area for idle layout
-- name: string of area name
function setIdleActiveArea(name, x1, y1, x2, y2, prop)
setActiveArea('idle', name, x1, y1, x2, y2, prop)
end
-- set an active area for play layout
-- name: string of area name
function setPlayActiveArea(name, x1, y1, x2, y2, prop)
setActiveArea('play', name, x1, y1, x2, y2, prop)
end
-- a general function to set active area
-- layout: 'idle', 'play', or else
function setActiveArea(layout, name, x1, y1, x2, y2, prop)
local area = {x1 = x1, y1 = y1, x2 = x2, y2 = y2, prop = prop}
activeAreas[layout][name] = area
end
-- get mouse position scaled by display factor
function getMousePos()
local x, y = mp.get_mouse_pos()
return x*mouseScale, y*mouseScale
end
-- mouse move event handler
local function eventMove()
local x, y = getMousePos()
local check = false
local areas = activeAreas[oscLayout]
for _, v in pairs(areas) do
if v.x1 <= x and x <= v.x2 and v.y1 <= y and y <= v.y2 then
if v.prop == 'show_hide' then
showOsc()
goto next
else
-- default
check = true
if not active then
mp.enable_key_bindings('_button_')
active = true
end
end
dispatchEvent('mouse_move', {x, y})
end
::next::
end
if not check and active then
mp.disable_key_bindings('_button_')
active = false
dispatchEvent('mouse_leave')
end
end
-- mouse leave player window
local function eventLeave()
if active then
mp.disable_key_bindings('_button_')
active = false
end
dispatchEvent('mouse_leave')
end
-- mouse button event handler
local function eventButton(event)
local x, y = getMousePos()
dispatchEvent(event, {x, y})
end
-- mouse move bindings
mp.set_key_bindings({
{'mouse_move', eventMove},
{'mouse_leave', eventLeave},
}, '_move_', 'force')
mp.enable_key_bindings('_move_', 'allow-vo-dragging+allow-hide-cursor')
--mouse input bindings
mp.set_key_bindings({
{'mbtn_left', function() eventButton('mbtn_left_up') end, function() eventButton('mbtn_left_down') end},
{'mbtn_right', function() eventButton('mbtn_right_up') end, function() eventButton('mbtn_right_down') end},
{'mbtn_mid', function() eventButton('mbtn_mid_up') end, function() eventButton('mbtn_mid_down') end},
{'wheel_up', function() eventButton('wheel_up') end},
{'wheel_down', function() eventButton('wheel_down') end},
{'mbtn_left_dbl', function() eventButton('mbtn_left_dbl') end},
{'mbtn_right_dbl', function() eventButton('mbtn_right_dbl') end},
{'mbtn_mid_dbl', function() eventButton('mbtn_mid_dbl') end},
}, '_button_', 'force')
-- mouse button events control for user scripts
-- enable mouse button events
function enableMouseButtonEvents()
mp.enable_key_binding('_button_')
end
-- disable mouse button events
function disableMouseButtonEvents()
mp.mp.disable_key_bindings('_button_')
end