├── README.md ├── expansion.lua ├── main.lua └── oscf.lua /README.md: -------------------------------------------------------------------------------- 1 | # mpv-osc-framework 2 | 3 | Oscf is an “osc framework” to help building your custom osc for mpv player. 4 | 5 | changelog: 6 | 7 | ver 1.5 8 | 9 | [change] improve compatability with --osd-back-color settings 10 | 11 | ver 1.4 12 | 13 | [change] the global var 'elements' is local now 14 | [change] element['default'] now have ''default'' style params 15 | [change] setAlpha, setStyle for element['default'] is optimized to use the ''default'' style params if not provided by user. This may change the behavior of previous scripts which have used undefined styles. 16 | 17 | ver 1.3 18 | 19 | [change] change behavior of active area actions 20 | 21 | ver 1.2 22 | 23 | [change] some tweaks on event handle methods 24 | 25 | ver 1.1 26 | 27 | [change] mouse leaving active areas will produce a 'mouse_leave' event 28 | 29 | ver 1.0 30 | 31 | [change] change fixedSize to fixedHeight 32 | [fix] a bug fix in mouseMove() 33 | [change] some minor tweak in the framework 34 | 35 | ver 0.6 36 | 37 | [add] realize the init function for element 'default' 38 | [add] add a seperate setAlpha function to set alpha codes, yet setStyle function set alpha codes as well 39 | [change] element.pack now has 4 elements, [2] = alpha codes, [4] = render codes 40 | [change] renderLayout function use setAlpha to mix global transparency 41 | [change] optimize setPos() and setStyle() 42 | [fix] bugfix for expansions and main 43 | 44 | ver 0.5 45 | 46 | first release 47 | 48 | ## Introduction 49 | 50 | Mpv-osc-framewokr, or oscf, is a simple tool to help building your own osc(on screen control), as well as sharing codes between different oscs. 51 | 52 | The file “oscf.lua” provides a core set of functions to run this tool, and another file "expansion.lua" provides more functions and templates to make it works better. 53 | 54 | The file "main.lua" has realized a ["mpv-osc-modern"](https://github.com/maoiscat/mpv-osc-modern) like osc with this tool, as a demo. 55 | 56 | To try it, you need to make a new folder like "\~\~/mpv/scripts/demo", and download all 3 files there. Remenber to remove other osc scripts. And you will need [material-design-iconic-font](https://zavoloklom.github.io/material-design-iconic-font/) as well. 57 | 58 | ## Getting Start 59 | 60 | The oscf is coded in [lua](http://www.lua.org/) language, which is natively supported by mpv. The [manual](https://mpv.io/manual/master/#script-location) has told everything about the scripting work, so I just suggest a simple method: 61 | 62 | 1. Make a new folder in "~~/mpv/scripts/", such as "~~/mpv/scripts/demo/". 63 | 2. Copy oscf.lua to "demo". 64 | 3. Make a new file "main.lua" in "demo". 65 | 4. Use "require 'oscf'" in main.lua to import oscf. 66 | 67 | Now when mpv starts, it loads demo/main.lua automatically, and oscf starts as well. 68 | 69 | ## Elements 70 | 71 | Elements are basic units of the osc. An element can be a button, a shape, or even an invisible updater. Elements are created like: 72 | 73 | ``` 74 | local el1 = newElement('element1') 75 | local el2 = newElement('element2', 'element1') 76 | ``` 77 | Here 'element2' is the name of el2, and el2 is created using the element named as 'element1', which is el1, as a template. The template for el1 is an internal default element, whose name is 'default' as well. 78 | 79 | The created element is completely the same as the template. It use a "deep copy" method to make the clone from each key and value of the template recursively. 80 | 81 | The "default" element is defined as follows: 82 | 83 | ``` 84 | elements['default'] = { 85 | layer = 0, 86 | geo = {x = 0, y = 0, w = 0, h = 0, an = 7}, 87 | trans = 0, 88 | style = { 89 | color = {'ffffff', 'ffffff', 'ffffff', 'ffffff'}, 90 | alpha = {0, 0, 0, 255}, 91 | border = 0, 92 | blur = 0, 93 | shadow = 0, 94 | font = '', 95 | fontsize = 10, 96 | wrap = 2, 97 | }, 98 | visible = true, 99 | pack = {'', '', '', ''}, 100 | init = function(self) ... end, 101 | setPos = function(self) ... end, 102 | setAlpha = function(self, trans) ... end, 103 | setStyle = function(self) ... end, 104 | render = function(self) end, 105 | tick = function(self) ... end, 106 | responder = {}, 107 | } 108 | ``` 109 | 110 | Here are details: 111 | 112 | **layer** is the z order of an element. An element of higher layer place on top of an lower one when overlaped. 113 | 114 | **geo** is the geometry parameters of an element. They are x - left, y - top, w - width, h - height, and an - alignment respectively. Definitions of alignments are the same as ASS/SSA styles, because elements are rendered as ASS subtitles. More details can be found [here](http://www.perlfu.co.uk/projects/asa/ass-specs.doc). 115 | 116 | **trans** is a global transparency modifier for the visual effect realization. Users may not need to touch it. It's a decimal ranging from 0 to 1, and 1 means invisible. 117 | 118 | **style** is the style params to render the element. They are all ASS styled params. 119 | 120 | *color* - primary, secondary, outline and background color in **BGR** order. currently, background color only works when --osd-back-color is set in mpv 121 | 122 | *alpha* - primary, secondary, outline and background transparency, 0~255, 255 is invisible. 123 | 124 | *border* - border size, decimal numbers. 125 | 126 | *blur* - blur size, decimal numbers. 127 | 128 | *shadow* - shadow size, decimal numbers. 129 | 130 | *font* - fontname, string. 131 | 132 | *fontsize* - font size, decimal numbers. 133 | 134 | *wrap* - wrap style, 0 - auto wrap, 1- end wrap, 2 - no wrap, 3 - another auto wrap. 135 | 136 | **visible** is true when the element is visible. 137 | 138 | **pack** stores then render results. In the pack, [1] stores the position and alignment code, [2] stores the alpha code, [3] stores other style codes, and [4] stores text and drawing codes. They are all string in ASS format. 139 | 140 | **init(self)** is the initialize method, which is realized to do the following work: 141 | 142 | setPos() 143 | setStyle() 144 | render() 145 | 146 | users can overwrite a new init if needed. 147 | 148 | **setPos(self)** is a method to update position codes in pack[1]. Users may not need to overwrite it. 149 | 150 | **setAlpha(self, trans)** is a method to update alpha codes in pack[2]. It's usually called by the framework, and users may not need to overwrite it. 151 | 152 | **setStyle(self)** is a method to update other style codes in pack[3]. The default method hasn't realized all ASS style codes, users may overwrite this method in their own needs. 153 | 154 | **render(self)** is a method to update text and drawing codes in pack[4]. This method does nothing by default. Users have to realize it. 155 | 156 | **tick(self)** is a method called by a timer of the framework automatically. The framework updates the render results of each element in every tick, which is 0.03 second by default. If there are any periodical tasks, they can be done here. By default this method returns the concatenated string of pack if the element is visible. If an user overwrite this method, he must make sure it always return a string, or the framework may halt. 157 | 158 | **responder** stores the event responder methods. An example of a responder is like this: 159 | 160 | ``` 161 | el.responder['event_name'] = function(self, arg) 162 | -- this is a universal method that works with any 'event_name' 163 | return true/false 164 | end 165 | el.responder.event_name = function(self, arg) 166 | -- this is a more convenient method, yet not all the 'event_name' works with lua syntax 167 | return true/false 168 | end 169 | ``` 170 | 171 | A responder returning **true** will terminate this event for other elements. This may be useful in mouse action events when multiple elements are overlapped and only the top one is allowed to responde. 172 | 173 | ## Layouts 174 | 175 | Having created a new element, you should add it to a layout to take effect. 176 | 177 | There are two internal layouts: idle and play. Idle means the "idle-active" status that the player is just started and no file is loaded. Yet play means files are loaded and playing, which is opposite to idle. 178 | 179 | Therefore, if an element is added to the idle layout, it only appears when player is idle, like the logo. On contrary, an element added to the play layout shows up when the player is playing. 180 | 181 | The related funtions are: 182 | 183 | ``` 184 | function addToIdleLayout(name) -- add an element to idle layout 185 | function addToPlayLayout(name) -- add an element to play layout 186 | function addToLayout(layout, name) -- add an element to a layout 187 | ``` 188 | 189 | Here "name" is the string of your element name, rather than the element table name. 190 | 191 | ## Events 192 | 193 | In this tool, events are identified by name, which is a string, such as 'get_read', 'stop'. 194 | 195 | There are 3 events built in to support this framework: 'resize', 'idle', and 'mouse_leave'. 196 | 197 | 'resize' happens when the osc dimesions are changed, which is very useful to reset the geometry of an element. 198 | 199 | 'idle' happens when mpv goes into/out of idle status. 200 | 201 | 'mouse_leave' happens when the mouse pointer moves out of an active area. 202 | 203 | Users can generate and dispatch other events using 204 | 205 | ``` 206 | dispatchEvent('event_name', args) 207 | ``` 208 | 209 | This function dispatch events for all layouts. Then the function in **element.responder\['event_name'\]** will be called if it exists for every single element in current layout, except that a responder returns **false** and terminates this event. 210 | 211 | ## Mouse Action Support 212 | 213 | This tool provides basic mouse action support, they are: 214 | 215 | mouse_move/ mouse_leave 216 | mbtn_left_down/ mbtn_left_up 217 | mbtn_mid_down/ mbtn_mid_up 218 | mbtn_right_down/ mbnt_right_up 219 | mbtn_left_dbl/ mbtn_right_dbl 220 | wheel_up/ wheel_down 221 | 222 | All mouse actions are treated as events. Normally the responder should be like: 223 | 224 | ``` 225 | element.responder['mouse_move'] = function(self, pos) 226 | local x, y = pos[1], pos[2] 227 | .... 228 | return true 229 | end 230 | ``` 231 | 232 | It should be noticed that except for 'mouse_move' and 'mouse_leave', other mouse button events are generated only when the mouse pointer is inside of an 'active area'. 233 | 234 | ## Active Area 235 | 236 | When mouse moves inside of an active area, the osc will be shown if it's faded out. The mouse button key bindings are enabled, and thus mouse button events can be generated. 237 | 238 | The active areas for idle and play layouts are different, and both layouts support multiple active areas. The related functions are: 239 | 240 | ``` 241 | function setIdleActiveArea(name, x1, y1, x2, y2, prop) -- set active area for idle layout 242 | function setIdleActiveArea(name, x1, y1, x2, y2, prop) -- set active area for play layout 243 | function setActiveArea(layout, name, x1, y1, x2, y2, prop)-- set active area for a layout 244 | ``` 245 | 246 | Here 'name' is the name string of an area, and x1, y1, x2, y2 are left, top right, bottom position of an area. 247 | 248 | 'prop' is the property of the area. It is optional, and only supported property other than nil by now is 249 | 250 | show_hide - mouse moves in this area whill show osc once, but won't generate mouse events, nor enabling mouse keybindings. 251 | 252 | ## Timer 253 | 254 | As said, this framework use a periodical timer to call tick() method to update render results. The timing interval is 0.03 seconds by default, which limits the maximum fps to about 33. 255 | 256 | This timer also updates a public variable *player.now*. Users may use it to realize some time related functions. 257 | 258 | ## Visual Effects 259 | 260 | This tool uses a fading out effect to hide osc elements. Yet the osc can be "always on" or "hidden forever". The related funcion is: 261 | 262 | ``` 263 | getVisibility() -- get osc visibility 264 | setVisibility(mode)-- set osc visibility 265 | ``` 266 | 267 | Supported visibility modes are 'normal', 'always', 'hide'. And the fadding effect can be tuned with variables in *opts* table. 268 | 269 | ## Public Variables 270 | 271 | This tool introduces two public tables: *player* and *opts*. 272 | 273 | ``` 274 | player = { 275 | now = 0, 276 | geo = {width = 0, height = 0, aspect = 0}, 277 | idle = true, 278 | } 279 | 280 | opts = { 281 | scale = 1, 282 | fixedHeight = false, 283 | hideTimeout = 1, 284 | fadeDuration = 0.5, 285 | } 286 | ``` 287 | 288 | *player* table reflects the player status for public access, which is normally generated by your program, and changing their values do not interfere the framework 289 | 290 | **now** is the now time of the player in seconds. Users may use this value to do some time related tasks. 291 | 292 | **geo** is the geometry of the video area. It is usually used to determine elements' placement. 293 | 294 | **idle** is true when player is in idle status. This is used to check if it's using the idle layout. 295 | 296 | *opts* table means user options, and altering them may change osc behavior. 297 | 298 | **scale** is the render scale of an element. scale = 2 will double the size of an element. 299 | 300 | **fixedHeight** chooses wether to fix the y resolution to 480. On true, all elements will scale with the player window height. On false the window keeps its real y resolution. 301 | 302 | **hideTimeout** is the time before the osc starts to hide after mouse leaves all active area. Measured in seconds. A negative value means never hide. 303 | 304 | **fadeDuration** is the time length during the fading out effect. Measured in seconds. A negative value means never fade. 305 | 306 | ## Public Function List 307 | 308 | More details can be found in the script. 309 | 310 | ``` 311 | getVisibility() -- get osc visibility 312 | setVisibility(mode)-- set osc visibility 313 | showOsc() -- show osc if it's faded out 314 | newElement(name, source) -- create a new element, either from 'default', or from an existing source 315 | getElement(name) -- get the table of an element 316 | addToIdleLayout(name) -- add an element to idle layout 317 | addToPlayLayout(name) -- add an element to play layout 318 | addToLayout(layout, name) -- add an element to a layout 319 | dispatchEvent(event, arg) -- dispatch an event 320 | setIdleActiveArea(name, x1, y1, x2, y2, prop) -- set active area for idle layout 321 | setPlayActiveArea(name, x1, y1, x2, y2, prop) -- set active area for play layout 322 | setActiveArea(layout, name, x1, y1, x2, y2, prop)-- set active area for a layout 323 | getMousePos() -- get mouse position 324 | enableMouseButtonEvents() -- temporarily enable mouse button events 325 | disableMouseButtonEvents()-- temporarily disable mouse button events 326 | ``` 327 | -------------------------------------------------------------------------------- /expansion.lua: -------------------------------------------------------------------------------- 1 | -- osc framework expansions 2 | -- by maoiscat 3 | -- github.com/maoiscat/ 4 | 5 | local assdraw = require 'mp.assdraw' 6 | require 'oscf' 7 | 8 | -- # some useful functions 9 | -- print table, for debug 10 | function ptb(tab, prefix) 11 | local fmt, str 12 | if prefix == nil then prefix = tostring(tab) end 13 | if type(tab) ~= 'table' then 14 | str = tostring(tab) 15 | string.gsub(str, '\n', '[nl]') 16 | print(string.format('%s = %s', prefix, str)) 17 | else 18 | for k, v in pairs(tab) do 19 | str = prefix .. '.' .. tostring(k) 20 | ptb(v, str) 21 | end 22 | end 23 | end 24 | 25 | -- a simple clone function to help copying style table 26 | function clone(sth) 27 | if type(sth) ~= 'table' then return sth end 28 | local copy = {} 29 | for k, v in pairs(sth) do 30 | copy[k] = clone(v) 31 | end 32 | return copy 33 | end 34 | 35 | -- get the outline box coordinates of an element. 36 | -- geo: same format as element.geo 37 | -- return: left, top, right, bottom position 38 | function getBoxPos(geo) 39 | local box = { 40 | [1] = function(geo) return geo.x, geo.y-geo.h, geo.x+geo.w, geo.y end, 41 | [2] = function(geo) return geo.x-geo.w/2, geo.y-geo.h, geo.x+geo.w/2, geo.y end, 42 | [3] = function(geo) return geo.x-geo.w, geo.y-geo.h, geo.x, geo.y end, 43 | [4] = function(geo) return geo.x, geo.y-geo.h/2, geo.x+geo.w, geo.y+geo.h/2 end, 44 | [5] = function(geo) return geo.x-geo.w/2, geo.y-geo.h/2, geo.x+geo.w/2, geo.y+geo.h/2 end, 45 | [6] = function(geo) return geo.x-geo.w, geo.y-geo.h/2, geo.x, geo.y+geo.h/2 end, 46 | [7] = function(geo) return geo.x, geo.y, geo.x+geo.w, geo.y+geo.h end, 47 | [8] = function(geo) return geo.x-geo.w/2, geo.y, geo.x+geo.w/2, geo.y+geo.h end, 48 | [9] = function(geo) return geo.x-geo.w, geo.y, geo.x, geo.y+geo.h end, 49 | } 50 | local x1, y1, x2, y2 51 | if box[geo.an] then 52 | x1, y1, x2, y2 = box[geo.an](geo) 53 | end 54 | return x1, y1, x2, y2 55 | end 56 | -- get the list of tracks 57 | -- return: tracks categorize as video, audio and sub 58 | function getTrackList() 59 | local trackList = mp.get_property_native('track-list') 60 | local tracks = {video = {}, audio = {}, sub = {}} 61 | for i, v in ipairs(trackList) do 62 | if v.type ~= 'unknown' then 63 | table.insert(tracks[v.type], v) 64 | end 65 | end 66 | return tracks 67 | end 68 | -- get playlist 69 | function getPlaylist() 70 | local playlist = mp.get_property_native('playlist') 71 | return playlist 72 | end 73 | -- get position on playlist 74 | -- return: pos number start from 1 75 | function getPlaylistPos() 76 | local pos = mp.get_property_number('playlist-pos-1') 77 | return pos 78 | end 79 | -- get chapter list 80 | function getChapterList() 81 | local chapters = mp.get_property_native('chapter-list') 82 | return chapters 83 | end 84 | -- get current track 85 | -- name: 'video', 'audio' or 'sub' 86 | -- return: track index, 0 for none 87 | function getTrack(name) 88 | local prop = string.format('current-tracks/%s/id', name) 89 | local index = mp.get_property_number(prop) 90 | if index then return index 91 | else return 0 end 92 | end 93 | 94 | -- cycle through tracks 95 | -- name: 'video', 'audio' or 'sub' 96 | -- direction: optional 'next' or 'prev', default is 'next' 97 | function cycleTrack(name, direction) 98 | local current = getTrack(name) 99 | local index 100 | local tracks = getTrackList() 101 | tracks = tracks[name] 102 | if not tracks then return end 103 | if direction == 'prev' then 104 | index = current - 1 105 | else 106 | index = current + 1 107 | end 108 | if index > #tracks then index = 0 109 | elseif index < 0 then index = #tracks 110 | end 111 | local newTrack 112 | 113 | if index == 0 then 114 | newTrack = 'no' 115 | else 116 | newTrack = tracks[index].id 117 | end 118 | mp.commandv('set', name, newTrack) 119 | end 120 | 121 | -- build ass fomrat style code, almost a copy from oscf.lua 122 | -- trans is a global tansparency modifier 123 | -- return foramted style text 124 | function buildStyle(style, trans) 125 | if not style then return '' end 126 | if not trans then trans = 0 end 127 | local fmt = {'{'} 128 | if style.color then 129 | table.insert(fmt, 130 | string.format('\\1c&H%s&\\2c&H%s&\\3c&H%s&\\4c&H%s&', 131 | style.color[1], style.color[2], style.color[3], style.color[4])) 132 | end 133 | local alpha = {} 134 | if style.alpha then 135 | for i = 1, 4 do 136 | alpha[i] = 255 - (((1-(style.alpha[i]/255)) * (1-trans)) * 255) 137 | end 138 | else 139 | alpha = {trans*255, trans*255, trans*255, trans*255} 140 | end 141 | table.insert(fmt, string.format('\\1a&H%x&\\2a&H%x&\\3a&H%x&\\4a&H%x&', 142 | alpha[1], alpha[2], alpha[3], alpha[4])) 143 | if style.border then 144 | table.insert(fmt, string.format('\\bord%.2f', style.border)) end 145 | if style.blur then 146 | table.insert(fmt, string.format('\\blur%.2f', style.blur)) end 147 | if style.shadow then 148 | table.insert(fmt, string.format('\\shad%.2f', style.shadow)) end 149 | if style.font then 150 | table.insert(fmt, string.format('\\fn%s', style.font)) end 151 | if style.fontsize then 152 | table.insert(fmt, string.format('\\fs%d', style.fontsize)) end 153 | if style.wrap then 154 | table.insert(fmt, string.format('\\q%d', style.wrap)) end 155 | table.insert(fmt, '}') 156 | return table.concat(fmt) 157 | end 158 | 159 | -- check if a position{x, y} is inside the hitbox of an object 160 | -- the object must contain a .hitBox = {x1, y1, x2, y2} table 161 | -- return: true if inside 162 | function isInside(obj, pos) 163 | local x, y = pos[1], pos[2] 164 | if obj.hitBox.x1 <= x and x <= obj.hitBox.x2 165 | and obj.hitBox.y1 <= y and y <= obj.hitBox.y2 then 166 | return true 167 | else 168 | return false 169 | end 170 | end 171 | 172 | 173 | -- ass draw alias 174 | -- draw a circle in clockwise direction 175 | function assDrawCirCW(ass, x, y, r) 176 | ass:round_rect_cw(x-r, y-r, x+r, y+r, r) 177 | end 178 | -- draw a circle in counter-clockwise direction 179 | function assDrawCirCCW(ass, x, y, r) 180 | ass:round_rect_ccw(x-r, y-r, x+r, y+r, r) 181 | end 182 | -- draw rectangle 183 | -- r2 is optional 184 | function assDrawRectCW(ass, x1, y1, x2, y2, r1, r2) 185 | ass:round_rect_cw(x1, y1, x2, y2, r1, r2) 186 | end 187 | 188 | function assDrawRectCCW(ass, x1, y1, x2, y2, r1, r2) 189 | ass:round_rect_ccw(x1, y1, x2, y2, r1, r2) 190 | end 191 | -- draw hexagon 192 | -- r2 is optional 193 | function assDrawHexaCW(ass, x1, y1, x2, y2, r1, r2) 194 | ass:hexagon_cw(x1, y1, x2, y2, r1, r2) 195 | end 196 | 197 | function assDrawHexaCCW(ass, x1, y1, x2, y2, r1, r2) 198 | ass:hexagon_ccw(x1, y1, x2, y2, r1, r2) 199 | end 200 | -- draw lines 201 | function assDrawLine(ass, x1, y1, x2, y2) 202 | ass:move_to(x1, y1) 203 | ass:line_to(x2, y2) 204 | end 205 | 206 | function assDrawLineTo(ass, x, y) 207 | ass:line_to(x, y) 208 | end 209 | 210 | 211 | -- # element templates 212 | -- logo 213 | -- shows a logo in the center 214 | local ne = newElement('logo') 215 | ne.init = function(self) 216 | self.geo.x = player.geo.width / 2 217 | self.geo.y = player.geo.height / 2 218 | local ass = assdraw.ass_new() 219 | ass:new_event() 220 | ass:pos(self.geo.x, self.geo.y) 221 | ass:append('{\\1c&H8E348D&\\3c&H0&\\3a&H60&\\blur1\\bord0.5\\4a&HFF&}') 222 | ass:draw_start() 223 | assDrawCirCW(ass, 0, 0, 100) 224 | ass:draw_stop() 225 | 226 | ass:new_event() 227 | ass:pos(self.geo.x, self.geo.y) 228 | ass:append('{\\1c&H632462&\\bord0\\4a&HFF&}') 229 | ass:draw_start() 230 | assDrawCirCW(ass, 6, -6, 75) 231 | ass:draw_stop() 232 | 233 | ass:new_event() 234 | ass:pos(self.geo.x, self.geo.y) 235 | ass:append('{\\1c&HFFFFFF&\\bord0\\4a&HFF&}') 236 | ass:draw_start() 237 | assDrawCirCW(ass, -4, 4, 50) 238 | ass:draw_stop() 239 | 240 | ass:new_event() 241 | ass:pos(self.geo.x, self.geo.y) 242 | ass:append('{\\1c&H632462&\\bord0&\\4a&HFF&}') 243 | ass:draw_start() 244 | ass:move_to(-20, -20) 245 | ass:line_to(23.3, 5) 246 | ass:line_to(-20, 35) 247 | ass:draw_stop() 248 | 249 | ass:new_event() 250 | ass:pos(self.geo.x, player.geo.height - 20) 251 | ass:an(2) 252 | ass:append('{\\fs30\\1c&H0&\\3c&HFFFFFF&\\q2\\4a&HFF&}DROP FILES HERE TO PLAY') 253 | 254 | self.pack[4] = ass.text 255 | end 256 | ne.responder['resize'] = function(self) 257 | self:init() 258 | end 259 | 260 | -- msg 261 | -- display a message in the screen 262 | ne = newElement('message') 263 | ne.geo.x = 40 264 | ne.geo.y = 20 265 | ne.geo.an = 7 266 | ne.layer = 1000 267 | ne.visible = false 268 | ne.text = '' 269 | ne.startTime = 0 270 | ne.duration = 0 271 | ne.style.color = {'ffffff', '0', '0', '333333'} 272 | ne.style.border = 1 273 | ne.style.shadow = 1 274 | ne.render = function(self) 275 | self.pack[4] = self.text 276 | end 277 | ne.tick = function(self) 278 | if not self.visible then return '' end 279 | if player.now-self.startTime >= self.duration then 280 | self.visible = false 281 | end 282 | return table.concat(self.pack) 283 | end 284 | ne.display = function(self, text, duration) 285 | if not duration then duration = 1 end 286 | self.duration = duration 287 | -- text too long may be slow 288 | text = string.sub(text, 0, 2000) 289 | text = string.gsub(text, '\\', '\\\\') 290 | self.text = text 291 | self:render() 292 | self.startTime = player.now 293 | self.visible = true 294 | end 295 | 296 | -- box 297 | -- draw a simple box, usually used as backgrounds 298 | ne = newElement('box') 299 | ne.geo.r = 0 -- corner radius 300 | ne.init = function(self) 301 | self:setPos() 302 | self:setStyle() 303 | self:render() 304 | end 305 | ne.render = function(self) 306 | local ass = assdraw.ass_new() 307 | ass:new_event() 308 | ass:draw_start() 309 | assDrawRectCW(ass, 0, 0, self.geo.w, self.geo.h, self.geo.r) 310 | ass:draw_stop() 311 | self.pack[4] = ass.text 312 | end 313 | 314 | -- button 315 | -- display some content, also respond to mouse button 316 | ne = newElement('button') 317 | ne.enabled = true 318 | ne.text = '' 319 | ne.style.color1 = {'0', '0', '0', '0'} 320 | ne.style.color2 = {'ffffff', 'ffffff', 'ffffff', 'ffffff'} 321 | -- responder active area, left top right bottom 322 | ne.hitBox = {x1 = 0, y1 = 0, x2 = 0, y2 = 0} 323 | ne.init = function(self) 324 | self:setPos() 325 | self:enable() 326 | self:render() 327 | self:setHitBox() 328 | end 329 | ne.render = function(self) 330 | self.pack[4] = self.text 331 | end 332 | ne.enable = function(self) 333 | self.enabled = true 334 | self.style.color = self.style.color1 335 | self:setStyle() 336 | end 337 | ne.disable = function(self) 338 | self.enabled = false 339 | self.style.color = self.style.color2 340 | self:setStyle() 341 | end 342 | ne.setHitBox = function(self) 343 | local x1, y1, x2, y2 = getBoxPos(self.geo) 344 | self.hitBox = {x1 = x1, y1 = y1, x2 = x2, y2 = y2} 345 | end 346 | -- check if mouse event happens inside hitbox 347 | ne.isInside = isInside 348 | 349 | -- tooltip 350 | ne = newElement('tooltip') 351 | ne.visible = false 352 | -- key is optional 353 | -- pos is in '{x, y}' format 354 | ne.show = function(self, text, pos, key) 355 | self.geo.x = pos[1] 356 | self.geo.y = pos[2] 357 | self.pack[4] = text 358 | self.key = key 359 | if self.geo.x < player.geo.width*0.1 then 360 | self.geo.an = 1 361 | self.geo.x = self.geo.x - 15 362 | elseif self.geo.x > player.geo.width*0.9 then 363 | self.geo.an = 3 364 | self.geo.x = self.geo.x + 15 365 | else 366 | self.geo.an = 2 367 | end 368 | self:setPos() 369 | self.visible = true 370 | end 371 | -- update tooltip content regardless of visible status if key matches 372 | ne.update = function(self, text, key) 373 | if self.key == key then 374 | self.pack[4] = text 375 | return true 376 | end 377 | return false 378 | end 379 | -- only hides when key matches, maybe useful for shared tooltip 380 | -- return true if key match 381 | ne.hide = function(self, key) 382 | if self.key == key then 383 | self.visible = false 384 | return true 385 | end 386 | return false 387 | end 388 | ne.responder['mouse_leave'] = function(self) 389 | self.visible = false 390 | end 391 | 392 | -- slider 393 | ne = newElement('slider') 394 | ne.barHeight = 0 395 | ne.barRadius = 0 396 | ne.nobRadius = 0 397 | ne.geo.gap = 0 398 | ne.geo.bar = {x1 = 0, y1 = 0, x2 = 0, y2 = 0, r = 0} -- relative pos 399 | ne.geo.nob = {x = 0, y = 0, r = 0} -- will be flushed by setParam 400 | ne.value = 0 -- 0~100 401 | ne.xMin = 0 402 | ne.xMax = 0 -- min/max x pos 403 | ne.xLength = 0 -- xMax - xMin 404 | ne.xValue = 0 -- value/100 * xLength 405 | ne.style.color1 = {} -- color1 for enabled 406 | ne.style.color2 = {} -- color2 for disabled 407 | ne.enabled = true 408 | ne.hitBox = {} 409 | ne.markers = {} 410 | -- get corresponding slider value at a position 411 | ne.getValueAt = function(self, pos) 412 | local x = pos[1] 413 | local val = (x - self.xMin)*100 / self.xLength 414 | if val < 0 then val = 0 415 | elseif val > 100 then val = 100 end 416 | return val 417 | end 418 | ne.setParam = function(self) 419 | local x1, y1, x2, y2 = getBoxPos(self.geo) 420 | local bar, nob = self.geo.bar, self.geo.nob 421 | self.hitBox = {x1 = x1, y1 = y1, x2 = x2, y2 = y2} 422 | 423 | self.geo.x = x1 424 | self.geo.y = y1 425 | self.geo.an = 7 -- help drawing 426 | 427 | local gap = math.max(self.barRadius, self.nobRadius) 428 | self.xMin = x1 + gap 429 | self.xMax = x2 - gap 430 | self.xLength = self.xMax - self.xMin 431 | self.xValue = self.value/100 * self.xLength 432 | 433 | bar.r = self.barRadius 434 | bar.x1 = gap - bar.r 435 | bar.y1 = (self.geo.h - self.barHeight) / 2 436 | bar.x2 = bar.x1 + self.xValue + 2*bar.r 437 | bar.y2 = bar.y1 + self.barHeight 438 | 439 | nob.x = gap + self.xValue 440 | nob.y = self.geo.h / 2 441 | nob.r = self.nobRadius 442 | 443 | self.geo.gap = gap 444 | end 445 | ne.init = function(self) 446 | self:setParam() 447 | self:setPos() 448 | self:enable() 449 | self:render() 450 | end 451 | ne.render = function(self) 452 | local bar, nob = self.geo.bar, self.geo.nob 453 | bar.x2 = bar.x1 + self.xValue + 2*self.barRadius 454 | nob.x = self.geo.gap + self.xValue 455 | local ass = assdraw.ass_new() 456 | ass:new_event() 457 | ass:draw_start() 458 | -- bar 459 | assDrawRectCW(ass, bar.x1, bar.y1, bar.x2, bar.y2, bar.r) 460 | -- nob 461 | assDrawCirCW(ass, nob.x, nob.y, nob.r) 462 | -- markers 463 | for i, v in ipairs(self.markers) do 464 | local x = v/100 * self.xLength + self.geo.gap 465 | local y1, y2 = self.geo.bar.y1-3, self.geo.bar.y2+3 466 | assDrawRectCW(ass, x-1, y1, x+1, y2, 0) 467 | end 468 | ass:draw_stop() 469 | self.pack[4] = ass.text 470 | end 471 | ne.enable = function(self) 472 | self.enabled = true 473 | self.style.color = self.style.color1 474 | self:setStyle() 475 | end 476 | ne.disable = function(self) 477 | self.enabled = false 478 | self.style.color = self.style.color2 479 | self:setStyle() 480 | end 481 | ne.isInside = isInside -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | -- mpv oscf modern 2 | -- by maoiscat 3 | -- github.com/maoiscat/ 4 | require 'expansion' 5 | local assdraw = require 'mp.assdraw' 6 | 7 | -- user options 8 | opts = { 9 | scale = 1, -- osc render scale 10 | fixedHeight = false, -- true to allow osc scale with window 11 | hideTimeout = 1, -- seconds untile osc hides, negative means never 12 | fadeDuration = 0.5, -- seconds during fade out, negative means never 13 | } 14 | 15 | -- logo and message works out of box 16 | addToIdleLayout('logo') 17 | 18 | -- define styles 19 | local styles = { 20 | background = { 21 | color = {'0', '0', '0', '0'}, 22 | alpha = {255, 255, 0, 0}, 23 | border = 140, 24 | blur = 140, 25 | }, 26 | tooltip = { 27 | color = {'FFFFFF', 'FFFFFF', '0', '0'}, 28 | border = 0.5, 29 | blur = 1, 30 | fontsize = 18, 31 | wrap = 2, 32 | }, 33 | button1 = { 34 | color1 = {'FFFFFF', 'FFFFFF', 'FFFFFF', 'FFFFFF'}, 35 | color2 = {'999999', '999999', '999999', '999999'}, 36 | fontsize = 36, 37 | border = 0, 38 | blur = 0, 39 | font = 'material-design-iconic-font', 40 | wrap = 2, 41 | }, 42 | button2 = { 43 | color1 = {'FFFFFF', 'FFFFFF', 'FFFFFF', 'FFFFFF'}, 44 | color2 = {'999999', '999999', '999999', '999999'}, 45 | border = 0, 46 | blur = 0, 47 | fontsize = 24, 48 | font = 'material-design-iconic-font', 49 | wrap = 2, 50 | }, 51 | seekbarFg = { 52 | color1 = {'E39C42', 'E39C42', '0', '0'}, 53 | color2 = {'999999', '999999', '0', '0'}, 54 | border = 0.5, 55 | blur = 1, 56 | }, 57 | seekbarBg = { 58 | color = {'eeeeee', 'eeeeee', '0', '0'}, 59 | border = 0, 60 | blur = 0, 61 | }, 62 | volumeSlider = { 63 | color = {'ffffff', '0', '0', '0'}, 64 | border = 0, 65 | blur = 0, 66 | }, 67 | time = { 68 | color1 = {'ffffff', 'ffffff', '0', '0'}, 69 | color2 = {'eeeeee', 'eeeeee', '0', '0'}, 70 | border = 0, 71 | blur = 0, 72 | fontsize = 17, 73 | }, 74 | title = { 75 | color = {'ffffff', '0', '0', '0'}, 76 | border = 0.5, 77 | blur = 1, 78 | fontsize = 48, 79 | wrap = 2, 80 | }, 81 | winControl = { 82 | color1 = {'ffffff', 'ffffff', '0', '0'}, 83 | color2 = {'eeeeee', 'eeeeee', '0', '0'}, 84 | border = 0.5, 85 | blur = 1, 86 | font = 'mpv-osd-symbols', 87 | fontsize = 20, 88 | }, 89 | } 90 | 91 | -- enviroment updater 92 | -- this element updates shared vairables, sets active areas and starts event generators 93 | local env 94 | env = newElement('env') 95 | env.layer = 1000 96 | env.visible = false 97 | env.updateTime = function() 98 | dispatchEvent('time') 99 | end 100 | env.init = function(self) 101 | self.slowTimer = mp.add_periodic_timer(0.25, self.updateTime) --use a slower timer to update playtime 102 | -- event generators 103 | mp.observe_property('track-list/count', 'native', 104 | function(name, val) 105 | if val==0 then return end 106 | player.tracks = getTrackList() 107 | player.playlist = getPlaylist() 108 | player.chapters = getChapterList() 109 | player.playlistPos = getPlaylistPos() 110 | player.duration = mp.get_property_number('duration') 111 | showOsc() 112 | dispatchEvent('file-loaded') 113 | end) 114 | mp.observe_property('pause', 'bool', 115 | function(name, val) 116 | player.paused = val 117 | dispatchEvent('pause') 118 | end) 119 | mp.observe_property('fullscreen', 'bool', 120 | function(name, val) 121 | player.fullscreen = val 122 | dispatchEvent('fullscreen') 123 | end) 124 | mp.observe_property('window-maximized', 'bool', 125 | function(name, val) 126 | player.maximized = val 127 | dispatchEvent('window-maximized') 128 | end) 129 | mp.observe_property('current-tracks/audio/id', 'number', 130 | function(name, val) 131 | if val then player.audioTrack = val 132 | else player.audioTrack = 0 133 | end 134 | dispatchEvent('audio-changed') 135 | end) 136 | mp.observe_property('current-tracks/sub/id', 'number', 137 | function(name, val) 138 | if val then player.subTrack = val 139 | else player.subTrack = 0 140 | end 141 | dispatchEvent('sub-changed') 142 | end) 143 | mp.observe_property('mute', 'bool', 144 | function(name, val) 145 | player.muted = val 146 | dispatchEvent('mute') 147 | end) 148 | mp.observe_property('volume', 'number', 149 | function(name, val) 150 | player.volume = val 151 | dispatchEvent('volume') 152 | end) 153 | end 154 | env.tick = function(self) 155 | player.percentPos = mp.get_property_number('percent-pos') 156 | player.timePos = mp.get_property_number('time-pos') 157 | player.timeRem = mp.get_property_number('time-remaining') 158 | return '' 159 | end 160 | env.responder['resize'] = function(self) 161 | player.geo.refX = player.geo.width / 2 162 | player.geo.refY = player.geo.height - 40 163 | setPlayActiveArea('bg1', 0, player.geo.height - 120, player.geo.width, player.geo.height) 164 | if player.fullscreen then 165 | setPlayActiveArea('wc1', player.geo.width - 200, 0, player.geo.width, 48) 166 | else 167 | setPlayActiveArea('wc1', -1, -1, -1, -1) 168 | end 169 | return false 170 | end 171 | env.responder['pause'] = function(self) 172 | if player.idle then return end 173 | if player.paused then 174 | setVisibility('always') 175 | else 176 | setVisibility('normal') 177 | end 178 | end 179 | env.responder['idle'] = function(self) 180 | if player.idle then 181 | setVisibility('always') 182 | else 183 | setVisibility('normal') 184 | end 185 | return false 186 | end 187 | env:init() 188 | addToPlayLayout('env') 189 | 190 | -- background 191 | local ne 192 | ne = newElement('background', 'box') 193 | ne.geo.h = 1 194 | ne.geo.an = 8 195 | ne.layer = 5 196 | -- DO NOT directly assign a shared style tabe!! 197 | ne.style = clone(styles.background) 198 | ne.responder['resize'] = function(self) 199 | self.geo.x = player.geo.refX 200 | self.geo.y = player.geo.height 201 | self.geo.w = player.geo.width 202 | self.setPos(self) 203 | self.render(self) 204 | return false 205 | end 206 | ne:init() 207 | addToPlayLayout('background') 208 | 209 | -- a shared tooltip 210 | ne = newElement('tip', 'tooltip') 211 | ne.layer = 20 212 | ne.style = clone(styles.tooltip) 213 | ne:init() 214 | addToPlayLayout('tip') 215 | local tooltip = ne 216 | 217 | -- playpause button 218 | ne = newElement('btnPlay', 'button') 219 | ne.layer = 10 220 | ne.style = clone(styles.button1) 221 | ne.geo.w = 45 222 | ne.geo.h = 45 223 | ne.geo.an = 5 224 | ne.responder['resize'] = function(self) 225 | self.geo.x = player.geo.refX 226 | self.geo.y = player.geo.refY 227 | self:setPos() 228 | self:setHitBox() 229 | return false 230 | end 231 | ne.responder['mbtn_left_up'] = function(self, pos) 232 | if self.enabled and self:isInside(pos) then 233 | mp.commandv('cycle', 'pause') 234 | return true 235 | end 236 | return false 237 | end 238 | ne.responder['pause'] = function(self) 239 | if player.paused then 240 | self.text = '\xEF\x8E\xAA' 241 | else 242 | self.text = '\xEF\x8E\xA7' 243 | end 244 | self:render() 245 | return false 246 | end 247 | ne:init() 248 | addToPlayLayout('btnPlay') 249 | 250 | 251 | -- skip back button 252 | ne = newElement('btnBack', 'button') 253 | ne.layer = 10 254 | ne.style = clone(styles.button2) 255 | ne.geo.w = 30 256 | ne.geo.h = 24 257 | ne.geo.an = 5 258 | ne.text = '\xEF\x8E\xA0' 259 | ne.responder['resize'] = function(self) 260 | self.geo.x = player.geo.refX - 60 261 | self.geo.y = player.geo.refY 262 | self:setPos() 263 | self:setHitBox() 264 | return false 265 | end 266 | ne.responder['mbtn_left_up'] = function(self, pos) 267 | if self.enabled and self:isInside(pos) then 268 | mp.commandv('seek', -5, 'relative', 'keyframes') 269 | return true 270 | end 271 | return false 272 | end 273 | ne:init() 274 | addToPlayLayout('btnBack') 275 | 276 | 277 | -- skip forward button 278 | ne = newElement('btnForward', 'btnBack') 279 | ne.text = '\xEF\x8E\x9F' 280 | ne.responder['mbtn_left_up'] = function(self, pos) 281 | if self.enabled and self:isInside(pos) then 282 | mp.commandv('seek', 5, 'relative', 'keyframes') 283 | return true 284 | end 285 | return false 286 | end 287 | ne.responder['resize'] = function(self) 288 | self.geo.x = player.geo.refX + 60 289 | self.geo.y = player.geo.refY 290 | self:setPos() 291 | self:setHitBox() 292 | return false 293 | end 294 | ne:init() 295 | addToPlayLayout('btnForward') 296 | 297 | 298 | -- play previous file button 299 | ne = newElement('btnPrev', 'button') 300 | ne.layer = 10 301 | ne.style = clone(styles.button2) 302 | ne.geo.w = 30 303 | ne.geo.h = 24 304 | ne.geo.an = 5 305 | ne.text = '\xEF\x8E\xB5' 306 | ne.responder['mbtn_left_up'] = function(self, pos) 307 | if self.enabled and self:isInside(pos) then 308 | mp.commandv('playlist-prev', 'weak') 309 | return true 310 | end 311 | return false 312 | end 313 | ne.responder['resize'] = function(self) 314 | self.geo.x = player.geo.refX - 120 315 | self.geo.y = player.geo.refY 316 | self:setPos() 317 | self:setHitBox() 318 | return false 319 | end 320 | ne.responder['file-loaded'] = function(self) 321 | if player.playlistPos <= 1 and player.loopPlaylist == 'no' then 322 | self:disable() 323 | else 324 | self:enable() 325 | end 326 | return false 327 | end 328 | ne:init() 329 | addToPlayLayout('btnPrev') 330 | 331 | 332 | -- play next file button 333 | ne = newElement('btnNext', 'btnPrev') 334 | ne.text = '\xEF\x8E\xB4' 335 | ne.responder['mbtn_left_up'] = function(self, pos) 336 | if self.enabled and self:isInside(pos) then 337 | mp.commandv('playlist-next', 'weak') 338 | return true 339 | end 340 | return false 341 | end 342 | ne.responder['resize'] = function(self) 343 | self.geo.x = player.geo.refX + 120 344 | self.geo.y = player.geo.refY 345 | self:setPos() 346 | self:setHitBox() 347 | return false 348 | end 349 | ne.responder['file-loaded'] = function(self) 350 | if player.playlistPos >= #player.playlist 351 | and player.loopPlaylist == 'no' then 352 | self:disable() 353 | else 354 | self:enable() 355 | end 356 | return false 357 | end 358 | ne:init() 359 | addToPlayLayout('btnNext') 360 | 361 | -- cycle audio button 362 | ne = newElement('cycleAudio', 'button') 363 | ne.layer = 10 364 | ne.style = clone(styles.button2) 365 | ne.geo.w = 30 366 | ne.geo.h = 24 367 | ne.geo.an = 5 368 | ne.text = '\xEF\x8E\xB7' 369 | ne.tipText = '' 370 | ne.responder['resize'] = function(self) 371 | self.geo.x = 37 372 | self.geo.y = player.geo.refY 373 | self.visible = player.geo.width >= 540 374 | self:setPos() 375 | self:setHitBox() 376 | return false 377 | end 378 | ne.responder['mouse_move'] = function(self, pos) 379 | if self.enabled and self:isInside(pos) then 380 | tooltip:show(self.tipText, {self.geo.x, self.geo.y+30}, self) 381 | return true 382 | else 383 | tooltip:hide(self) 384 | return false 385 | end 386 | end 387 | ne.responder['file-loaded'] = function(self) 388 | if #player.tracks.audio > 0 then 389 | self:enable() 390 | else 391 | self:disable() 392 | end 393 | end 394 | ne.responder['audio-changed'] = function(self) 395 | if player.tracks then 396 | local lang 397 | if player.audioTrack == 0 then 398 | lang = 'OFF' 399 | else 400 | lang = player.tracks.audio[player.audioTrack].lang 401 | end 402 | if not lang then lang = 'unknown' end 403 | self.tipText = string.format('[%s/%s][%s]', 404 | player.audioTrack, #player.tracks.audio, lang) 405 | tooltip:update(self.tipText, self) 406 | end 407 | return false 408 | end 409 | ne.responder['mbtn_left_up'] = function(self, pos) 410 | if self.enabled and self:isInside(pos) then 411 | cycleTrack('audio') 412 | return true 413 | end 414 | return false 415 | end 416 | ne.responder['mbtn_right_up'] = function(self, pos) 417 | if self.enabled and self:isInside(pos) then 418 | cycleTrack('audio', 'prev') 419 | return true 420 | end 421 | return false 422 | end 423 | ne:init() 424 | addToPlayLayout('cycleAudio') 425 | 426 | 427 | -- cycle sub button 428 | ne = newElement('cycleSub', 'cycleAudio') 429 | ne.text = '\xEF\x8F\x93' 430 | ne.responder['resize'] = function(self) 431 | self.geo.x = 87 432 | self.geo.y = player.geo.refY 433 | self.visible = player.geo.width >= 600 434 | self:setPos() 435 | self:setHitBox() 436 | return false 437 | end 438 | ne.responder['file-loaded'] = function(self) 439 | if #player.tracks.sub > 0 then 440 | self:enable() 441 | else 442 | self:disable() 443 | end 444 | end 445 | ne.responder['audio-changed'] = nil 446 | ne.responder['sub-changed'] = function(self) 447 | if player.tracks then 448 | local title 449 | if player.subTrack == 0 then 450 | title = 'OFF' 451 | else 452 | title = player.tracks.sub[player.subTrack].title 453 | end 454 | if not title then title = 'unknown' end 455 | self.tipText = string.format('[%s/%s][%s]', 456 | player.subTrack, #player.tracks.sub, title) 457 | tooltip:update(self.tipText, self) 458 | end 459 | return false 460 | end 461 | ne.responder['mbtn_left_up'] = function(self, pos) 462 | if self.enabled and self:isInside(pos) then 463 | cycleTrack('sub') 464 | return true 465 | end 466 | return false 467 | end 468 | ne.responder['mbtn_right_up'] = function(self, pos) 469 | if self.enabled and self:isInside(pos) then 470 | cycleTrack('sub', 'prev') 471 | return true 472 | end 473 | return false 474 | end 475 | ne:init() 476 | addToPlayLayout('cycleSub') 477 | 478 | -- toggle mute 479 | ne = newElement('togMute', 'button') 480 | ne.layer = 10 481 | ne.style = clone(styles.button2) 482 | ne.geo.x = 137 483 | ne.geo.w = 30 484 | ne.geo.h = 24 485 | ne.geo.an = 5 486 | ne.responder['resize'] = function(self) 487 | self.geo.y = player.geo.refY 488 | self.visible = player.geo.width >= 700 489 | self:setPos() 490 | self:setHitBox() 491 | return false 492 | end 493 | ne.responder['mbtn_left_up'] = function(self, pos) 494 | if self.enabled and self:isInside(pos) then 495 | mp.commandv('cycle', 'mute') 496 | return true 497 | end 498 | return false 499 | end 500 | ne.responder['mute'] = function(self) 501 | if player.muted then 502 | self.text = '\xEF\x8E\xBB' 503 | else 504 | self.text = '\xEF\x8E\xBC' 505 | end 506 | self:render() 507 | return false 508 | end 509 | ne:init() 510 | addToPlayLayout('togMute', 'button') 511 | 512 | -- volume slider 513 | -- background 514 | ne = newElement('volumeSliderBg', 'box') 515 | ne.layer = 9 516 | ne.style = clone(styles.volumeSlider) 517 | ne.geo.r = 0 518 | ne.geo.h = 1 519 | ne.geo.an = 4 520 | ne.responder['resize'] = function(self) 521 | self.visible = player.geo.width > 740 522 | self.geo.x = 156 523 | self.geo.y = player.geo.refY 524 | self.geo.w = 80 525 | self:init() 526 | end 527 | ne:init() 528 | addToPlayLayout('volumeSliderBg') 529 | 530 | -- seekbar 531 | ne = newElement('volumeSlider', 'slider') 532 | ne.layer = 10 533 | ne.style = clone(styles.volumeSlider) 534 | ne.geo.an = 4 535 | ne.geo.h = 14 536 | ne.barHeight = 2 537 | ne.barRadius = 0 538 | ne.nobRadius = 4 539 | ne.allowDrag = false 540 | ne.lastSeek = nil 541 | ne.responder['resize'] = function(self) 542 | self.visible = player.geo.width > 740 543 | self.geo.an = 4 544 | self.geo.x = 152 545 | self.geo.y = player.geo.refY 546 | self.geo.w = 88 547 | self:setParam() -- setParam may change geo settings 548 | self:setPos() 549 | self:render() 550 | end 551 | ne.responder['volume'] = function(self) 552 | local val = player.volume 553 | if val then 554 | if val > 140 then val = 140 555 | elseif val < 0 then val = 0 end 556 | self.value = val/1.4 557 | self.xValue = val/140 * self.xLength 558 | self:render() 559 | end 560 | return false 561 | end 562 | ne.responder['idle'] = ne.responder['volume'] 563 | ne.responder['mouse_move'] = function(self, pos) 564 | if not self.enabled then return false end 565 | local vol = self:getValueAt(pos) 566 | if self.allowDrag then 567 | if vol then 568 | mp.commandv('set', 'volume', vol*1.4) 569 | env.updateTime() 570 | end 571 | end 572 | if self:isInside(pos) then 573 | local tipText 574 | if vol then 575 | tipText = string.format('%d', vol*1.4) 576 | else 577 | tipText = 'N/A' 578 | end 579 | tooltip:show(tipText, {pos[1], self.geo.y}, self) 580 | return true 581 | else 582 | tooltip:hide(self) 583 | return false 584 | end 585 | end 586 | ne.responder['mbtn_left_down'] = function(self, pos) 587 | if not self.enabled then return false end 588 | if self:isInside(pos) then 589 | self.allowDrag = true 590 | local vol = self:getValueAt(pos) 591 | if vol then 592 | mp.commandv('set', 'volume', vol*1.4) 593 | return true 594 | end 595 | end 596 | return false 597 | end 598 | ne.responder['mbtn_left_up'] = function(self, pos) 599 | self.allowDrag = false 600 | self.lastSeek = nil 601 | end 602 | ne:init() 603 | addToPlayLayout('volumeSlider') 604 | 605 | 606 | -- toggle info 607 | ne = newElement('togInfo', 'button') 608 | ne.layer = 10 609 | ne.style = clone(styles.button2) 610 | ne.geo.w = 30 611 | ne.geo.h = 24 612 | ne.geo.an = 5 613 | ne.text = '\xEF\x87\xB7' 614 | ne.responder['resize'] = function(self) 615 | self.geo.x = player.geo.width - 87 616 | self.geo.y = player.geo.refY 617 | self.visible = player.geo.width >= 640 618 | self:setPos() 619 | self:setHitBox() 620 | return false 621 | end 622 | ne.responder['mbtn_left_up'] = function(self, pos) 623 | if self.enabled and self:isInside(pos) then 624 | mp.commandv('script-binding', 'stats/display-stats-toggle') 625 | return true 626 | end 627 | return false 628 | end 629 | ne:init() 630 | addToPlayLayout('togInfo') 631 | 632 | 633 | -- toggle fullscreen 634 | ne = newElement('togFs', 'togInfo') 635 | ne.text = '\xEF\x85\xAD' 636 | ne.responder['resize'] = function(self) 637 | self.geo.x = player.geo.width - 37 638 | self.geo.y = player.geo.refY 639 | self.visible = player.geo.width >= 600 640 | if (player.fullscreen) then 641 | self.text = '\xEF\x85\xAC' 642 | else 643 | self.text = '\xEF\x85\xAD' 644 | end 645 | self:render() 646 | self:setPos() 647 | self:setHitBox() 648 | return false 649 | end 650 | ne.responder['mbtn_left_up'] = function(self, pos) 651 | if self.enabled and self:isInside(pos) then 652 | mp.commandv('cycle', 'fullscreen') 653 | return true 654 | end 655 | return false 656 | end 657 | ne:init() 658 | addToPlayLayout('togFs') 659 | 660 | -- seekbar background 661 | ne = newElement('seekbarBg', 'box') 662 | ne.layer = 9 663 | ne.style = clone(styles.seekbarBg) 664 | ne.geo.r = 0 665 | ne.geo.h = 2 666 | ne.geo.an = 5 667 | ne.responder['resize'] = function(self) 668 | self.geo.x = player.geo.refX 669 | self.geo.y = player.geo.refY - 56 670 | self.geo.w = player.geo.width - 50 671 | self:init() 672 | end 673 | ne:init() 674 | addToPlayLayout('seekbarBg') 675 | 676 | -- seekbar 677 | ne = newElement('seekbar', 'slider') 678 | ne.layer = 10 679 | ne.style = clone(styles.seekbarFg) 680 | ne.geo.an = 5 681 | ne.geo.h = 20 682 | ne.barHeight = 2 683 | ne.barRadius = 0 684 | ne.nobRadius = 8 685 | ne.allowDrag = false 686 | ne.lastSeek = nil 687 | ne.responder['resize'] = function(self) 688 | self.geo.an = 5 689 | self.geo.x = player.geo.refX 690 | self.geo.y = player.geo.refY - 56 691 | self.geo.w = player.geo.width - 34 692 | self:setParam() -- setParam may change geo settings 693 | self:setPos() 694 | self:render() 695 | end 696 | ne.responder['time'] = function(self) 697 | local val = player.percentPos 698 | if val and not self.enabled then 699 | self:enable() 700 | elseif not val and self.enabled then 701 | tooltip:hide(self) 702 | self:disable() 703 | end 704 | if val then 705 | self.value = val 706 | self.xValue = val/100 * self.xLength 707 | self:render() 708 | end 709 | return false 710 | end 711 | ne.responder['mouse_move'] = function(self, pos) 712 | if not self.enabled then return false end 713 | if self.allowDrag then 714 | local seekTo = self:getValueAt(pos) 715 | if seekTo then 716 | mp.commandv('seek', seekTo, 'absolute-percent') 717 | env.updateTime() 718 | end 719 | end 720 | if self:isInside(pos) then 721 | local tipText 722 | if player.duration then 723 | local seconds = self:getValueAt(pos)/100 * player.duration 724 | if #player.chapters > 0 then 725 | local ch = #player.chapters 726 | for i, v in ipairs(player.chapters) do 727 | if seconds < v.time then 728 | ch = i - 1 729 | break 730 | end 731 | end 732 | if ch == 0 then 733 | tipText = string.format('[0/%d][unknown]\\N%s', 734 | #player.chapters, mp.format_time(seconds)) 735 | else 736 | local title = player.chapters[ch].title 737 | if not title then title = 'unknown' end 738 | tipText = string.format('[%d/%d][%s]\\N%s', 739 | ch, #player.chapters, title, 740 | mp.format_time(seconds)) 741 | end 742 | else 743 | tipText = mp.format_time(seconds) 744 | end 745 | else 746 | tipText = '--:--:--' 747 | end 748 | tooltip:show(tipText, {pos[1], self.geo.y}, self) 749 | return true 750 | else 751 | tooltip:hide(self) 752 | return false 753 | end 754 | end 755 | ne.responder['mbtn_left_down'] = function(self, pos) 756 | if not self.enabled then return false end 757 | if self:isInside(pos) then 758 | self.allowDrag = true 759 | local seekTo = self:getValueAt(pos) 760 | if seekTo then 761 | mp.commandv('seek', seekTo, 'absolute-percent') 762 | env.updateTime() 763 | return true 764 | end 765 | end 766 | return false 767 | end 768 | ne.responder['mbtn_left_up'] = function(self, pos) 769 | self.allowDrag = false 770 | self.lastSeek = nil 771 | end 772 | ne.responder['file-loaded'] = function(self) 773 | -- update chapter markers 774 | env.updateTime() 775 | self.markers = {} 776 | if player.duration then 777 | for i, v in ipairs(player.chapters) do 778 | self.markers[i] = (v.time*100 / player.duration) 779 | end 780 | self:render() 781 | end 782 | return false 783 | end 784 | ne:init() 785 | addToPlayLayout('seekbar') 786 | 787 | -- time display 788 | ne = newElement('time1', 'button') 789 | ne.layer = 10 790 | ne.style = clone(styles.time) 791 | ne.geo.w = 64 792 | ne.geo.h = 20 793 | ne.geo.an = 7 794 | ne.enabled = true 795 | ne.responder['resize'] = function(self) 796 | self.geo.x = 25 797 | self.geo.y = player.geo.refY - 44 798 | self:setPos() 799 | end 800 | ne.responder['time'] = function(self) 801 | if player.timePos then 802 | self.pack[4] = mp.format_time(player.timePos) 803 | else 804 | self.pack[4] = '--:--:--' 805 | end 806 | end 807 | ne:init() 808 | addToPlayLayout('time1') 809 | 810 | -- time duration 811 | ne = newElement('time2', 'time1') 812 | ne.geo.an = 9 813 | ne.isDuration = true 814 | ne.responder['resize'] = function(self) 815 | self.geo.x = player.geo.width - 25 816 | self.geo.y = player.geo.refY - 44 817 | self:setPos() 818 | self:setHitBox() 819 | end 820 | ne.responder['time'] = function(self) 821 | if self.isDuration then 822 | val = player.duration 823 | else 824 | val = -player.timeRem 825 | end 826 | if val then 827 | self.pack[4] = mp.format_time(val) 828 | else 829 | self.pack[4] = '--:--:--' 830 | end 831 | end 832 | ne.responder['mbtn_left_up'] = function(self, pos) 833 | if self:isInside(pos) then 834 | self.isDuration = not self.isDuration 835 | return true 836 | end 837 | return false 838 | end 839 | ne:init() 840 | addToPlayLayout('time2') 841 | 842 | -- title 843 | ne = newElement('title') 844 | ne.layer = 10 845 | ne.style = clone(styles.title) 846 | ne.geo.x = 20 847 | ne.geo.an = 1 848 | ne.visible = false 849 | ne.title = '' 850 | ne.render = function(self) 851 | local maxchars = player.geo.width / 23 852 | local text = self.title 853 | -- 估计1个中文字符约等于1.5个英文字符 854 | local charcount = (text:len() + select(2, text:gsub('[^\128-\193]', ''))*2) / 3 855 | if not (maxchars == nil) and (charcount > maxchars) then 856 | local limit = math.max(0, maxchars - 3) 857 | if (charcount > limit) then 858 | while (charcount > limit) do 859 | text = text:gsub('.[\128-\191]*$', '') 860 | charcount = (text:len() + select(2, text:gsub('[^\128-\193]', ''))*2) / 3 861 | end 862 | text = text .. '...' 863 | end 864 | end 865 | self.pack[4] = text 866 | end 867 | ne.tick = function(self) 868 | if not self.visible then return '' end 869 | if self.trans >= 0.9 then 870 | self.visible = false 871 | end 872 | return table.concat(self.pack) 873 | end 874 | ne.responder['resize'] = function(self) 875 | self.geo.y = player.geo.refY - 92 876 | self:setPos() 877 | self:render() 878 | self.visible = self.visible and (player.geo.height >= 320) 879 | end 880 | ne.responder['pause'] = function(self) 881 | self.visible = (self.visible or player.paused) and (player.geo.height >= 320) 882 | end 883 | ne.responder['file-loaded'] = function(self) 884 | local title = mp.command_native({'expand-text', '${media-title}'}) 885 | title = title:gsub('\\n', ' '):gsub('\\$', ''):gsub('{','\\{') 886 | self.title = title 887 | self:render() 888 | self.visible = true 889 | end 890 | ne.responder['idle'] = function(self) 891 | self.visible = not player.idle 892 | return false 893 | end 894 | ne:init() 895 | addToPlayLayout('title') 896 | 897 | -- window controllers 898 | ne = newElement('winClose', 'button') 899 | ne.layer = 10 900 | ne.style = clone(styles.winControl) 901 | ne.geo.y = 16 902 | ne.geo.w = 40 903 | ne.geo.h = 32 904 | ne.geo.an = 5 905 | ne.text = '\238\132\149' 906 | ne.responder['resize'] = function(self) 907 | self.geo.x = player.geo.width - 20 908 | self:init() 909 | end 910 | ne.responder['mbtn_left_up'] = function(self, pos) 911 | if self.visible and self:isInside(pos) then 912 | mp.commandv('quit') 913 | return true 914 | end 915 | return false 916 | end 917 | ne.responder['fullscreen'] = function(self) 918 | self.visible = player.fullscreen 919 | end 920 | ne:init() 921 | addToPlayLayout('winClose') 922 | 923 | 924 | ne = newElement('winMax', 'winClose') 925 | ne.text = '' 926 | ne.responder['resize'] = function(self) 927 | self.geo.x = player.geo.width - 60 928 | if player.maximized or player.fullscreen then 929 | self.text = '\238\132\148' 930 | else 931 | self.text = '\238\132\147' 932 | end 933 | self:init() 934 | end 935 | ne.responder['mbtn_left_up'] = function(self, pos) 936 | if self.visible and self:isInside(pos) then 937 | if player.fullscreen then 938 | mp.commandv('cycle', 'fullscreen') 939 | else 940 | mp.commandv('cycle', 'window-maximized') 941 | end 942 | return true 943 | end 944 | return false 945 | end 946 | ne:init() 947 | addToPlayLayout('winMax') 948 | 949 | ne = newElement('winMin', 'winClose') 950 | ne.text = '\238\132\146' 951 | ne.responder['resize'] = function(self) 952 | self.geo.x = player.geo.width - 100 953 | self:init() 954 | end 955 | ne.responder['mbtn_left_up'] = function(self, pos) 956 | if self.visible and self:isInside(pos) then 957 | mp.commandv('cycle', 'window-minimized') 958 | return true 959 | end 960 | return false 961 | end 962 | ne:init() 963 | addToPlayLayout('winMin') 964 | -------------------------------------------------------------------------------- /oscf.lua: -------------------------------------------------------------------------------- 1 | -- osc framework 2 | -- ver 1.5 3 | 4 | -- # variables 5 | -- player status for public access, generated by program 6 | -- changing their values do not interfere the framework 7 | player = { 8 | now = 0, -- a copy of local now for public access 9 | geo = {width = 0, height = 0, aspect = 0}, 10 | idle = true, -- idle status 11 | } 12 | -- user options, altering them may change osc behavior 13 | opts = { 14 | scale = 1, -- osc render scale 15 | fixedHeight = false, -- true means osc y resolution is fixed 480, all elements scale with player window height 16 | hideTimeout = 1, -- seconds untile osc hides, negative means never 17 | fadeDuration = 0.5, -- seconds during fade out, negative means never 18 | } 19 | 20 | -- local variables, users do not touch them 21 | local elements = {} -- all available elements 22 | local elementsInUse = {} -- an element added to a layout will be marked here 23 | local layouts = {idle = {}, play = {}} -- tow layouts: idle, play 24 | local oscLayout = 'idle' -- selected layout will be rendered 25 | local activeAreas = { -- mouse in these rectangle areas avtivate osc and key bindings 26 | idle = {}, play = {}} -- example: idle['name'] = {x1=n1, y1=n2, x2=n3, y2=n4, prop=''} 27 | local active = false -- true if mouse is in activeArea 28 | local osd = mp.create_osd_overlay('ass-events') 29 | local tickTimer = nil -- osc timer objects 30 | local tickDelay = 0.03 -- 33 fps limit 31 | local now = 0 -- now time, updated by tick() 32 | local mouseScale = 1 -- mouse positon scale 33 | local visible = false -- osc visiblility 34 | local visualMode = 'out' -- visual mode: in, out, always, hide 35 | local fadeLastTime = 0 -- fade effect time base 36 | local fadeFactor = 0 -- fade factor as transparency modifier 37 | 38 | -- # basic osc functions 39 | -- set osd display 40 | -- text: in ASS format 41 | local function setOsd(text) 42 | if text == osd.data then return end 43 | osd.data = text 44 | osd:update() 45 | end 46 | 47 | -- update visual effects, which deal with the fade out effect 48 | local function updateVisual() 49 | if visualMode == 'in' then -- osc is shown 50 | visible = true 51 | fadeFactor = 0 52 | if opts.hideTimeout < 0 then return end 53 | if now - fadeLastTime >= opts.hideTimeout then 54 | visualMode = 'out' 55 | fadeLastTime = now 56 | end 57 | elseif visualMode == 'out' then -- fading out 58 | if opts.fadeDuration < 0 then return end 59 | local time = now - fadeLastTime 60 | local factor = time / opts.fadeDuration 61 | if factor > 1 then factor = 1 end 62 | fadeFactor = factor 63 | if time >= opts.fadeDuration then visible = false end 64 | elseif visualMode == 'always' then 65 | fadeFactor = 0 66 | visible = true 67 | elseif visualMode == 'hide' then 68 | visible = false 69 | end 70 | end 71 | 72 | function getVisibility() 73 | if visualMode == 'in' or visualMode == 'out' then 74 | return 'normal' 75 | elseif visualMode == 'always' then 76 | return 'always' 77 | elseif visualMode == 'hide' then 78 | return 'hide' 79 | else return 'unknown' end 80 | end 81 | 82 | -- set osc visibility 83 | -- mode: 'normal', 'always', 'hide' 84 | function setVisibility(mode) 85 | if mode == 'normal' then 86 | visualMode = 'in' 87 | elseif mode == 'always' then 88 | visualMode = 'always' 89 | elseif mode == 'hide' then 90 | visualMode = 'hide' 91 | end 92 | end 93 | 94 | -- show osc if it's faded out 95 | function showOsc() 96 | if visualMode == 'in' or visualMode == 'out' then 97 | visualMode = 'in' 98 | fadeLastTime = now 99 | end 100 | end 101 | 102 | -- render a selected layout 103 | local renderLayout = { 104 | idle = function() 105 | local text = {} 106 | for i, e in ipairs(layouts.idle) do 107 | text[i] = e.tick(e) 108 | end 109 | setOsd(table.concat(text, '\n')) 110 | end, 111 | play = function() 112 | local text = {} 113 | updateVisual() 114 | if visible then 115 | for i, e in ipairs(layouts.play) do 116 | e.setAlpha(e, fadeFactor) -- remix fade effect 117 | text[i] = e.tick(e) 118 | end 119 | end 120 | setOsd(table.concat(text, '\n')) 121 | end 122 | } 123 | 124 | -- called by mpv timer periodically 125 | local function tick() 126 | now = mp.get_time() 127 | player.now = now 128 | if active then showOsc() end 129 | renderLayout[oscLayout]() 130 | end 131 | 132 | -- called on player resize 133 | local function oscResize() 134 | local baseWidth, baseHeight = 720, 480 135 | local dispWidth, dispHeight, dispAspect = mp.get_osd_size() 136 | if dispAspect > 0 then -- in some cases osd size could be zero, need to check 137 | if opts.fixedHeight then -- if true, baseWidth is calculated according to baseHeight 138 | baseWidth = baseHeight * dispAspect 139 | else -- or else, use real window size 140 | baseWidth, baseHeight = dispWidth, dispHeight 141 | end 142 | end 143 | local x = baseWidth / opts.scale 144 | local y = baseHeight / opts.scale 145 | player.geo = { 146 | width = x, 147 | height = y, 148 | aspect = dispAspect 149 | } 150 | -- set osd resolution 151 | osd.res_x = x 152 | osd.res_y = y 153 | -- display positon may not be actual mouse position. need scale 154 | if dispHeight > 0 then 155 | mouseScale = y / dispHeight 156 | else 157 | mouseScale = 0 158 | end 159 | end 160 | 161 | -- initialize 162 | local function init() 163 | -- init osd params 164 | osd.res_x = 0 165 | osd.res_y = 0 166 | osd.z = 10 167 | -- init timer 168 | tickTimer = mp.add_periodic_timer(tickDelay, tick) 169 | -- disable internal osc 170 | mp.commandv('set', 'osc', 'no') 171 | end 172 | 173 | -- osc starts here 174 | init() 175 | 176 | -- # element management 177 | local elements = {} 178 | -- creat a default element as a template 179 | elements['default'] = { 180 | -- z order 181 | layer = 0, 182 | -- geometry, left, right, width, height, alignmet 183 | geo = {x = 0, y = 0, w = 0, h = 0, an = 7}, 184 | -- global transparency modifier 185 | trans = 0, 186 | -- render style, each property can be optional 187 | style = { 188 | -- primary, secondary, outline, back colors in BGR format 189 | -- back color only works when "osd-back-color" is set in mpv 190 | color = {'ffffff', 'ffffff', 'ffffff', 'ffffff'}, 191 | -- transparency, 0~255, 255 is invisible 192 | alpha = {0, 0, 0, 255}, 193 | -- border size, decimal 194 | border = 0, 195 | -- blur size, decimal 196 | blur = 0, 197 | -- shadow size, decimal 198 | shadow = 0, 199 | -- font name, string 200 | font = '', 201 | -- fontsize, decimal 202 | fontsize = 10, 203 | -- 0-auto, 1-end wrap, 2-no wrap, 3-auto2 204 | wrap = 2, 205 | }, 206 | visible = true, 207 | -- pack[1] - position codes 208 | -- pack[2] - alpha codes 209 | -- pack[3] - other style codes 210 | -- pack[4] - other contents 211 | -- pack[>4] also works if you like 212 | pack = {'', '', '', ''}, 213 | -- initialize the element 214 | init = function(self) 215 | self:setPos() 216 | self:setStyle() 217 | self:render() 218 | end, 219 | -- set positon codes 220 | setPos = function(self) 221 | if not self.geo then return end 222 | self.pack[1] = string.format('{\\pos(%f,%f)\\an%d}', self.geo.x, self.geo.y, self.geo.an) 223 | end, 224 | -- set alpha codes, usually called by oscf 225 | -- trans: transparency modifier, 0~1, 1 for invisible 226 | setAlpha = function(self, trans) 227 | local sta = self.style.alpha or elements['default'].style.alpha 228 | local alpha = {0, 0, 0, 0} 229 | self.trans = trans 230 | for i = 1, 4 do 231 | alpha[i] = 255 - (255-sta[i])*(1-trans) 232 | end 233 | self.pack[2] = string.format('{\\1a&H%x&\\2a&H%x&\\3a&H%x&\\4a&H%x&}', 234 | alpha[1], alpha[2], alpha[3], alpha[4]) 235 | end, 236 | -- set style codes, including alpha codes 237 | setStyle = function(self) 238 | local st, std = self.style, elements['default'].style 239 | local fmt = {'{', '', '', '', '', '', '', '', '}'} 240 | if st.color then 241 | fmt[2] = string.format('\\1c&H%s&\\2c&H%s&\\3c&H%s&\\4c&H%s&', 242 | st.color[1], st.color[2], st.color[3], st.color[4]) 243 | else 244 | fmt[2] = string.format('\\1c&H%s&\\2c&H%s&\\3c&H%s&\\4c&H%s&', 245 | std.color[1], std.color[2], std.color[3], std.color[4]) 246 | end 247 | fmt[3] = string.format('\\bord%.2f', st.border or std.border) 248 | fmt[4] = string.format('\\blur%.2f', st.blur or std.blur) 249 | fmt[5] = string.format('\\shad%.2f', st.shadow or std.shadow) 250 | fmt[6] = '\\fn' .. (st.font or std.font) 251 | fmt[7] = string.format('\\fs%.2f', st.fontsize or std.fontsize) 252 | fmt[8] = string.format('\\q%d', st.wrap or std.wrap) 253 | self.pack[3] = table.concat(fmt) 254 | self:setAlpha(self.trans) 255 | end, 256 | -- update other contents 257 | render = function(self) end, 258 | -- called by function tick(), return the pack as a string for further render 259 | -- it MUST return a string, or the osc will halt 260 | tick = function(self) 261 | if self.visible then return table.concat(self.pack) 262 | else return '' 263 | end 264 | end, 265 | --[[responders are functions called by dispatchEvents(), the format is like 266 | responder['event'] = function(self, arg) 267 | ... 268 | return true/false 269 | end 270 | return true will prevent this event from other responders, useful in overlaped elements that only one is allowed to respond.]]-- 271 | responder = {}, 272 | } 273 | 274 | -- create a new element, either from 'default', or from an existing source 275 | -- name: name string of the new element 276 | -- source: OPTIONAL name string of an element as a template 277 | -- return: table of the new element 278 | function newElement(name, source) 279 | local ne, lookup = {}, {} 280 | if source == nil then source = 'default' end 281 | local function clone(e) -- deep clone 282 | if type(e) ~= 'table' then return e end 283 | if lookup[e] then return lookup[e] end --keep reference relations 284 | local copy = {} 285 | lookup[e] = copy 286 | for k, v in pairs(e) do 287 | copy[k] = clone(v) 288 | end 289 | return setmetatable(copy, getmetatable(e)) 290 | end 291 | ne = clone(elements[source]) 292 | elements[name] = ne 293 | return ne 294 | end 295 | 296 | -- get the table of an element 297 | -- name: name string of the element 298 | function getElement(name) 299 | return elements[name] 300 | end 301 | 302 | -- sort element order 303 | local function lowerFirst (a, b) 304 | return a.layer < b.layer 305 | end 306 | 307 | local function higherFirst (a, b) 308 | return a.layer > b.layer 309 | end 310 | 311 | -- add an element to idle layout 312 | -- name: name string of the element 313 | -- return: element table 314 | function addToIdleLayout(name) 315 | return addToLayout('idle', name) 316 | end 317 | 318 | -- add an element to play layout 319 | -- name: name string of the element 320 | -- return: element table 321 | function addToPlayLayout(name) 322 | return addToLayout('play', name) 323 | end 324 | 325 | -- add an element ot a layout 326 | -- layout: 'idle', 'play', or something you like 327 | -- name: name string 328 | -- return: element table 329 | function addToLayout(layout, name) 330 | local e = elements[name] 331 | if e then 332 | table.insert(layouts[layout], e) 333 | table.sort(layouts[layout], lowerFirst) 334 | -- elementsInUse require unique values 335 | local check = true 336 | local n = #elementsInUse 337 | for i = 1, n do 338 | if e == elementsInUse[i] then 339 | check = false 340 | break 341 | end 342 | end 343 | if check then 344 | elementsInUse[n+1] = e 345 | end 346 | table.sort(elementsInUse, higherFirst) 347 | end 348 | return e 349 | end 350 | 351 | -- # event management 352 | -- dispatch event to elements in current layout 353 | -- event: string of event name 354 | -- arg: OPTIONAL arguments 355 | function dispatchEvent(event, arg) 356 | for _, v in ipairs(elementsInUse) do 357 | if v.responder[event] and 358 | v.responder[event](v, arg) then 359 | -- a responder return true can terminate this event 360 | break end 361 | end 362 | end 363 | 364 | -- property observers to generate events 365 | -- these are minimum events for osc framework 366 | mp.observe_property('osd-dimensions', 'native', 367 | function(name, val) 368 | oscResize() 369 | dispatchEvent('resize') 370 | end) 371 | mp.observe_property('idle-active', 'bool', 372 | function(name, val) 373 | player.idle = val 374 | if not player.idle then oscLayout = 'play' 375 | else oscLayout = 'idle' 376 | end 377 | dispatchEvent('idle') 378 | end) 379 | -- set an active area for idle layout 380 | -- name: string of area name 381 | function setIdleActiveArea(name, x1, y1, x2, y2, prop) 382 | setActiveArea('idle', name, x1, y1, x2, y2, prop) 383 | end 384 | -- set an active area for play layout 385 | -- name: string of area name 386 | function setPlayActiveArea(name, x1, y1, x2, y2, prop) 387 | setActiveArea('play', name, x1, y1, x2, y2, prop) 388 | end 389 | -- a general function to set active area 390 | -- layout: 'idle', 'play', or else 391 | function setActiveArea(layout, name, x1, y1, x2, y2, prop) 392 | local area = {x1 = x1, y1 = y1, x2 = x2, y2 = y2, prop = prop} 393 | activeAreas[layout][name] = area 394 | end 395 | 396 | -- get mouse position scaled by display factor 397 | function getMousePos() 398 | local x, y = mp.get_mouse_pos() 399 | return x*mouseScale, y*mouseScale 400 | end 401 | 402 | -- mouse move event handler 403 | local function eventMove() 404 | local x, y = getMousePos() 405 | local check = false 406 | local areas = activeAreas[oscLayout] 407 | for _, v in pairs(areas) do 408 | if v.x1 <= x and x <= v.x2 and v.y1 <= y and y <= v.y2 then 409 | if v.prop == 'show_hide' then 410 | showOsc() 411 | goto next 412 | else 413 | -- default 414 | check = true 415 | if not active then 416 | mp.enable_key_bindings('_button_') 417 | active = true 418 | end 419 | end 420 | dispatchEvent('mouse_move', {x, y}) 421 | end 422 | ::next:: 423 | end 424 | if not check and active then 425 | mp.disable_key_bindings('_button_') 426 | active = false 427 | dispatchEvent('mouse_leave') 428 | end 429 | end 430 | -- mouse leave player window 431 | local function eventLeave() 432 | if active then 433 | mp.disable_key_bindings('_button_') 434 | active = false 435 | end 436 | dispatchEvent('mouse_leave') 437 | end 438 | -- mouse button event handler 439 | local function eventButton(event) 440 | local x, y = getMousePos() 441 | dispatchEvent(event, {x, y}) 442 | end 443 | 444 | -- mouse move bindings 445 | mp.set_key_bindings({ 446 | {'mouse_move', eventMove}, 447 | {'mouse_leave', eventLeave}, 448 | }, '_move_', 'force') 449 | mp.enable_key_bindings('_move_', 'allow-vo-dragging+allow-hide-cursor') 450 | 451 | --mouse input bindings 452 | mp.set_key_bindings({ 453 | {'mbtn_left', function() eventButton('mbtn_left_up') end, function() eventButton('mbtn_left_down') end}, 454 | {'mbtn_right', function() eventButton('mbtn_right_up') end, function() eventButton('mbtn_right_down') end}, 455 | {'mbtn_mid', function() eventButton('mbtn_mid_up') end, function() eventButton('mbtn_mid_down') end}, 456 | {'wheel_up', function() eventButton('wheel_up') end}, 457 | {'wheel_down', function() eventButton('wheel_down') end}, 458 | {'mbtn_left_dbl', function() eventButton('mbtn_left_dbl') end}, 459 | {'mbtn_right_dbl', function() eventButton('mbtn_right_dbl') end}, 460 | {'mbtn_mid_dbl', function() eventButton('mbtn_mid_dbl') end}, 461 | }, '_button_', 'force') 462 | 463 | -- mouse button events control for user scripts 464 | -- enable mouse button events 465 | function enableMouseButtonEvents() 466 | mp.enable_key_binding('_button_') 467 | end 468 | -- disable mouse button events 469 | function disableMouseButtonEvents() 470 | mp.mp.disable_key_bindings('_button_') 471 | end --------------------------------------------------------------------------------