├── preview.png ├── modernx-osc-icon.ttf ├── README.md └── modernx.lua /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1-minute-to-midnight/mpv-modern-x-compact/HEAD/preview.png -------------------------------------------------------------------------------- /modernx-osc-icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1-minute-to-midnight/mpv-modern-x-compact/HEAD/modernx-osc-icon.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpv-modern-x-compact 2 | Compact version of modern-x osc for mpv with a neat web-player type UI 3 | 4 | ![preview](https://raw.githubusercontent.com/1-minute-to-midnight/mpv-morden-x-compact/main/preview.png) 5 | 6 | # How to install 7 | Put the .lua file into "~~/scripts/" folder, and remove other osc scripts. 8 | 9 | Set the following setting in mpv.conf : 10 | 11 | `osc=no` 12 | 13 | :warning: **Important**: Install [dexeonify's](https://github.com/dexeonify/mpv-config/) custom-made [ModernX OSC font](https://github.com/1-minute-to-midnight/mpv-morden-x-compact/raw/main/modernx-osc-icon.ttf) for the OSC icons or create a folder named `fonts` in the mpv config folder and drop the ttf file in there. 14 | 15 | # Configuration 16 | You can change various options like accent color by editing osc.conf which should be put in the "~~/script-opts/" folder. The usage/creation of this file is optional if you want to use the config as is. 17 | ``` 18 | # Accent of the OSC and the title bar 19 | osc_color=000000 20 | 21 | # Color of the seekbar progress and handle 22 | seekbarfg_color=E39C42 23 | 24 | # Color of the remaining seekbar 25 | seekbarbg_color=FFFFFF 26 | ``` 27 | If you want to change the height of the seekbar or want to change the positions of various elements: 28 | By line 1675 you will see various titles indicating each element (Seekbar, Title, Playback control buttons etc.). To move the elements horizontally (along x-axis), add or subtract values to refX or x values. Similar process can be done for y values to move it vertically (along y-axis). 29 | 30 | ## Thumbnail Support 31 | 32 | Go to [thumbfast](https://github.com/po5/thumbfast) and drop thumbfast.lua into scripts folder 33 | 34 | # Fonts 35 | - [Manrope](https://github.com/sharanda/manrope) 36 | 37 | (Change osd-font in "~~mpv.conf" to get like it is in the screenshot) 38 | 39 | # Credits 40 | - [Dexeonify's Personal Config](https://github.com/dexeonify/mpv-config) 41 | 42 | - [Maoiscat's Modern Layout](https://github.com/maoiscat/mpv-osc-morden) 43 | 44 | - [po5's Thumbfast Thumbnailer](https://github.com/po5/thumbfast) 45 | -------------------------------------------------------------------------------- /modernx.lua: -------------------------------------------------------------------------------- 1 | -- mpv-osc-morden by maoiscat 2 | -- email:valarmor@163.com 3 | -- https://github.com/maoiscat/mpv-osc-morden 4 | 5 | -- fork by cyl0 6 | -- https://github.com/cyl0/MordenX 7 | 8 | -- forked again by dexeonify 9 | -- https://github.com/dexeonify/mpv-config/blob/main/scripts/modernx.lua 10 | 11 | -- forked once again by 1-minute-to-midnight 12 | -- https://github.com/1-minute-to-midnight/mpv-modern-x-compact/ 13 | 14 | local ipairs,loadfile,pairs,pcall,tonumber,tostring = ipairs,loadfile,pairs,pcall,tonumber,tostring 15 | local debug,io,math,os,string,table,utf8 = debug,io,math,os,string,table,utf8 16 | local min,max,floor,ceil,huge = math.min,math.max,math.floor,math.ceil,math.huge 17 | local mp = require 'mp' 18 | local assdraw = require 'mp.assdraw' 19 | local msg = require 'mp.msg' 20 | local opt = require 'mp.options' 21 | local utils = require 'mp.utils' 22 | 23 | -- 24 | -- Parameters 25 | -- 26 | -- default user option values 27 | -- do not touch, change them in osc.conf 28 | local user_opts = { 29 | showwindowed = true, -- show OSC when windowed? 30 | showfullscreen = true, -- show OSC when fullscreen? 31 | idlescreen = true, -- show mpv logo on idle 32 | scalewindowed = 1, -- scaling of the controller when windowed 33 | scalefullscreen = 1, -- scaling of the controller when fullscreen 34 | scaleforcedwindow = 1.5, -- scaling when rendered on a forced window 35 | vidscale = true, -- scale the controller with the video? 36 | barmargin = 0, -- vertical margin of top/bottombar 37 | boxalpha = 80, -- alpha of the background box, 38 | -- 0 (opaque) to 255 (fully transparent) 39 | hidetimeout = 500, -- duration in ms until the OSC hides if no 40 | -- mouse movement. enforced non-negative for the 41 | -- user, but internally negative is "always-on". 42 | fadeduration = 200, -- duration of fade out in ms, 0 = no fade 43 | deadzonesize = 0.5, -- size of deadzone 44 | minmousemove = 0, -- minimum amount of pixels the mouse has to 45 | -- move between ticks to make the OSC show up 46 | iamaprogrammer = false, -- use native mpv values and disable OSC 47 | -- internal track list management (and some 48 | -- functions that depend on it) 49 | layout = "modernx", -- set thumbnail layout 50 | seekbarhandlesize = 0.6, -- size ratio of the knob handle 51 | seekrangealpha = 64, -- transparency of seekranges 52 | seekbarkeyframes = true, -- use keyframes when dragging the seekbar 53 | title = "${media-title}", -- string compatible with property-expansion 54 | -- to be shown as OSC title 55 | tooltipborder = 1, -- border of tooltip in bottom/topbar 56 | timetotal = false, -- display total time instead of remaining time? 57 | timems = false, -- display timecodes with milliseconds? 58 | visibility = "auto", -- only used at init to set visibility_mode(...) 59 | windowcontrols = "auto", -- whether to show window controls 60 | windowcontrols_alignment = "right", -- which side to show window controls on 61 | windowcontrols_title = true, -- whether to show the title with the window controls 62 | greenandgrumpy = false, -- disable santa hat 63 | livemarkers = true, -- update seekbar chapter markers on duration change 64 | chapters_osd = true, -- whether to show chapters OSD on next/prev 65 | playlist_osd = true, -- whether to show playlist OSD on next/prev 66 | chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable 67 | showtitle = true, -- show title in OSC 68 | showonpause = true, -- show OSC on pause 69 | showonstart = true, -- show OSC on startup or when the next file in 70 | -- playlist starts playing 71 | showonseek = false, -- show OSC when seeking 72 | movesub = true, -- move subtitles when the OSC is visible 73 | titlefont = "", -- font used for the title above OSC and 74 | -- in the window controls bar 75 | blur_intensity = 150, -- adjust the strength of the OSC blur 76 | osc_color = "000000", -- accent of the OSC and the title bar 77 | seekbarfg_color = "E39C42", -- color of the seekbar progress and handle 78 | seekbarbg_color = "FFFFFF", -- color of the remaining seekbar 79 | tick_delay = 1 / 60, -- minimum interval between OSC redraws in seconds 80 | tick_delay_follow_display_fps = false -- use display fps as the minimum interval 81 | } 82 | 83 | -- read options from config and command-line 84 | opt.read_options(user_opts, "osc", function(list) update_options(list) end) 85 | 86 | user_opts.userdataAvail = (function() 87 | local list = mp.get_property_native("property-list") 88 | for k,v in ipairs(list) do 89 | if (v == "user-data") then 90 | return true 91 | end 92 | end 93 | return false 94 | end)() 95 | 96 | -- deus0ww - 2021-11-26 97 | 98 | ------------ 99 | -- tn_osc -- 100 | ------------ 101 | local message = { 102 | osc = { 103 | registration = "tn_osc_registration", 104 | reset = "tn_osc_reset", 105 | update = "tn_osc_update", 106 | finish = "tn_osc_finish", 107 | }, 108 | debug = "Thumbnailer-debug", 109 | 110 | queued = 1, 111 | processing = 2, 112 | ready = 3, 113 | failed = 4, 114 | } 115 | 116 | 117 | ----------- 118 | -- Utils -- 119 | ----------- 120 | local OS_MAC, OS_WIN, OS_NIX = "MAC", "WIN", "NIX" 121 | local function get_os() 122 | if jit and jit.os then 123 | if jit.os == "Windows" then return OS_WIN 124 | elseif jit.os == "OSX" then return OS_MAC 125 | else return OS_NIX end 126 | end 127 | if (package.config:sub(1,1) ~= "/") then return OS_WIN end 128 | local res = mp.command_native({ name = "subprocess", args = {"uname", "-s"}, playback_only = false, capture_stdout = true, capture_stderr = true, }) 129 | return (res and res.stdout and res.stdout:lower():find("darwin") ~= nil) and OS_MAC or OS_NIX 130 | end 131 | local OPERATING_SYSTEM = get_os() 132 | 133 | local function format_json(tab) 134 | local json, err = utils.format_json(tab) 135 | if err then msg.error("Formatting JSON failed:", err) end 136 | if json then return json else return "" end 137 | end 138 | 139 | local function parse_json(json) 140 | local tab, err = utils.parse_json(json, true) 141 | if err then msg.error("Parsing JSON failed:", err) end 142 | if tab then return tab else return {} end 143 | end 144 | 145 | local function join_paths(...) 146 | local sep = OPERATING_SYSTEM == OS_WIN and "\\" or "/" 147 | local result = "" 148 | for _, p in ipairs({...}) do 149 | result = (result == "") and p or result .. sep .. p 150 | end 151 | return result 152 | end 153 | 154 | 155 | -------------------- 156 | -- Data Structure -- 157 | -------------------- 158 | local tn_state, tn_osc, tn_osc_options, tn_osc_stats 159 | local tn_thumbnails_indexed, tn_thumbnails_ready 160 | local tn_gen_time_start, tn_gen_duration 161 | 162 | local function reset_all() 163 | tn_state = nil 164 | tn_osc = { 165 | cursor = {}, 166 | position = {}, 167 | scale = {}, 168 | osc_scale = {}, 169 | spacer = {}, 170 | osd = {}, 171 | background = {text = "︎✇",}, 172 | font_scale = {}, 173 | display_progress = {}, 174 | progress = {}, 175 | mini = {text = "⚆",}, 176 | thumbnail = { 177 | visible = false, 178 | path_last = nil, 179 | x_last = nil, 180 | y_last = nil, 181 | }, 182 | } 183 | tn_osc_options = nil 184 | tn_osc_stats = { 185 | queued = 0, 186 | processing = 0, 187 | ready = 0, 188 | failed = 0, 189 | total = 0, 190 | total_expected = 0, 191 | percent = 0, 192 | timer = 0, 193 | } 194 | tn_thumbnails_indexed = {} 195 | tn_thumbnails_ready = {} 196 | tn_gen_time_start = nil 197 | tn_gen_duration = nil 198 | end 199 | 200 | ------------ 201 | -- TN OSC -- 202 | ------------ 203 | local osc_reg = { 204 | script_name = mp.get_script_name(), 205 | osc_opts = { 206 | scalewindowed = user_opts.scalewindowed, 207 | scalefullscreen = user_opts.scalefullscreen, 208 | }, 209 | } 210 | mp.command_native({"script-message", message.osc.registration, format_json(osc_reg)}) 211 | 212 | local tn_palette = { 213 | black = "000000", 214 | white = "FFFFFF", 215 | alpha_opaque = 0, 216 | alpha_clear = 255, 217 | alpha_black = min(255, user_opts.boxalpha), 218 | alpha_white = min(255, user_opts.boxalpha + (255 - user_opts.boxalpha) * 0.8), 219 | } 220 | 221 | local tn_style_format = { 222 | background = "{\\bord0\\1c&H%s&\\1a&H%X&}", 223 | subbackground = "{\\bord0\\1c&H%s&\\1a&H%X&}", 224 | spinner = "{\\bord0\\fs%d\\fscx%f\\fscy%f", 225 | spinner2 = "\\1c&%s&\\1a&H%X&\\frz%d}", 226 | closest_index = "{\\1c&H%s&\\1a&H%X&\\3c&H%s&\\3a&H%X&\\xbord%d\\ybord%d}", 227 | progress_mini = "{\\bord0\\1c&%s&\\1a&H%X&\\fs18\\fscx%f\\fscy%f", 228 | progress_mini2 = "\\frz%d}", 229 | progress_block = "{\\bord0\\1c&H%s&\\1a&H%X&}", 230 | progress_text = "{\\1c&%s&\\3c&H%s&\\1a&H%X&\\3a&H%X&\\blur0.25\\fs18\\fscx%f\\fscy%f\\xbord%f\\ybord%f}", 231 | text_timer = "%.2ds", 232 | text_progress = "%.3d/%.3d", 233 | text_progress2 = "[%d]", 234 | text_percent = "%d%%", 235 | } 236 | 237 | local tn_style = { 238 | background = (tn_style_format.background):format(tn_palette.black, tn_palette.alpha_black), 239 | subbackground = (tn_style_format.subbackground):format(tn_palette.white, tn_palette.alpha_white), 240 | spinner = (tn_style_format.spinner):format(0, 1, 1), 241 | closest_index = (tn_style_format.closest_index):format(tn_palette.white, tn_palette.alpha_black, tn_palette.black, tn_palette.alpha_black, -1, -1), 242 | progress_mini = (tn_style_format.progress_mini):format(tn_palette.white, tn_palette.alpha_opaque, 1, 1), 243 | progress_block = (tn_style_format.progress_block):format(tn_palette.white, tn_palette.alpha_white), 244 | progress_text = (tn_style_format.progress_text):format(tn_palette.white, tn_palette.black, tn_palette.alpha_opaque, tn_palette.alpha_black, 1, 1, 2, 2), 245 | } 246 | 247 | local function set_thumbnail_above(offset) 248 | local tn_osc = tn_osc 249 | tn_osc.background.bottom = tn_osc.position.y - offset - tn_osc.spacer.bottom 250 | tn_osc.background.top = tn_osc.background.bottom - tn_osc.background.h 251 | tn_osc.thumbnail.top = tn_osc.background.bottom - tn_osc.thumbnail.h 252 | tn_osc.progress.top = tn_osc.background.bottom - tn_osc.background.h 253 | tn_osc.progress.mid = tn_osc.progress.top + tn_osc.progress.h * 0.5 254 | tn_osc.background.rotation = -1 255 | end 256 | 257 | local function set_thumbnail_below(offset) 258 | local tn_osc = tn_osc 259 | tn_osc.background.top = tn_osc.position.y + offset + tn_osc.spacer.top 260 | tn_osc.thumbnail.top = tn_osc.background.top 261 | tn_osc.progress.top = tn_osc.background.top + tn_osc.thumbnail.h + tn_osc.spacer.y 262 | tn_osc.progress.mid = tn_osc.progress.top + tn_osc.progress.h * 0.5 263 | tn_osc.background.rotation = 1 264 | end 265 | 266 | local function set_mini_above() tn_osc.mini.y = (tn_osc.background.top - 12 * tn_osc.osc_scale.y) end 267 | local function set_mini_below() tn_osc.mini.y = (tn_osc.background.bottom + 12 * tn_osc.osc_scale.y) end 268 | 269 | local set_thumbnail_layout = { 270 | topbar = function() tn_osc.spacer.top = 0.25 271 | set_thumbnail_below(38.75) 272 | set_mini_above() end, 273 | bottombar = function() tn_osc.spacer.bottom = 0.25 274 | set_thumbnail_above(38.75) 275 | set_mini_below() end, 276 | box = function() set_thumbnail_above(15) 277 | set_mini_above() end, 278 | slimbox = function() set_thumbnail_above(12) 279 | set_mini_above() end, 280 | modernx = function() set_thumbnail_above(20) 281 | set_mini_below() end, 282 | } 283 | 284 | local function update_tn_osc_params(seek_y) 285 | local tn_state, tn_osc_stats, tn_osc, tn_style, tn_style_format = tn_state, tn_osc_stats, tn_osc, tn_style, tn_style_format 286 | tn_osc.scale.x, tn_osc.scale.y = get_virt_scale_factor() 287 | tn_osc.osd.w, tn_osc.osd.h = mp.get_osd_size() 288 | tn_osc.cursor.x, tn_osc.cursor.y = get_virt_mouse_pos() 289 | tn_osc.position.y = seek_y 290 | 291 | local osc_changed = false 292 | if tn_osc.scale.x_last ~= tn_osc.scale.x or tn_osc.scale.y_last ~= tn_osc.scale.y 293 | or tn_osc.w_last ~= tn_state.width or tn_osc.h_last ~= tn_state.height 294 | or tn_osc.osd.w_last ~= tn_osc.osd.w or tn_osc.osd.h_last ~= tn_osc.osd.h 295 | then 296 | tn_osc.scale.x_last, tn_osc.scale.y_last = tn_osc.scale.x, tn_osc.scale.y 297 | tn_osc.w_last, tn_osc.h_last = tn_state.width, tn_state.height 298 | tn_osc.osd.w_last, tn_osc.osd.h_last = tn_osc.osd.w, tn_osc.osd.h 299 | osc_changed = true 300 | end 301 | 302 | if osc_changed then 303 | tn_osc.osc_scale.x, tn_osc.osc_scale.y = 1, 1 304 | tn_osc.spacer.x, tn_osc.spacer.y = tn_osc_options.spacer, tn_osc_options.spacer 305 | tn_osc.font_scale.x, tn_osc.font_scale.y = 100, 100 306 | tn_osc.progress.h = (16 + tn_osc_options.spacer) 307 | if not user_opts.vidscale then 308 | tn_osc.osc_scale.x = tn_osc.scale.x * tn_osc_options.scale 309 | tn_osc.osc_scale.y = tn_osc.scale.y * tn_osc_options.scale 310 | tn_osc.spacer.x = tn_osc.osc_scale.x * tn_osc.spacer.x 311 | tn_osc.spacer.y = tn_osc.osc_scale.y * tn_osc.spacer.y 312 | tn_osc.font_scale.x = tn_osc.osc_scale.x * tn_osc.font_scale.x 313 | tn_osc.font_scale.y = tn_osc.osc_scale.y * tn_osc.font_scale.y 314 | tn_osc.progress.h = tn_osc.osc_scale.y * tn_osc.progress.h 315 | end 316 | tn_osc.spacer.top, tn_osc.spacer.bottom = tn_osc.spacer.y, tn_osc.spacer.y 317 | tn_osc.thumbnail.w, tn_osc.thumbnail.h = tn_state.width * tn_osc.scale.x, tn_state.height * tn_osc.scale.y 318 | tn_osc.osd.w_scaled, tn_osc.osd.h_scaled = tn_osc.osd.w * tn_osc.scale.x, tn_osc.osd.h * tn_osc.scale.y 319 | tn_style.spinner = (tn_style_format.spinner):format(min(tn_osc.thumbnail.w, tn_osc.thumbnail.h) * 0.6667, tn_osc.font_scale.x, tn_osc.font_scale.y) 320 | tn_style.closest_index = (tn_style_format.closest_index):format(tn_palette.white, tn_palette.alpha_black, tn_palette.black, tn_palette.alpha_black, -1 * tn_osc.scale.x, -1 * tn_osc.scale.y) 321 | if tn_osc_stats.percent < 1 then 322 | tn_style.progress_text = (tn_style_format.progress_text):format(tn_palette.white, tn_palette.black, tn_palette.alpha_opaque, tn_palette.alpha_black, tn_osc.font_scale.x, tn_osc.font_scale.y, 2 * tn_osc.scale.x, 2 * tn_osc.scale.y) 323 | tn_style.progress_mini = (tn_style_format.progress_mini):format(tn_palette.white, tn_palette.alpha_opaque, tn_osc.font_scale.x, tn_osc.font_scale.y) 324 | end 325 | end 326 | 327 | if not tn_osc.position.y then return end 328 | if (osc_changed or tn_osc.cursor.x_last ~= tn_osc.cursor.x) and tn_osc.osd.w_scaled >= (tn_osc.thumbnail.w + 2 * tn_osc.spacer.x) then 329 | tn_osc.cursor.x_last = tn_osc.cursor.x 330 | if tn_osc_options.centered then 331 | tn_osc.position.x = tn_osc.osd.w_scaled * 0.5 332 | else 333 | local limit_left = tn_osc.spacer.x + tn_osc.thumbnail.w * 0.5 334 | local limit_right = tn_osc.osd.w_scaled - limit_left 335 | tn_osc.position.x = min(max(tn_osc.cursor.x, limit_left), limit_right) 336 | end 337 | tn_osc.thumbnail.left, tn_osc.thumbnail.right = tn_osc.position.x - tn_osc.thumbnail.w * 0.5, tn_osc.position.x + tn_osc.thumbnail.w * 0.5 338 | tn_osc.mini.x = tn_osc.thumbnail.right - 6 * tn_osc.osc_scale.x 339 | end 340 | 341 | if (osc_changed or tn_osc.display_progress.last ~= tn_osc.display_progress.current) then 342 | tn_osc.display_progress.last = tn_osc.display_progress.current 343 | tn_osc.background.h = tn_osc.thumbnail.h + (tn_osc.display_progress.current and (tn_osc.progress.h + tn_osc.spacer.y) or 0) 344 | set_thumbnail_layout[user_opts.layout]() 345 | end 346 | end 347 | 348 | local function find_closest(seek_index, round_up) 349 | local tn_state, tn_thumbnails_indexed, tn_thumbnails_ready = tn_state, tn_thumbnails_indexed, tn_thumbnails_ready 350 | if not (tn_thumbnails_indexed and tn_thumbnails_ready) then return nil, nil end 351 | local time_index = floor(seek_index * tn_state.delta) 352 | if tn_thumbnails_ready[time_index] then return seek_index + 1, tn_thumbnails_indexed[time_index] end 353 | local direction, index = round_up and 1 or -1 354 | for i = 1, tn_osc_stats.total_expected do 355 | index = seek_index + (i * direction) 356 | time_index = floor(index * tn_state.delta) 357 | if tn_thumbnails_ready[time_index] then return index + 1, tn_thumbnails_indexed[time_index] end 358 | index = seek_index + (i * -direction) 359 | time_index = floor(index * tn_state.delta) 360 | if tn_thumbnails_ready[time_index] then return index + 1, tn_thumbnails_indexed[time_index] end 361 | end 362 | return nil, nil 363 | end 364 | 365 | local draw_cmd = { name = "overlay-add", id = 9, offset = 0, fmt = "bgra" } 366 | local hide_cmd = { name = "overlay-remove", id = 9} 367 | 368 | local function draw_thumbnail(x, y, path) 369 | draw_cmd.x = x 370 | draw_cmd.y = y 371 | draw_cmd.file = path 372 | mp.command_native(draw_cmd) 373 | tn_osc.thumbnail.visible = true 374 | end 375 | 376 | local function hide_thumbnail() 377 | if tn_osc and tn_osc.thumbnail and tn_osc.thumbnail.visible then 378 | mp.command_native(hide_cmd) 379 | tn_osc.thumbnail.visible = false 380 | end 381 | end 382 | 383 | local function show_thumbnail(seek_percent) 384 | if not seek_percent then return nil, nil end 385 | local scale, thumbnail, total_expected, ready = tn_osc.scale, tn_osc.thumbnail, tn_osc_stats.total_expected, tn_osc_stats.ready 386 | local seek = seek_percent * (total_expected - 1) 387 | local seek_index = floor(seek + 0.5) 388 | local closest_index, path = thumbnail.closest_index_last, thumbnail.path_last 389 | if thumbnail.seek_index_last ~= seek_index 390 | or thumbnail.ready_last ~= ready 391 | or thumbnail.total_expected_last ~= tn_osc_stats.total_expected 392 | then 393 | closest_index, path = find_closest(seek_index, seek_index < seek) 394 | thumbnail.closest_index_last, thumbnail.total_expected_last, thumbnail.ready_last, thumbnail.seek_index_last = closest_index, total_expected, ready, seek_index 395 | end 396 | local x, y = floor((thumbnail.left or 0) / scale.x + 0.5), floor((thumbnail.top or 0) / scale.y + 0.5) 397 | if path and not (thumbnail.visible and thumbnail.x_last == x and thumbnail.y_last == y and thumbnail.path_last == path) then 398 | thumbnail.x_last, thumbnail.y_last, thumbnail.path_last = x, y, path 399 | draw_thumbnail(x, y, path) 400 | end 401 | return closest_index, path 402 | end 403 | 404 | local function ass_new(ass, x, y, align, style, text) 405 | ass:new_event() 406 | ass:pos(x, y) 407 | if align then ass:an(align) end 408 | if style then ass:append(style) end 409 | if text then ass:append(text) end 410 | end 411 | 412 | local function ass_rect(ass, x1, y1, x2, y2) 413 | ass:draw_start() 414 | ass:rect_cw(x1, y1, x2, y2) 415 | ass:draw_stop() 416 | end 417 | 418 | local draw_progress = { 419 | [message.queued] = function(ass, index, block_w, block_h) ass:rect_cw((index - 1) * block_w, 0, index * block_w, block_h) end, 420 | [message.processing] = function(ass, index, block_w, block_h) ass:rect_cw((index - 1) * block_w, block_h * 0.2, index * block_w, block_h * 0.8) end, 421 | [message.failed] = function(ass, index, block_w, block_h) ass:rect_cw((index - 1) * block_w, block_h * 0.4, index * block_w, block_h * 0.6) end, 422 | } 423 | 424 | local function display_tn_osc(seek_y, seek_percent, ass) 425 | if not (seek_y and seek_percent and ass and tn_state and tn_osc_stats and tn_osc_options and tn_state.width and tn_state.height and tn_state.duration and tn_state.cache_dir) or not tn_osc_options.visible then hide_thumbnail() return end 426 | 427 | update_tn_osc_params(seek_y) 428 | local tn_osc_stats, tn_osc, tn_style, tn_style_format, ass_new, ass_rect, seek_percent = tn_osc_stats, tn_osc, tn_style, tn_style_format, ass_new, ass_rect, seek_percent * 0.01 429 | local closest_index, path = show_thumbnail(seek_percent) 430 | 431 | -- Background 432 | ass_new(ass, tn_osc.thumbnail.left, tn_osc.background.top, 7, tn_style.background) 433 | ass_rect(ass, -tn_osc.spacer.x, -tn_osc.spacer.top, tn_osc.thumbnail.w + tn_osc.spacer.x, tn_osc.background.h + tn_osc.spacer.bottom) 434 | 435 | local spinner_color, spinner_alpha = tn_palette.white, tn_palette.alpha_white 436 | if not path then 437 | ass_new(ass, tn_osc.thumbnail.left, tn_osc.thumbnail.top, 7, tn_style.subbackground) 438 | ass_rect(ass, 0, 0, tn_osc.thumbnail.w, tn_osc.thumbnail.h) 439 | spinner_color, spinner_alpha = tn_palette.black, tn_palette.alpha_black 440 | end 441 | ass_new(ass, tn_osc.position.x, tn_osc.thumbnail.top + tn_osc.thumbnail.h * 0.5, 5, tn_style.spinner .. (tn_style_format.spinner2):format(spinner_color, spinner_alpha, tn_osc.background.rotation * seek_percent * 1080), tn_osc.background.text) 442 | 443 | -- Mini Progress Spinner 444 | if tn_osc.display_progress.current ~= nil and not tn_osc.display_progress.current and tn_osc_stats.percent < 1 then 445 | ass_new(ass, tn_osc.mini.x, tn_osc.mini.y, 5, tn_style.progress_mini .. (tn_style_format.progress_mini2):format(tn_osc_stats.percent * -360 + 90), tn_osc.mini.text) 446 | end 447 | 448 | -- Progress Bar 449 | if tn_osc.display_progress.current then 450 | local block_w, index = tn_osc_stats.total_expected > 0 and tn_state.width * tn_osc.scale.y / tn_osc_stats.total_expected or 0, 0 451 | if tn_thumbnails_indexed and block_w > 0 then 452 | -- Loading bar 453 | ass_new(ass, tn_osc.thumbnail.left, tn_osc.progress.top, 7, tn_style.progress_block) 454 | ass:draw_start() 455 | for time_index, status in pairs(tn_thumbnails_indexed) do 456 | index = floor(time_index / tn_state.delta) + 1 457 | if index ~= closest_index and not tn_thumbnails_ready[time_index] and index <= tn_osc_stats.total_expected and draw_progress[status] ~= nil then 458 | draw_progress[status](ass, index, block_w, tn_osc.progress.h) 459 | end 460 | end 461 | ass:draw_stop() 462 | 463 | if closest_index and closest_index <= tn_osc_stats.total_expected then 464 | ass_new(ass, tn_osc.thumbnail.left, tn_osc.progress.top, 7, tn_style.closest_index) 465 | ass_rect(ass, (closest_index - 1) * block_w, 0, closest_index * block_w, tn_osc.progress.h) 466 | end 467 | end 468 | 469 | -- Text: Timer 470 | ass_new(ass, tn_osc.thumbnail.left + 3 * tn_osc.osc_scale.y, tn_osc.progress.mid, 4, tn_style.progress_text, (tn_style_format.text_timer):format(tn_osc_stats.timer)) 471 | 472 | -- Text: Number or Index of Thumbnail 473 | local temp = tn_osc_stats.percent < 1 and tn_osc_stats.ready or closest_index 474 | local processing = tn_osc_stats.processing > 0 and (tn_style_format.text_progress2):format(tn_osc_stats.processing) or "" 475 | ass_new(ass, tn_osc.position.x, tn_osc.progress.mid, 5, tn_style.progress_text, (tn_style_format.text_progress):format(temp and temp or 0, tn_osc_stats.total_expected) .. processing) 476 | 477 | -- Text: Percentage 478 | ass_new(ass, tn_osc.thumbnail.right - 3 * tn_osc.osc_scale.y, tn_osc.progress.mid, 6, tn_style.progress_text, (tn_style_format.text_percent):format(min(100, tn_osc_stats.percent * 100))) 479 | end 480 | end 481 | 482 | 483 | --------------- 484 | -- Listeners -- 485 | --------------- 486 | mp.register_script_message(message.osc.reset, function() 487 | hide_thumbnail() 488 | reset_all() 489 | end) 490 | 491 | local text_progress_format = { two_digits = "%.2d/%.2d", three_digits = "%.3d/%.3d" } 492 | 493 | mp.register_script_message(message.osc.update, function(json) 494 | local new_data = parse_json(json) 495 | if not new_data then return end 496 | if new_data.state then 497 | tn_state = new_data.state 498 | if tn_state.is_rotated then tn_state.width, tn_state.height = tn_state.height, tn_state.width end 499 | draw_cmd.w = tn_state.width 500 | draw_cmd.h = tn_state.height 501 | draw_cmd.stride = tn_state.width * 4 502 | end 503 | if new_data.osc_options then tn_osc_options = new_data.osc_options end 504 | if new_data.osc_stats then 505 | tn_osc_stats = new_data.osc_stats 506 | if tn_osc_options and tn_osc_options.show_progress then 507 | if tn_osc_options.show_progress == 0 then tn_osc.display_progress.current = false 508 | elseif tn_osc_options.show_progress == 1 then tn_osc.display_progress.current = tn_osc_stats.percent < 1 509 | else tn_osc.display_progress.current = true end 510 | end 511 | tn_style_format.text_progress = tn_osc_stats.total > 99 and text_progress_format.three_digits or text_progress_format.two_digits 512 | if tn_osc_stats.percent >= 1 then mp.command_native({"script-message", message.osc.finish}) end 513 | end 514 | if new_data.thumbnails and tn_state then 515 | local index, ready 516 | for time_string, status in pairs(new_data.thumbnails) do 517 | index, ready = tonumber(time_string), (status == message.ready) 518 | tn_thumbnails_indexed[index] = ready and join_paths(tn_state.cache_dir, time_string) .. tn_state.cache_extension or status 519 | tn_thumbnails_ready[index] = ready 520 | end 521 | end 522 | request_tick() 523 | end) 524 | 525 | mp.register_script_message(message.debug, function() 526 | msg.info("Thumbnailer OSC Internal States:") 527 | msg.info("tn_state:", tn_state and utils.to_string(tn_state) or "nil") 528 | msg.info("tn_thumbnails_indexed:", tn_thumbnails_indexed and utils.to_string(tn_thumbnails_indexed) or "nil") 529 | msg.info("tn_thumbnails_ready:", tn_thumbnails_ready and utils.to_string(tn_thumbnails_ready) or "nil") 530 | msg.info("tn_osc_options:", tn_osc_options and utils.to_string(tn_osc_options) or "nil") 531 | msg.info("tn_osc_stats:", tn_osc_stats and utils.to_string(tn_osc_stats) or "nil") 532 | msg.info("tn_osc:", tn_osc and utils.to_string(tn_osc) or "nil") 533 | end) 534 | 535 | 536 | 537 | 538 | ----------------- 539 | -- modernx.lua -- 540 | ----------------- 541 | 542 | local osc_param = { -- calculated by osc_init() 543 | playresy = 0, -- canvas size Y 544 | playresx = 0, -- canvas size X 545 | display_aspect = 1, 546 | unscaled_y = 0, 547 | areas = {}, 548 | video_margins = { 549 | l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom 550 | }, 551 | } 552 | 553 | local osc_styles = { 554 | transBg = "{\\blur100\\bord" .. user_opts.blur_intensity .. "\\1c&H000000&\\3c&H" .. user_opts.osc_color .. "&}", 555 | seekbarBg = "{\\blur0\\bord0\\1c&H" .. user_opts.seekbarbg_color .. "&}", 556 | seekbarFg = "{\\blur1\\bord1\\1c&H" .. user_opts.seekbarfg_color .. "&}", 557 | 558 | bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs18\\fnmodernx-osc-icon}", 559 | mediumButtons = "{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs18\\fnmodernx-osc-icon}", 560 | smallButtons = "{\\blur0\\bord0\\1c&HFFFFFF&\\3c&HFFFFFF&\\fs15\\fnmodernx-osc-icon}", 561 | 562 | timecodes = "{\\blur0\\bord0\\1c&HFFFFFF&\\3c&H000000&\\fs13}", 563 | tooltip = "{\\blur1\\bord" .. user_opts.tooltipborder .. "\\1c&HFFFFFF&\\3c&H000000&\\fs13}", 564 | vidTitle = "{\\blur1\\bord0.5\\1c&HFFFFFF&\\3c&H0\\fs26\\q2\\fn" .. user_opts.titlefont .. "}", 565 | 566 | wcButtons = "{\\1c&HFFFFFF&\\fs20\\fnmodernx-osc-icon}", 567 | wcTitle = "{\\1c&HFFFFFF&\\fs24\\q2\\fn" .. user_opts.titlefont .. "}", 568 | wcBar = "{\\1c&H" .. user_opts.osc_color .. "}", 569 | 570 | elementDown = "{\\1c&H999999&}", 571 | elementHover = "{\\blur5\\1c&HFFFFFF&}" 572 | } 573 | 574 | local osc_icons = { 575 | close = "\xEE\xA4\x80", 576 | minimize = "\xEE\xA4\x81", 577 | restore = "\xEE\xA4\x82", 578 | maximize = "\xEE\xA4\x83", 579 | 580 | volume_mute = "\xEE\xA4\x84", 581 | volume_low = "\xEE\xA4\x85", 582 | volume_med = "\xEE\xA4\x86", 583 | volume_high = "\xEE\xA4\x87", 584 | volume_loud = "\xEE\xA4\x88", 585 | 586 | audio = "\xEE\xA4\x89", 587 | subtitle = "\xEE\xA4\x90", 588 | info = "\xEE\xA4\x91", 589 | fullscreen_exit = "\xEE\xA4\x92", 590 | fullscreen = "\xEE\xA4\x93", 591 | 592 | playlist_prev = "\xEE\xA4\x94", 593 | playlist_next = "\xEE\xA4\x95", 594 | chapter_prev = "\xEE\xA4\x96", 595 | chapter_next = "\xEE\xA4\x97", 596 | play = "\xEE\xA4\x98", 597 | pause = "\xEE\xA4\x99", 598 | skipback = "\xEE\xA4\xA0", 599 | skipforward = "\xEE\xA4\xA1", 600 | } 601 | 602 | -- internal states, do not touch 603 | local state = { 604 | showtime, -- time of last invocation (last mouse move) 605 | osc_visible = false, 606 | anistart, -- time when the animation started 607 | anitype, -- current type of animation 608 | animation, -- current animation alpha 609 | mouse_down_counter = 0, -- used for softrepeat 610 | active_element = nil, -- nil = none, 0 = background, 1+ = see elements[] 611 | active_event_source = nil, -- the "button" that issued the current event 612 | rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time 613 | tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds 614 | mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs 615 | initREQ = false, -- is a re-init request pending? 616 | marginsREQ = false, -- is a margins update pending? 617 | last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement 618 | mouse_in_window = false, 619 | message_text, 620 | message_hide_timer, 621 | fullscreen = false, 622 | tick_timer = nil, 623 | tick_last_time = 0, -- when the last tick() was run 624 | hide_timer = nil, 625 | cache_state = nil, 626 | idle = false, 627 | enabled = true, 628 | input_enabled = true, 629 | showhide_enabled = false, 630 | dmx_cache = 0, 631 | border = true, 632 | maximized = false, 633 | osd = mp.create_osd_overlay("ass-events"), 634 | chapter_list = {}, -- sorted by time 635 | lastvisibility = user_opts.visibility, -- save last visibility on pause if showonpause 636 | subpos = 100, -- last value of sub-pos set by the user 637 | } 638 | 639 | local thumbfast = { 640 | width = 0, 641 | height = 0, 642 | disabled = true, 643 | available = false 644 | } 645 | 646 | local window_control_box_width = 80 647 | local tick_delay = 1 / 60 648 | 649 | local is_december = os.date("*t").month == 12 650 | 651 | --- Automatically disable OSC 652 | local builtin_osc_enabled = mp.get_property_native("osc") 653 | if builtin_osc_enabled then 654 | mp.set_property_native("osc", false) 655 | end 656 | 657 | -- 658 | -- Helperfunctions 659 | -- 660 | 661 | function kill_animation() 662 | state.anistart = nil 663 | state.animation = nil 664 | state.anitype = nil 665 | end 666 | 667 | function set_osd(res_x, res_y, text) 668 | if state.osd.res_x == res_x and 669 | state.osd.res_y == res_y and 670 | state.osd.data == text then 671 | return 672 | end 673 | state.osd.res_x = res_x 674 | state.osd.res_y = res_y 675 | state.osd.data = text 676 | state.osd.z = 1000 677 | state.osd:update() 678 | end 679 | 680 | -- scale factor for translating between real and virtual ASS coordinates 681 | function get_virt_scale_factor() 682 | local w, h = mp.get_osd_size() 683 | if w <= 0 or h <= 0 then 684 | return 0, 0 685 | end 686 | return osc_param.playresx / w, osc_param.playresy / h 687 | end 688 | 689 | -- return mouse position in virtual ASS coordinates (playresx/y) 690 | function get_virt_mouse_pos() 691 | if state.mouse_in_window then 692 | local sx, sy = get_virt_scale_factor() 693 | local x, y = mp.get_mouse_pos() 694 | return x * sx, y * sy 695 | else 696 | return -1, -1 697 | end 698 | end 699 | 700 | function set_virt_mouse_area(x0, y0, x1, y1, name) 701 | local sx, sy = get_virt_scale_factor() 702 | mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name) 703 | end 704 | 705 | function scale_value(x0, x1, y0, y1, val) 706 | local m = (y1 - y0) / (x1 - x0) 707 | local b = y0 - (m * x0) 708 | return (m * val) + b 709 | end 710 | 711 | -- returns hitbox spanning coordinates (top left, bottom right corner) 712 | -- according to alignment 713 | function get_hitbox_coords(x, y, an, w, h) 714 | 715 | local alignments = { 716 | [1] = function () return x, y-h, x+w, y end, 717 | [2] = function () return x-(w/2), y-h, x+(w/2), y end, 718 | [3] = function () return x-w, y-h, x, y end, 719 | 720 | [4] = function () return x, y-(h/2), x+w, y+(h/2) end, 721 | [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end, 722 | [6] = function () return x-w, y-(h/2), x, y+(h/2) end, 723 | 724 | [7] = function () return x, y, x+w, y+h end, 725 | [8] = function () return x-(w/2), y, x+(w/2), y+h end, 726 | [9] = function () return x-w, y, x, y+h end, 727 | } 728 | 729 | return alignments[an]() 730 | end 731 | 732 | function get_hitbox_coords_geo(geometry) 733 | return get_hitbox_coords(geometry.x, geometry.y, geometry.an, 734 | geometry.w, geometry.h) 735 | end 736 | 737 | function get_element_hitbox(element) 738 | return element.hitbox.x1, element.hitbox.y1, 739 | element.hitbox.x2, element.hitbox.y2 740 | end 741 | 742 | function mouse_hit(element) 743 | return mouse_hit_coords(get_element_hitbox(element)) 744 | end 745 | 746 | function mouse_hit_coords(bX1, bY1, bX2, bY2) 747 | local mX, mY = get_virt_mouse_pos() 748 | return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2) 749 | end 750 | 751 | function limit_range(min, max, val) 752 | if val > max then 753 | val = max 754 | elseif val < min then 755 | val = min 756 | end 757 | return val 758 | end 759 | 760 | -- translate value into element coordinates 761 | function get_slider_ele_pos_for(element, val) 762 | 763 | local ele_pos = scale_value( 764 | element.slider.min.value, element.slider.max.value, 765 | element.slider.min.ele_pos, element.slider.max.ele_pos, 766 | val) 767 | 768 | return limit_range( 769 | element.slider.min.ele_pos, element.slider.max.ele_pos, 770 | ele_pos) 771 | end 772 | 773 | -- translates global (mouse) coordinates to value 774 | function get_slider_value_at(element, glob_pos) 775 | 776 | local val = scale_value( 777 | element.slider.min.glob_pos, element.slider.max.glob_pos, 778 | element.slider.min.value, element.slider.max.value, 779 | glob_pos) 780 | 781 | return limit_range( 782 | element.slider.min.value, element.slider.max.value, 783 | val) 784 | end 785 | 786 | -- get value at current mouse position 787 | function get_slider_value(element) 788 | return get_slider_value_at(element, get_virt_mouse_pos()) 789 | end 790 | 791 | function countone(val) 792 | if not (user_opts.iamaprogrammer) then 793 | val = val + 1 794 | end 795 | return val 796 | end 797 | 798 | -- align: -1 .. +1 799 | -- frame: size of the containing area 800 | -- obj: size of the object that should be positioned inside the area 801 | -- margin: min. distance from object to frame (as long as -1 <= align <= +1) 802 | function get_align(align, frame, obj, margin) 803 | return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align) 804 | end 805 | 806 | -- multiplies two alpha values, formular can probably be improved 807 | function mult_alpha(alphaA, alphaB) 808 | return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255) 809 | end 810 | 811 | function add_area(name, x1, y1, x2, y2) 812 | -- create area if needed 813 | if (osc_param.areas[name] == nil) then 814 | osc_param.areas[name] = {} 815 | end 816 | table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2}) 817 | end 818 | 819 | function ass_append_alpha(ass, alpha, modifier) 820 | local ar = {} 821 | 822 | for ai, av in pairs(alpha) do 823 | av = mult_alpha(av, modifier) 824 | if state.animation then 825 | av = mult_alpha(av, state.animation) 826 | end 827 | ar[ai] = av 828 | end 829 | 830 | ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}", 831 | ar[1], ar[2], ar[3], ar[4])) 832 | end 833 | 834 | function ass_draw_cir_cw(ass, x, y, r) 835 | ass:round_rect_cw(x-r, y-r, x+r, y+r, r) 836 | end 837 | 838 | function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2) 839 | if hexagon then 840 | ass:hexagon_cw(x0, y0, x1, y1, r1, r2) 841 | else 842 | ass:round_rect_cw(x0, y0, x1, y1, r1, r2) 843 | end 844 | end 845 | 846 | function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2) 847 | if hexagon then 848 | ass:hexagon_ccw(x0, y0, x1, y1, r1, r2) 849 | else 850 | ass:round_rect_ccw(x0, y0, x1, y1, r1, r2) 851 | end 852 | end 853 | 854 | function round(number, decimals) 855 | local power = 10^(decimals or 1) 856 | return math.floor(number * power + 0.5) / power 857 | end 858 | 859 | 860 | -- 861 | -- Tracklist Management 862 | -- 863 | 864 | local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"} 865 | 866 | -- updates the OSC internal playlists, should be run each time the track-layout changes 867 | function update_tracklist() 868 | local tracktable = mp.get_property_native("track-list", {}) 869 | 870 | -- by osc_id 871 | tracks_osc = {} 872 | tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {} 873 | -- by mpv_id 874 | tracks_mpv = {} 875 | tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {} 876 | for n = 1, #tracktable do 877 | if not (tracktable[n].type == "unknown") then 878 | local type = tracktable[n].type 879 | local mpv_id = tonumber(tracktable[n].id) 880 | 881 | -- by osc_id 882 | table.insert(tracks_osc[type], tracktable[n]) 883 | 884 | -- by mpv_id 885 | tracks_mpv[type][mpv_id] = tracktable[n] 886 | tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type] 887 | end 888 | end 889 | end 890 | 891 | -- return a nice list of tracks of the given type (video, audio, sub) 892 | function get_tracklist(type) 893 | local msg = "Available " .. nicetypes[type] .. " Tracks: " 894 | if not tracks_osc or #tracks_osc[type] == 0 then 895 | msg = msg .. "none" 896 | else 897 | for n = 1, #tracks_osc[type] do 898 | local track = tracks_osc[type][n] 899 | local lang, title, selected = "unknown", "", "○" 900 | if not(track.lang == nil) then lang = track.lang end 901 | if not(track.title == nil) then title = track.title end 902 | if (track.id == tonumber(mp.get_property(type))) then 903 | selected = "●" 904 | end 905 | msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title 906 | end 907 | end 908 | return msg 909 | end 910 | 911 | -- relatively change the track of given by tracks 912 | --(+1 -> next, -1 -> previous) 913 | function set_track(type, next) 914 | local current_track_mpv, current_track_osc 915 | if (mp.get_property(type) == "no") then 916 | current_track_osc = 0 917 | else 918 | current_track_mpv = tonumber(mp.get_property(type)) 919 | current_track_osc = tracks_mpv[type][current_track_mpv].osc_id 920 | end 921 | local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1) 922 | local new_track_mpv 923 | if new_track_osc == 0 then 924 | new_track_mpv = "no" 925 | else 926 | new_track_mpv = tracks_osc[type][new_track_osc].id 927 | end 928 | 929 | mp.commandv("set", type, new_track_mpv) 930 | 931 | if (new_track_osc == 0) then 932 | show_message(nicetypes[type] .. " Track: none") 933 | else 934 | show_message(nicetypes[type] .. " Track: " 935 | .. new_track_osc .. "/" .. #tracks_osc[type] 936 | .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] " 937 | .. (tracks_osc[type][new_track_osc].title or "")) 938 | end 939 | end 940 | 941 | -- get the currently selected track of , OSC-style counted 942 | function get_track(type) 943 | local track = mp.get_property(type) 944 | if track ~= "no" and track ~= nil then 945 | local tr = tracks_mpv[type][tonumber(track)] 946 | if tr then 947 | return tr.osc_id 948 | end 949 | end 950 | return 0 951 | end 952 | 953 | -- WindowControl helpers 954 | function window_controls_enabled() 955 | val = user_opts.windowcontrols 956 | if val == "auto" then 957 | return not state.border 958 | elseif val == "fullscreen_only" then 959 | return (not state.border) or state.fullscreen 960 | else 961 | return val ~= "no" 962 | end 963 | end 964 | 965 | function window_controls_alignment() 966 | return user_opts.windowcontrols_alignment 967 | end 968 | 969 | -- 970 | -- Element Management 971 | -- 972 | 973 | local elements = {} 974 | 975 | function prepare_elements() 976 | 977 | -- remove elements without layout or invisble 978 | local elements2 = {} 979 | for n, element in pairs(elements) do 980 | if not (element.layout == nil) and (element.visible) then 981 | table.insert(elements2, element) 982 | end 983 | end 984 | elements = elements2 985 | 986 | function elem_compare (a, b) 987 | return a.layout.layer < b.layout.layer 988 | end 989 | 990 | table.sort(elements, elem_compare) 991 | 992 | 993 | for _,element in pairs(elements) do 994 | 995 | local elem_geo = element.layout.geometry 996 | 997 | -- Calculate the hitbox 998 | local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo) 999 | element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2} 1000 | 1001 | local style_ass = assdraw.ass_new() 1002 | 1003 | -- prepare static elements 1004 | style_ass:append("{}") -- hack to troll new_event into inserting a \n 1005 | style_ass:new_event() 1006 | style_ass:pos(elem_geo.x, elem_geo.y) 1007 | style_ass:an(elem_geo.an) 1008 | style_ass:append(element.layout.style) 1009 | 1010 | element.style_ass = style_ass 1011 | 1012 | local static_ass = assdraw.ass_new() 1013 | 1014 | 1015 | if (element.type == "box") then 1016 | --draw box 1017 | static_ass:draw_start() 1018 | ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, 1019 | element.layout.box.radius, element.layout.box.hexagon) 1020 | static_ass:draw_stop() 1021 | 1022 | elseif (element.type == "slider") then 1023 | --draw static slider parts 1024 | local slider_lo = element.layout.slider 1025 | -- calculate positions of min and max points 1026 | element.slider.min.ele_pos = user_opts.seekbarhandlesize * elem_geo.h / 2 1027 | element.slider.max.ele_pos = elem_geo.w - element.slider.min.ele_pos 1028 | element.slider.min.glob_pos = element.hitbox.x1 + element.slider.min.ele_pos 1029 | element.slider.max.glob_pos = element.hitbox.x1 + element.slider.max.ele_pos 1030 | 1031 | static_ass:draw_start() 1032 | -- a hack which prepares the whole slider area to allow center placements such like an=5 1033 | static_ass:rect_cw(0, 0, elem_geo.w, elem_geo.h) 1034 | static_ass:rect_ccw(0, 0, elem_geo.w, elem_geo.h) 1035 | -- marker nibbles 1036 | if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then 1037 | local markers = element.slider.markerF() 1038 | for _,marker in pairs(markers) do 1039 | if (marker >= element.slider.min.value) and 1040 | (marker <= element.slider.max.value) then 1041 | 1042 | local s = get_slider_ele_pos_for(element, marker) 1043 | 1044 | if (slider_lo.gap > 5) then -- draw triangles 1045 | 1046 | --top 1047 | if (slider_lo.nibbles_top) then 1048 | static_ass:move_to(s - 3, slider_lo.gap - 5) 1049 | static_ass:line_to(s + 3, slider_lo.gap - 5) 1050 | static_ass:line_to(s, slider_lo.gap - 1) 1051 | end 1052 | 1053 | --bottom 1054 | if (slider_lo.nibbles_bottom) then 1055 | static_ass:move_to(s - 3, 1056 | elem_geo.h - slider_lo.gap + 5) 1057 | static_ass:line_to(s, 1058 | elem_geo.h - slider_lo.gap + 1) 1059 | static_ass:line_to(s + 3, 1060 | elem_geo.h - slider_lo.gap + 5) 1061 | end 1062 | 1063 | else -- draw 2x1px nibbles 1064 | 1065 | --top 1066 | if (slider_lo.nibbles_top) then 1067 | static_ass:rect_cw(s - 1, 0, s + 1, slider_lo.gap); 1068 | end 1069 | 1070 | --bottom 1071 | if (slider_lo.nibbles_bottom) then 1072 | static_ass:rect_cw(s - 1, 1073 | elem_geo.h - slider_lo.gap, 1074 | s + 1, elem_geo.h); 1075 | end 1076 | end 1077 | end 1078 | end 1079 | end 1080 | end 1081 | 1082 | element.static_ass = static_ass 1083 | 1084 | 1085 | -- if the element is supposed to be disabled, 1086 | -- style it accordingly and kill the eventresponders 1087 | if not (element.enabled) then 1088 | element.layout.alpha[1] = 136 1089 | element.eventresponder = nil 1090 | end 1091 | 1092 | -- gray out the element if it is toggled off 1093 | if (element.off) then 1094 | element.layout.alpha[1] = 136 1095 | end 1096 | end 1097 | end 1098 | 1099 | 1100 | -- 1101 | -- Element Rendering 1102 | -- 1103 | 1104 | -- returns nil or a chapter element from the native property chapter-list 1105 | function get_chapter(possec) 1106 | local cl = state.chapter_list -- sorted, get latest before possec, if any 1107 | 1108 | for n=#cl,1,-1 do 1109 | if possec >= cl[n].time then 1110 | return cl[n] 1111 | end 1112 | end 1113 | end 1114 | 1115 | function observe_subpos(visible, pos) 1116 | if not visible then 1117 | mp.observe_property("sub-pos", "number", observe_subpos) 1118 | elseif visible == "sub-pos" then 1119 | state.subpos = pos 1120 | else 1121 | mp.unobserve_property(observe_subpos) 1122 | end 1123 | end 1124 | 1125 | function update_subpos(is_visible) 1126 | -- source: https://github.com/Zren/mpv-osc-tethys/commit/5173241 1127 | local max_subpos = 78 -- as the starting point for decrementing 1128 | local osc_height = math.max(150, user_opts.blur_intensity) 1129 | local new_subpos = max_subpos - (osc_height - 150) / 12.5 1130 | 1131 | if (state.subpos > new_subpos) then 1132 | local final_pos = is_visible and new_subpos or state.subpos 1133 | mp.set_property_number("sub-pos", round(final_pos, 0)) 1134 | end 1135 | end 1136 | 1137 | function render_elements(master_ass) 1138 | 1139 | -- when the slider is dragged or hovered and we have a target chapter name 1140 | -- then we use it instead of the normal title. we calculate it before the 1141 | -- render iterations because the title may be rendered before the slider. 1142 | state.forced_title = nil 1143 | local se, ae = state.slider_element, elements[state.active_element] 1144 | if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then 1145 | local dur = mp.get_property_number("duration", 0) 1146 | if dur > 0 then 1147 | local possec = get_slider_value(se) * dur / 100 -- of mouse pos 1148 | local ch = get_chapter(possec) 1149 | if ch and ch.title and ch.title ~= "" then 1150 | state.forced_title = string.format(user_opts.chapter_fmt, ch.title) 1151 | end 1152 | end 1153 | end 1154 | 1155 | for n=1, #elements do 1156 | local element = elements[n] 1157 | 1158 | local style_ass = assdraw.ass_new() 1159 | style_ass:merge(element.style_ass) 1160 | ass_append_alpha(style_ass, element.layout.alpha, 0) 1161 | 1162 | if element.eventresponder and (state.active_element == n) then 1163 | 1164 | -- run render event functions 1165 | if not (element.eventresponder.render == nil) then 1166 | element.eventresponder.render(element) 1167 | end 1168 | 1169 | if mouse_hit(element) then 1170 | -- mouse down styling 1171 | if (element.styledown) then 1172 | style_ass:append(osc_styles.elementDown) 1173 | end 1174 | 1175 | if (element.softrepeat) and (state.mouse_down_counter >= 15 1176 | and state.mouse_down_counter % 5 == 0) then 1177 | 1178 | element.eventresponder[state.active_event_source.."_down"](element) 1179 | end 1180 | state.mouse_down_counter = state.mouse_down_counter + 1 1181 | end 1182 | 1183 | end 1184 | 1185 | local elem_ass = assdraw.ass_new() 1186 | 1187 | elem_ass:merge(style_ass) 1188 | 1189 | if not (element.type == "button") then 1190 | elem_ass:merge(element.static_ass) 1191 | end 1192 | 1193 | if (element.type == "slider") then 1194 | 1195 | local slider_lo = element.layout.slider 1196 | local elem_geo = element.layout.geometry 1197 | local s_min = element.slider.min.value 1198 | local s_max = element.slider.max.value 1199 | 1200 | -- draw pos marker 1201 | local pos = element.slider.posF() 1202 | local seekRanges = element.slider.seekRangesF() 1203 | local rh = user_opts.seekbarhandlesize * elem_geo.h / 2 -- Handle radius 1204 | local xp 1205 | 1206 | if pos then 1207 | xp = get_slider_ele_pos_for(element, pos) 1208 | ass_draw_cir_cw(elem_ass, xp, elem_geo.h/2, rh) 1209 | elem_ass:rect_cw(0, slider_lo.gap, xp, elem_geo.h - slider_lo.gap) 1210 | end 1211 | 1212 | if seekRanges then 1213 | elem_ass:draw_stop() 1214 | elem_ass:merge(element.style_ass) 1215 | ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha) 1216 | elem_ass:merge(element.static_ass) 1217 | 1218 | for _,range in pairs(seekRanges) do 1219 | local pstart = get_slider_ele_pos_for(element, range["start"]) 1220 | local pend = get_slider_ele_pos_for(element, range["end"]) 1221 | elem_ass:rect_cw(pstart - rh, slider_lo.gap, pend + rh, elem_geo.h - slider_lo.gap) 1222 | end 1223 | end 1224 | 1225 | elem_ass:draw_stop() 1226 | 1227 | -- add tooltip 1228 | if not (element.slider.tooltipF == nil) then 1229 | 1230 | if mouse_hit(element) then 1231 | local sliderpos = get_slider_value(element) 1232 | local tooltiplabel = element.slider.tooltipF(sliderpos) 1233 | 1234 | local an = slider_lo.tooltip_an 1235 | 1236 | local ty 1237 | 1238 | if (an == 2) then 1239 | ty = element.hitbox.y1 1240 | else 1241 | ty = element.hitbox.y1 + elem_geo.h/2 1242 | end 1243 | 1244 | local tx = get_virt_mouse_pos() 1245 | if (slider_lo.adjust_tooltip) then 1246 | if (an == 2) then 1247 | if (sliderpos < (s_min + 3)) then 1248 | an = an - 1 1249 | elseif (sliderpos > (s_max - 3)) then 1250 | an = an + 1 1251 | end 1252 | elseif (sliderpos > (s_max-s_min)/2) then 1253 | an = an + 1 1254 | tx = tx - 5 1255 | else 1256 | an = an - 1 1257 | tx = tx + 10 1258 | end 1259 | end 1260 | 1261 | -- tooltip label 1262 | elem_ass:new_event() 1263 | elem_ass:pos(tx, ty) 1264 | elem_ass:an(an) 1265 | elem_ass:append(slider_lo.tooltip_style) 1266 | ass_append_alpha(elem_ass, slider_lo.alpha, 0) 1267 | elem_ass:append(tooltiplabel) 1268 | 1269 | -- thumbnail 1270 | if thumbfast.available then 1271 | local osd_w = mp.get_property_number("osd-width") 1272 | if osd_w and not thumbfast.disabled then 1273 | local r_w, r_h = get_virt_scale_factor() 1274 | 1275 | local thumbPad = 4 1276 | local thumbMarginX = 18 / r_w 1277 | local thumbMarginY = 40 1278 | local tooltipBgColor = "000000" 1279 | local tooltipBgAlpha = 80 1280 | local thumbX = math.min(osd_w - thumbfast.width - thumbMarginX, math.max(thumbMarginX, tx / r_w - thumbfast.width / 2)) 1281 | local thumbY = ((ty - thumbMarginY) / r_h - thumbfast.height) 1282 | 1283 | elem_ass:new_event() 1284 | elem_ass:pos(thumbX * r_w, ty - thumbMarginY - thumbfast.height * r_h) 1285 | elem_ass:an(7) 1286 | elem_ass:append(("{\\bord0\\1c&H%s&\\1a&H%X&}"):format(tooltipBgColor, tooltipBgAlpha)) 1287 | elem_ass:draw_start() 1288 | elem_ass:rect_cw(-thumbPad * r_h, -thumbPad * r_h, (thumbfast.width + thumbPad) * r_w, (thumbfast.height + thumbPad) * r_h) 1289 | elem_ass:draw_stop() 1290 | 1291 | mp.commandv("script-message-to", "thumbfast", "thumb", 1292 | mp.get_property_number("duration", 0) * (sliderpos / 100), 1293 | thumbX, 1294 | thumbY 1295 | ) 1296 | end 1297 | else 1298 | display_tn_osc(ty, sliderpos, elem_ass) 1299 | end 1300 | else 1301 | if thumbfast.available then 1302 | mp.commandv("script-message-to", "thumbfast", "clear") 1303 | else 1304 | hide_thumbnail() 1305 | end 1306 | end 1307 | end 1308 | 1309 | elseif (element.type == "button") then 1310 | 1311 | local buttontext 1312 | if type(element.content) == "function" then 1313 | buttontext = element.content() -- function objects 1314 | elseif not (element.content == nil) then 1315 | buttontext = element.content -- text objects 1316 | end 1317 | 1318 | local maxchars = element.layout.button.maxchars 1319 | if not (maxchars == nil) and (#buttontext > maxchars) then 1320 | local max_ratio = 1.25 -- up to 25% more chars while shrinking 1321 | local limit = math.max(0, math.floor(maxchars * max_ratio) - 3) 1322 | if (#buttontext > limit) then 1323 | while (#buttontext > limit) do 1324 | buttontext = buttontext:gsub(".[\128-\191]*$", "") 1325 | end 1326 | buttontext = buttontext .. "..." 1327 | end 1328 | local _, nchars2 = buttontext:gsub(".[\128-\191]*", "") 1329 | local stretch = (maxchars/#buttontext)*100 1330 | buttontext = string.format("{\\fscx%f}", 1331 | (maxchars/#buttontext)*100) .. buttontext 1332 | end 1333 | 1334 | elem_ass:append(buttontext) 1335 | 1336 | -- add tooltip for audio and subtitle tracks 1337 | if not (element.tooltipF == nil) and element.enabled then 1338 | if mouse_hit(element) then 1339 | local tooltiplabel = element.tooltipF 1340 | local an = 1 1341 | local ty = element.hitbox.y1 1342 | local tx = get_virt_mouse_pos() 1343 | 1344 | if ty < osc_param.playresy / 2 then 1345 | ty = element.hitbox.y2 1346 | an = 7 1347 | end 1348 | 1349 | -- tooltip label 1350 | if type(element.tooltipF) == "function" then 1351 | tooltiplabel = element.tooltipF() 1352 | else 1353 | tooltiplabel = element.tooltipF 1354 | end 1355 | elem_ass:new_event() 1356 | elem_ass:pos(tx, ty) 1357 | elem_ass:an(an) 1358 | elem_ass:append(element.tooltip_style) 1359 | elem_ass:append(tooltiplabel) 1360 | end 1361 | end 1362 | 1363 | -- add hover effect 1364 | -- source: https://github.com/Zren/mpvz/issues/13 1365 | local button_lo = element.layout.button 1366 | if mouse_hit(element) and element.hoverable and element.enabled then 1367 | local shadow_ass = assdraw.ass_new() 1368 | shadow_ass:merge(style_ass) 1369 | shadow_ass:append(button_lo.hoverstyle .. buttontext) 1370 | elem_ass:merge(shadow_ass) 1371 | end 1372 | end 1373 | 1374 | master_ass:merge(elem_ass) 1375 | end 1376 | end 1377 | 1378 | -- 1379 | -- Message display 1380 | -- 1381 | 1382 | -- pos is 1 based 1383 | function limited_list(prop, pos) 1384 | local proplist = mp.get_property_native(prop, {}) 1385 | local count = #proplist 1386 | if count == 0 then 1387 | return count, proplist 1388 | end 1389 | 1390 | local fs = tonumber(mp.get_property('options/osd-font-size')) 1391 | local max = math.ceil(osc_param.unscaled_y*0.75 / fs) 1392 | if max % 2 == 0 then 1393 | max = max - 1 1394 | end 1395 | local delta = math.ceil(max / 2) - 1 1396 | local begi = math.max(math.min(pos - delta, count - max + 1), 1) 1397 | local endi = math.min(begi + max - 1, count) 1398 | 1399 | local reslist = {} 1400 | for i=begi, endi do 1401 | local item = proplist[i] 1402 | item.current = (i == pos) and true or nil 1403 | table.insert(reslist, item) 1404 | end 1405 | return count, reslist 1406 | end 1407 | 1408 | function get_playlist() 1409 | local pos = mp.get_property_number('playlist-pos', 0) + 1 1410 | local count, limlist = limited_list('playlist', pos) 1411 | if count == 0 then 1412 | return 'Empty playlist.' 1413 | end 1414 | 1415 | local message = string.format('Playlist [%d/%d]:\n', pos, count) 1416 | for i, v in ipairs(limlist) do 1417 | local title = v.title 1418 | local _, filename = utils.split_path(v.filename) 1419 | if title == nil then 1420 | title = filename 1421 | end 1422 | message = string.format('%s %s %s\n', message, 1423 | (v.current and '●' or '○'), title) 1424 | end 1425 | return message 1426 | end 1427 | 1428 | function get_chapterlist() 1429 | local pos = mp.get_property_number('chapter', 0) + 1 1430 | local count, limlist = limited_list('chapter-list', pos) 1431 | if count == 0 then 1432 | return 'No chapters.' 1433 | end 1434 | 1435 | local message = string.format('Chapters [%d/%d]:\n', pos, count) 1436 | for i, v in ipairs(limlist) do 1437 | local time = mp.format_time(v.time) 1438 | local title = v.title 1439 | if title == nil then 1440 | title = string.format('Chapter %02d', i) 1441 | end 1442 | message = string.format('%s[%s] %s %s\n', message, time, 1443 | (v.current and '●' or '○'), title) 1444 | end 1445 | return message 1446 | end 1447 | 1448 | function show_message(text, duration) 1449 | 1450 | --print("text: "..text.." duration: " .. duration) 1451 | if duration == nil then 1452 | duration = tonumber(mp.get_property("options/osd-duration")) / 1000 1453 | elseif not type(duration) == "number" then 1454 | print("duration: " .. duration) 1455 | end 1456 | 1457 | -- cut the text short, otherwise the following functions 1458 | -- may slow down massively on huge input 1459 | text = string.sub(text, 0, 4000) 1460 | 1461 | state.message_text = mp.command_native({"escape-ass", text}) 1462 | 1463 | if not state.message_hide_timer then 1464 | state.message_hide_timer = mp.add_timeout(0, request_tick) 1465 | end 1466 | state.message_hide_timer:kill() 1467 | state.message_hide_timer.timeout = duration 1468 | state.message_hide_timer:resume() 1469 | request_tick() 1470 | end 1471 | 1472 | function render_message(ass) 1473 | if state.message_hide_timer and state.message_hide_timer:is_enabled() and 1474 | state.message_text 1475 | then 1476 | local _, lines = string.gsub(state.message_text, "\\N", "") 1477 | 1478 | local fontsize = tonumber(mp.get_property("options/osd-font-size")) 1479 | local outline = tonumber(mp.get_property("options/osd-border-size")) 1480 | local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize) 1481 | local counterscale = osc_param.playresy / osc_param.unscaled_y 1482 | 1483 | fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1) 1484 | outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1) 1485 | 1486 | local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}" 1487 | 1488 | 1489 | ass:new_event() 1490 | ass:append(style .. state.message_text) 1491 | else 1492 | state.message_text = nil 1493 | end 1494 | end 1495 | 1496 | -- 1497 | -- Initialisation and Layout 1498 | -- 1499 | 1500 | function new_element(name, type) 1501 | elements[name] = {} 1502 | elements[name].type = type 1503 | 1504 | -- add default stuff 1505 | elements[name].eventresponder = {} 1506 | elements[name].visible = true 1507 | elements[name].enabled = true 1508 | elements[name].softrepeat = false 1509 | elements[name].styledown = (type == "button") 1510 | elements[name].hoverable = (type == "button") 1511 | elements[name].state = {} 1512 | 1513 | if (type == "slider") then 1514 | elements[name].slider = {min = {value = 0}, max = {value = 100}} 1515 | end 1516 | 1517 | return elements[name] 1518 | end 1519 | 1520 | function add_layout(name) 1521 | if not (elements[name] == nil) then 1522 | -- new layout 1523 | elements[name].layout = {} 1524 | 1525 | -- set layout defaults 1526 | elements[name].layout.layer = 50 1527 | elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255} 1528 | 1529 | if (elements[name].type == "button") then 1530 | elements[name].layout.button = { 1531 | maxchars = nil, 1532 | hoverstyle = osc_styles.elementHover, 1533 | } 1534 | elseif (elements[name].type == "slider") then 1535 | -- slider defaults 1536 | elements[name].layout.slider = { 1537 | border = 1, 1538 | gap = 1, 1539 | nibbles_top = true, 1540 | nibbles_bottom = true, 1541 | adjust_tooltip = true, 1542 | tooltip_style = "", 1543 | tooltip_an = 2, 1544 | alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255}, 1545 | } 1546 | elseif (elements[name].type == "box") then 1547 | elements[name].layout.box = {radius = 0, hexagon = false} 1548 | end 1549 | 1550 | return elements[name].layout 1551 | else 1552 | msg.error("Can't add_layout to element \""..name.."\", doesn't exist.") 1553 | end 1554 | end 1555 | 1556 | -- Window Controls 1557 | function window_controls() 1558 | local wc_geo = { 1559 | x = 0, 1560 | y = 30 + user_opts.barmargin, 1561 | an = 1, 1562 | w = osc_param.playresx, 1563 | h = 30, 1564 | } 1565 | 1566 | local alignment = window_controls_alignment() 1567 | local controlbox_w = window_control_box_width 1568 | local titlebox_w = wc_geo.w - controlbox_w 1569 | 1570 | -- Default alignment is "right" 1571 | local controlbox_left = wc_geo.w - controlbox_w 1572 | local titlebox_left = wc_geo.x 1573 | local titlebox_right = wc_geo.w - controlbox_w 1574 | 1575 | if alignment == "left" then 1576 | controlbox_left = wc_geo.x + 10 1577 | titlebox_left = wc_geo.x + controlbox_w + 10 1578 | titlebox_right = wc_geo.w 1579 | end 1580 | 1581 | add_area("window-controls", 1582 | get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an, 1583 | controlbox_w, wc_geo.h)) 1584 | 1585 | local lo 1586 | 1587 | -- Background Bar 1588 | new_element("wcbar", "box") 1589 | lo = add_layout("wcbar") 1590 | lo.geometry = wc_geo 1591 | lo.layer = 10 1592 | lo.style = osc_styles.wcBar 1593 | lo.alpha[1] = user_opts.boxalpha 1594 | 1595 | local button_y = wc_geo.y - (wc_geo.h / 2) 1596 | local first_geo = 1597 | {x = controlbox_left - 5, y = button_y, an = 4, w = 30, h = 30} 1598 | local second_geo = 1599 | {x = controlbox_left + 25, y = button_y, an = 4, w = 30, h = 30} 1600 | local third_geo = 1601 | {x = controlbox_left + 55, y = button_y, an = 4, w = 30, h = 30} 1602 | 1603 | -- Window control buttons use symbols in the custom mpv osd font 1604 | -- because the official unicode codepoints are sufficiently 1605 | -- exotic that a system might lack an installed font with them, 1606 | -- and libass will complain that they are not present in the 1607 | -- default font, even if another font with them is available. 1608 | 1609 | -- Close: 🗙 1610 | ne = new_element("close", "button") 1611 | ne.content = osc_icons.close 1612 | ne.eventresponder["mbtn_left_up"] = 1613 | function () mp.commandv("quit") end 1614 | lo = add_layout("close") 1615 | lo.geometry = alignment == "left" and first_geo or third_geo 1616 | lo.style = osc_styles.wcButtons 1617 | lo.button.hoverstyle = "{\\c&H2311E8&}" 1618 | 1619 | -- Minimize: 🗕 1620 | ne = new_element("minimize", "button") 1621 | ne.content = osc_icons.minimize 1622 | ne.eventresponder["mbtn_left_up"] = 1623 | function () mp.commandv("cycle", "window-minimized") end 1624 | lo = add_layout("minimize") 1625 | lo.geometry = alignment == "left" and second_geo or first_geo 1626 | lo.style = osc_styles.wcButtons 1627 | 1628 | -- Maximize: 🗖 /🗗 1629 | ne = new_element("maximize", "button") 1630 | if state.maximized or state.fullscreen then 1631 | ne.content = osc_icons.restore 1632 | else 1633 | ne.content = osc_icons.maximize 1634 | end 1635 | ne.eventresponder["mbtn_left_up"] = 1636 | function () 1637 | if state.fullscreen then 1638 | mp.commandv("cycle", "fullscreen") 1639 | else 1640 | mp.commandv("cycle", "window-maximized") 1641 | end 1642 | end 1643 | lo = add_layout("maximize") 1644 | lo.geometry = alignment == "left" and third_geo or second_geo 1645 | lo.style = osc_styles.wcButtons 1646 | 1647 | -- deadzone below window controls 1648 | local sh_area_y0, sh_area_y1 1649 | sh_area_y0 = user_opts.barmargin 1650 | sh_area_y1 = (wc_geo.y + (wc_geo.h / 2)) + 1651 | get_align(1 - (2 * user_opts.deadzonesize), 1652 | osc_param.playresy - (wc_geo.y + (wc_geo.h / 2)), 0, 0) 1653 | add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1) 1654 | 1655 | -- Window Title 1656 | if user_opts.windowcontrols_title then 1657 | ne = new_element("wctitle", "button") 1658 | ne.content = function () 1659 | local title = mp.command_native({"expand-text", user_opts.title}) 1660 | title = title:gsub("\n", " ") 1661 | return title ~= "" and mp.command_native({"escape-ass", title}) or "mpv" 1662 | end 1663 | ne.hoverable = false 1664 | local left_pad = 5 1665 | local right_pad = 10 1666 | lo = add_layout("wctitle") 1667 | lo.geometry = 1668 | { x = titlebox_left + left_pad, y = wc_geo.y - 3, an = 1, 1669 | w = titlebox_w, h = wc_geo.h } 1670 | lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", 1671 | osc_styles.wcTitle, 1672 | titlebox_left + left_pad, wc_geo.y - wc_geo.h, 1673 | titlebox_right - right_pad, wc_geo.y + wc_geo.h) 1674 | 1675 | add_area("window-controls-title", 1676 | titlebox_left, 0, titlebox_right, wc_geo.h) 1677 | end 1678 | end 1679 | 1680 | -- 1681 | -- Modernx Layout 1682 | -- 1683 | 1684 | function layout() 1685 | local osc_geo = { 1686 | w = osc_param.playresx, 1687 | h = 180 1688 | } 1689 | 1690 | -- update bottom margin 1691 | osc_param.video_margins.b = 1692 | math.max(180, user_opts.blur_intensity) / osc_param.playresy 1693 | 1694 | -- origin of the controllers, bottom left corner 1695 | local posX = 0 1696 | local posY = osc_param.playresy 1697 | 1698 | -- alignment 1699 | local refX = osc_geo.w / 2 1700 | local refY = posY 1701 | 1702 | -- padding to decrease when player window is tall or small 1703 | local min = round(osc_param.display_aspect) <= 0.6 1704 | local pad = min and -10 or 0 1705 | 1706 | osc_param.areas = {} -- delete areas 1707 | 1708 | -- area for active mouse input 1709 | add_area("input", get_hitbox_coords(posX, posY, 1, osc_geo.w, osc_geo.h)) 1710 | 1711 | -- deadzone above OSC 1712 | local sh_area_y0, sh_area_y1 1713 | sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize), 1714 | posY - (osc_geo.h / 2), 0, 0) 1715 | sh_area_y1 = osc_param.playresy - user_opts.barmargin 1716 | 1717 | -- area for show/hide 1718 | add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1) 1719 | 1720 | local lo, geo 1721 | 1722 | -- Controller Background 1723 | new_element("transBg", "box") 1724 | lo = add_layout("transBg") 1725 | lo.geometry = {x = posX, y = posY, an = 7, w = osc_geo.w, h = 1} 1726 | lo.style = osc_styles.transBg 1727 | lo.layer = 10 1728 | lo.alpha[3] = 0 1729 | 1730 | -- Seekbar 1731 | new_element("bgBar", "box") 1732 | lo = add_layout("bgBar") 1733 | lo.geometry = {x = refX, y = refY - 60, an = 5, w = osc_geo.w - 50, h = 2} 1734 | lo.style = osc_styles.seekbarBg 1735 | lo.layer = 13 1736 | lo.alpha[1] = 128 1737 | lo.alpha[3] = 128 1738 | 1739 | lo = add_layout("seekbar") 1740 | lo.geometry = {x = refX, y = refY - 60, an = 5, w = osc_geo.w - 50, h = 16} 1741 | lo.style = osc_styles.seekbarFg 1742 | lo.slider.gap = 7 1743 | lo.slider.tooltip_style = osc_styles.tooltip 1744 | lo.slider.tooltip_an = 2 1745 | 1746 | -- Title 1747 | geo = {x = 25, y = refY - 72, an = 1, w = osc_geo.w - 50, h = 48} 1748 | lo = add_layout("title") 1749 | lo.geometry = geo 1750 | lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", osc_styles.vidTitle, 1751 | geo.x, geo.y - geo.h, geo.x + geo.w, geo.y) 1752 | lo.alpha[3] = 0 1753 | 1754 | -- Playback control buttons 1755 | lo = add_layout("pl_prev") 1756 | lo.geometry = {x = refX - 180, y = refY - 30, an = 5, w = 30, h = 24} 1757 | lo.style = osc_styles.mediumButtons 1758 | 1759 | lo = add_layout("ch_prev") 1760 | lo.geometry = {x = refX - 120, y = refY - 30, an = 5, w = 30, h = 24} 1761 | lo.style = osc_styles.mediumButtons 1762 | 1763 | lo = add_layout("skipback") 1764 | lo.geometry = {x = refX - 60 - pad, y = refY - 30, an = 5, w = 30, h = 24} 1765 | lo.style = osc_styles.mediumButtons 1766 | 1767 | lo = add_layout("playpause") 1768 | lo.geometry = {x = refX, y = refY - 30, an = 5, w = 45, h = 45} 1769 | lo.style = osc_styles.bigButtons 1770 | 1771 | lo = add_layout("skipfrwd") 1772 | lo.geometry = {x = refX + 60 + pad, y = refY - 30, an = 5, w = 30, h = 24} 1773 | lo.style = osc_styles.mediumButtons 1774 | 1775 | lo = add_layout("ch_next") 1776 | lo.geometry = {x = refX + 120, y = refY - 30, an = 5, w = 30, h = 24} 1777 | lo.style = osc_styles.mediumButtons 1778 | 1779 | lo = add_layout("pl_next") 1780 | lo.geometry = {x = refX + 180, y = refY - 30, an = 5, w = 30, h = 24} 1781 | lo.style = osc_styles.mediumButtons 1782 | 1783 | -- Timecode 1784 | lo = add_layout("tc_left") 1785 | lo.geometry = {x = 25, y = refY - 50, an = 7, w = 120, h = 20} 1786 | lo.style = osc_styles.timecodes 1787 | 1788 | lo = add_layout("tc_right") 1789 | lo.geometry = {x = osc_geo.w - 25, y = refY - 50, an = 9, w = 120, h = 20} 1790 | lo.style = osc_styles.timecodes 1791 | 1792 | -- Volume 1793 | lo = add_layout("volume") 1794 | lo.geometry = {x = 37, y = refY - 20, an = 5, w = 24, h = 24} 1795 | lo.style = osc_styles.smallButtons 1796 | 1797 | if min then lo.geometry.x = osc_geo.w - 87 - pad end 1798 | 1799 | -- Audio tracks 1800 | lo = add_layout("cy_audio") 1801 | lo.geometry = {x = 57, y = refY - 20, an = 5, w = 24, h = 24} 1802 | lo.style = osc_styles.smallButtons 1803 | 1804 | if min then lo.geometry.x = 37 end 1805 | 1806 | -- Subtitle tracks 1807 | lo = add_layout("cy_sub") 1808 | lo.geometry = {x = 77, y = refY - 20, an = 5, w = 24, h = 24} 1809 | lo.style = osc_styles.smallButtons 1810 | 1811 | if min then lo.geometry.x = 87 + pad end 1812 | 1813 | -- Cache 1814 | -- lo = add_layout("cache") 1815 | -- lo.geometry = {x = osc_geo.w - 187, y = refY - 20, an = 5, w = 64, h = 20} 1816 | -- lo.style = osc_styles.timecodes 1817 | 1818 | -- Toggle info 1819 | lo = add_layout("tog_info") 1820 | lo.geometry = {x = osc_geo.w - 87, y = refY - 20, an = 5, w = 24, h = 24} 1821 | lo.style = osc_styles.smallButtons 1822 | 1823 | -- Playback Speed 1824 | lo = add_layout("playback_speed") 1825 | lo.geometry = {x = osc_geo.w - 147, y = refY - 20, an = 5, w = 24, h = 24} 1826 | lo.style = osc_styles.smallButtons 1827 | 1828 | -- Toggle fullscreen 1829 | lo = add_layout("tog_fs") 1830 | lo.geometry = {x = osc_geo.w - 37, y = refY - 20, an = 5, w = 24, h = 24} 1831 | lo.style = osc_styles.smallButtons 1832 | end 1833 | 1834 | -- Validate string type user options 1835 | function validate_user_opts() 1836 | if user_opts.windowcontrols ~= "auto" and 1837 | user_opts.windowcontrols ~= "fullscreen_only" and 1838 | user_opts.windowcontrols ~= "yes" and 1839 | user_opts.windowcontrols ~= "no" then 1840 | msg.warn("windowcontrols cannot be \"" .. 1841 | user_opts.windowcontrols .. "\". Ignoring.") 1842 | user_opts.windowcontrols = "auto" 1843 | end 1844 | 1845 | if user_opts.windowcontrols_alignment ~= "right" and 1846 | user_opts.windowcontrols_alignment ~= "left" then 1847 | msg.warn("windowcontrols_alignment cannot be \"" .. 1848 | user_opts.windowcontrols_alignment .. "\". Ignoring.") 1849 | user_opts.windowcontrols_alignment = "right" 1850 | end 1851 | end 1852 | 1853 | function update_options(list, changed) 1854 | validate_user_opts() 1855 | if changed.tick_delay or changed.tick_delay_follow_display_fps then 1856 | set_tick_delay("display_fps", mp.get_property_number("display_fps", nil)) 1857 | end 1858 | request_tick() 1859 | set_tick_delay("display_fps", mp.get_property_number("display_fps", nil)) 1860 | visibility_mode(user_opts.visibility, true) 1861 | update_duration_watch() 1862 | request_init() 1863 | end 1864 | 1865 | -- OSC INIT 1866 | function osc_init() 1867 | msg.debug("osc_init") 1868 | 1869 | -- set canvas resolution according to display aspect and scaling setting 1870 | local baseResY = 720 1871 | local display_w, display_h, display_aspect = mp.get_osd_size() 1872 | local scale = 1 1873 | 1874 | if (mp.get_property("video") == "no") then -- dummy/forced window 1875 | scale = user_opts.scaleforcedwindow 1876 | elseif state.fullscreen then 1877 | scale = user_opts.scalefullscreen 1878 | else 1879 | scale = user_opts.scalewindowed 1880 | end 1881 | 1882 | if user_opts.vidscale then 1883 | osc_param.unscaled_y = baseResY 1884 | else 1885 | osc_param.unscaled_y = display_h 1886 | end 1887 | osc_param.playresy = osc_param.unscaled_y / scale 1888 | if (display_aspect > 0) then 1889 | osc_param.display_aspect = display_aspect 1890 | end 1891 | osc_param.playresx = osc_param.playresy * osc_param.display_aspect 1892 | 1893 | -- stop seeking with the slider to prevent skipping files 1894 | state.active_element = nil 1895 | 1896 | elements = {} 1897 | 1898 | -- some often needed stuff 1899 | local pl_count = mp.get_property_number("playlist-count", 0) 1900 | local have_pl = (pl_count > 1) 1901 | local pl_pos = mp.get_property_number("playlist-pos", 0) + 1 1902 | local have_ch = (mp.get_property_number("chapters", 0) > 0) 1903 | local loop = mp.get_property("loop-playlist", "no") 1904 | 1905 | local ne 1906 | 1907 | -- title 1908 | ne = new_element("title", "button") 1909 | 1910 | ne.visible = user_opts.showtitle 1911 | ne.hoverable = false 1912 | ne.content = function () 1913 | local title = state.forced_title or 1914 | mp.command_native({"expand-text", user_opts.title}) 1915 | title = title:gsub("\n", " ") 1916 | return title ~= "" and mp.command_native({"escape-ass", title}) or "mpv" 1917 | end 1918 | 1919 | ne.eventresponder["mbtn_left_up"] = function () 1920 | local title = mp.get_property_osd("media-title") 1921 | if (have_pl) then 1922 | title = string.format("[%d/%d] %s", countone(pl_pos - 1), 1923 | pl_count, title) 1924 | end 1925 | show_message(title) 1926 | end 1927 | 1928 | ne.eventresponder["mbtn_right_up"] = 1929 | function () show_message(mp.get_property_osd("filename")) end 1930 | 1931 | -- 1932 | -- playlist buttons 1933 | -- 1934 | 1935 | -- prev 1936 | ne = new_element("pl_prev", "button") 1937 | 1938 | ne.content = osc_icons.playlist_prev 1939 | ne.visible = (round(osc_param.display_aspect) > 1.1) 1940 | ne.enabled = (pl_pos > 1) or (loop ~= "no") 1941 | ne.eventresponder["mbtn_left_up"] = 1942 | function () 1943 | mp.commandv("playlist-prev", "weak") 1944 | if user_opts.playlist_osd then 1945 | show_message(get_playlist(), 3) 1946 | end 1947 | end 1948 | ne.eventresponder["shift+mbtn_left_up"] = 1949 | function () show_message(get_playlist(), 3) end 1950 | ne.eventresponder["mbtn_right_up"] = 1951 | function () show_message(get_playlist(), 3) end 1952 | 1953 | -- next 1954 | ne = new_element("pl_next", "button") 1955 | 1956 | ne.content = osc_icons.playlist_next 1957 | ne.visible = (round(osc_param.display_aspect) > 1.1) 1958 | ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no") 1959 | ne.eventresponder["mbtn_left_up"] = 1960 | function () 1961 | mp.commandv("playlist-next", "weak") 1962 | if user_opts.playlist_osd then 1963 | show_message(get_playlist(), 3) 1964 | end 1965 | end 1966 | ne.eventresponder["shift+mbtn_left_up"] = 1967 | function () show_message(get_playlist(), 3) end 1968 | ne.eventresponder["mbtn_right_up"] = 1969 | function () show_message(get_playlist(), 3) end 1970 | 1971 | -- 1972 | -- big buttons 1973 | -- 1974 | 1975 | -- playpause 1976 | ne = new_element("playpause", "button") 1977 | 1978 | ne.content = function () 1979 | if mp.get_property("pause") == "yes" then 1980 | return (osc_icons.play) 1981 | else 1982 | return (osc_icons.pause) 1983 | end 1984 | end 1985 | ne.eventresponder["mbtn_left_up"] = 1986 | function () mp.commandv("cycle", "pause") end 1987 | 1988 | -- skipback 1989 | ne = new_element("skipback", "button") 1990 | 1991 | ne.softrepeat = true 1992 | ne.content = osc_icons.skipback 1993 | ne.eventresponder["mbtn_left_down"] = 1994 | function () mp.commandv("seek", -5) end 1995 | ne.eventresponder["shift+mbtn_left_down"] = 1996 | function () mp.commandv("frame-back-step") end 1997 | ne.eventresponder["mbtn_right_down"] = 1998 | function () mp.commandv("seek", -30) end 1999 | 2000 | -- skipfrwd 2001 | ne = new_element("skipfrwd", "button") 2002 | 2003 | ne.softrepeat = true 2004 | ne.content = osc_icons.skipforward 2005 | ne.eventresponder["mbtn_left_down"] = 2006 | function () mp.commandv("seek", 10) end 2007 | ne.eventresponder["shift+mbtn_left_down"] = 2008 | function () mp.commandv("frame-step") end 2009 | ne.eventresponder["mbtn_right_down"] = 2010 | function () mp.commandv("seek", 60) end 2011 | 2012 | -- ch_prev 2013 | ne = new_element("ch_prev", "button") 2014 | 2015 | ne.enabled = have_ch 2016 | ne.content = osc_icons.chapter_prev 2017 | ne.visible = (round(osc_param.display_aspect) > 0.9) 2018 | ne.eventresponder["mbtn_left_up"] = 2019 | function () 2020 | mp.commandv("add", "chapter", -1) 2021 | if user_opts.chapters_osd then 2022 | show_message(get_chapterlist(), 3) 2023 | end 2024 | end 2025 | ne.eventresponder["shift+mbtn_left_up"] = 2026 | function () show_message(get_chapterlist(), 3) end 2027 | ne.eventresponder["mbtn_right_up"] = 2028 | function () show_message(get_chapterlist(), 3) end 2029 | 2030 | -- ch_next 2031 | ne = new_element("ch_next", "button") 2032 | 2033 | ne.enabled = have_ch 2034 | ne.content = osc_icons.chapter_next 2035 | ne.visible = (round(osc_param.display_aspect) > 0.9) 2036 | ne.eventresponder["mbtn_left_up"] = 2037 | function () 2038 | mp.commandv("add", "chapter", 1) 2039 | if user_opts.chapters_osd then 2040 | show_message(get_chapterlist(), 3) 2041 | end 2042 | end 2043 | ne.eventresponder["shift+mbtn_left_up"] = 2044 | function () show_message(get_chapterlist(), 3) end 2045 | ne.eventresponder["mbtn_right_up"] = 2046 | function () show_message(get_chapterlist(), 3) end 2047 | 2048 | update_tracklist() 2049 | 2050 | -- cy_audio 2051 | ne = new_element("cy_audio", "button") 2052 | 2053 | ne.enabled = (#tracks_osc.audio > 0) 2054 | ne.off = (get_track("audio") == 0) 2055 | ne.content = osc_icons.audio 2056 | ne.tooltip_style = osc_styles.tooltip 2057 | ne.tooltipF = function () 2058 | local msg = "OFF" 2059 | if not (get_track("audio") == 0) then 2060 | msg = "Audio ["..get_track("audio").."∕"..#tracks_osc.audio.."] " 2061 | local lang = mp.get_property("current-tracks/audio/lang") or "N/A" 2062 | local title = mp.get_property("current-tracks/audio/title") or "" 2063 | msg = msg .. "(" .. lang .. ")" .. " " .. title 2064 | return msg 2065 | end 2066 | return msg 2067 | end 2068 | ne.eventresponder["mbtn_left_up"] = 2069 | function () set_track("audio", 1) end 2070 | ne.eventresponder["mbtn_right_up"] = 2071 | function () set_track("audio", -1) end 2072 | ne.eventresponder["shift+mbtn_left_down"] = 2073 | function () show_message(get_tracklist("audio"), 2) end 2074 | 2075 | -- cy_sub 2076 | ne = new_element("cy_sub", "button") 2077 | 2078 | ne.enabled = (#tracks_osc.sub > 0) 2079 | ne.off = (get_track("sub") == 0) 2080 | ne.content = osc_icons.subtitle 2081 | ne.tooltip_style = osc_styles.tooltip 2082 | ne.tooltipF = function () 2083 | local msg = "OFF" 2084 | if not (get_track("sub") == 0) then 2085 | msg = "Subtitle ["..get_track("sub").."∕"..#tracks_osc.sub.."] " 2086 | local lang = mp.get_property("current-tracks/sub/lang") or "N/A" 2087 | local title = mp.get_property("current-tracks/sub/title") or "" 2088 | msg = msg .. "(" .. lang .. ")" .. " " .. title 2089 | return msg 2090 | end 2091 | return msg 2092 | end 2093 | ne.eventresponder["mbtn_left_up"] = 2094 | function () set_track("sub", 1) end 2095 | ne.eventresponder["mbtn_right_up"] = 2096 | function () set_track("sub", -1) end 2097 | ne.eventresponder["shift+mbtn_left_down"] = 2098 | function () show_message(get_tracklist("sub"), 2) end 2099 | 2100 | -- tog_fs 2101 | ne = new_element("tog_fs", "button") 2102 | 2103 | ne.content = function () 2104 | if (state.fullscreen) then 2105 | return (osc_icons.fullscreen_exit) 2106 | else 2107 | return (osc_icons.fullscreen) 2108 | end 2109 | end 2110 | ne.eventresponder["mbtn_left_up"] = 2111 | function () mp.commandv("cycle", "fullscreen") end 2112 | 2113 | -- tog_info 2114 | ne = new_element("tog_info", "button") 2115 | 2116 | ne.content = osc_icons.info 2117 | ne.visible = (round(osc_param.display_aspect) > 0.6) 2118 | ne.eventresponder["mbtn_left_up"] = 2119 | function () mp.commandv("script-binding", "stats/display-stats-toggle") end 2120 | 2121 | -- seekbar 2122 | ne = new_element("seekbar", "slider") 2123 | 2124 | ne.enabled = not (mp.get_property("percent-pos") == nil) 2125 | state.slider_element = ne.enabled and ne or nil -- used for forced_title 2126 | ne.slider.markerF = function () 2127 | local duration = mp.get_property_number("duration") 2128 | if not (duration == nil) then 2129 | local chapters = mp.get_property_native("chapter-list", {}) 2130 | local markers = {} 2131 | for n = 1, #chapters do 2132 | markers[n] = (chapters[n].time / duration * 100) 2133 | end 2134 | return markers 2135 | else 2136 | return {} 2137 | end 2138 | end 2139 | ne.slider.posF = 2140 | function () return mp.get_property_number("percent-pos") end 2141 | ne.slider.tooltipF = function (pos) 2142 | local duration = mp.get_property_number("duration") 2143 | if not ((duration == nil) or (pos == nil)) then 2144 | possec = duration * (pos / 100) 2145 | return mp.format_time(possec) 2146 | else 2147 | return "" 2148 | end 2149 | end 2150 | ne.slider.seekRangesF = function() 2151 | if user_opts.seekrangestyle == "none" then 2152 | return nil 2153 | end 2154 | local cache_state = state.cache_state 2155 | if not cache_state then 2156 | return nil 2157 | end 2158 | local duration = mp.get_property_number("duration") 2159 | if (duration == nil) or duration <= 0 then 2160 | return nil 2161 | end 2162 | local ranges = cache_state["seekable-ranges"] 2163 | if #ranges == 0 then 2164 | return nil 2165 | end 2166 | local nranges = {} 2167 | for _, range in pairs(ranges) do 2168 | nranges[#nranges + 1] = { 2169 | ["start"] = 100 * range["start"] / duration, 2170 | ["end"] = 100 * range["end"] / duration, 2171 | } 2172 | end 2173 | return nranges 2174 | end 2175 | ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged 2176 | function (element) 2177 | -- mouse move events may pile up during seeking and may still get 2178 | -- sent when the user is done seeking, so we need to throw away 2179 | -- identical seeks 2180 | local seekto = get_slider_value(element) 2181 | if (element.state.lastseek == nil) or 2182 | (not (element.state.lastseek == seekto)) then 2183 | local flags = "absolute-percent" 2184 | if not user_opts.seekbarkeyframes then 2185 | flags = flags .. "+exact" 2186 | end 2187 | mp.commandv("seek", seekto, flags) 2188 | element.state.lastseek = seekto 2189 | end 2190 | 2191 | end 2192 | ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks 2193 | function (element) mp.commandv("seek", get_slider_value(element), 2194 | "absolute-percent+exact") end 2195 | ne.eventresponder["reset"] = 2196 | function (element) element.state.lastseek = nil end 2197 | 2198 | -- tc_left (current pos) 2199 | ne = new_element("tc_left", "button") 2200 | 2201 | ne.content = function () 2202 | if (state.tc_ms) then 2203 | return (mp.get_property_osd("playback-time/full")) 2204 | else 2205 | return (mp.get_property_osd("playback-time")) 2206 | end 2207 | end 2208 | ne.eventresponder["mbtn_left_up"] = function () 2209 | state.tc_ms = not state.tc_ms 2210 | request_init() 2211 | end 2212 | 2213 | -- tc_right (total/remaining time) 2214 | ne = new_element("tc_right", "button") 2215 | 2216 | ne.visible = (mp.get_property_number("duration", 0) > 0) 2217 | ne.content = function () 2218 | if (state.rightTC_trem) then 2219 | if state.tc_ms then 2220 | return ("-"..mp.get_property_osd("playtime-remaining/full")) 2221 | else 2222 | return ("-"..mp.get_property_osd("playtime-remaining")) 2223 | end 2224 | else 2225 | if state.tc_ms then 2226 | return (mp.get_property_osd("duration/full")) 2227 | else 2228 | return (mp.get_property_osd("duration")) 2229 | end 2230 | end 2231 | end 2232 | ne.eventresponder["mbtn_left_up"] = 2233 | function () state.rightTC_trem = not state.rightTC_trem end 2234 | 2235 | -- cache 2236 | ne = new_element("cache", "button") 2237 | 2238 | ne.visible = (round(osc_param.display_aspect) > 1.3) 2239 | ne.hoverable = false 2240 | ne.content = function () 2241 | local cache_state = state.cache_state 2242 | if not (cache_state and cache_state["seekable-ranges"] and 2243 | #cache_state["seekable-ranges"] > 0) then 2244 | -- probably not a network stream 2245 | return "" 2246 | end 2247 | local dmx_cache = cache_state and cache_state["cache-duration"] 2248 | local thresh = math.min(state.dmx_cache * 0.05, 5) -- 5% or 5s 2249 | if dmx_cache and math.abs(dmx_cache - state.dmx_cache) >= thresh then 2250 | state.dmx_cache = dmx_cache 2251 | else 2252 | dmx_cache = state.dmx_cache 2253 | end 2254 | local min = math.floor(dmx_cache / 60) 2255 | local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60 2256 | return "Cache: " .. (min > 0 and 2257 | string.format("%sm%02.0fs", min, sec) or 2258 | string.format("%3.0fs", sec)) 2259 | end 2260 | 2261 | -- volume 2262 | ne = new_element("volume", "button") 2263 | 2264 | ne.content = function() 2265 | local volume = mp.get_property_number("volume", 0) 2266 | local mute = mp.get_property_native("mute") 2267 | local volicon = {osc_icons.volume_low, osc_icons.volume_med, 2268 | osc_icons.volume_high, osc_icons.volume_loud} 2269 | if volume == 0 or mute then 2270 | return osc_icons.volume_mute 2271 | else 2272 | return volicon[math.min(4,math.ceil(volume / (100/3)))] 2273 | end 2274 | end 2275 | ne.eventresponder["mbtn_left_up"] = 2276 | function () mp.commandv("cycle", "mute") end 2277 | 2278 | ne.eventresponder["wheel_up_press"] = 2279 | function () mp.commandv("osd-auto", "add", "volume", 5) end 2280 | ne.eventresponder["wheel_down_press"] = 2281 | function () mp.commandv("osd-auto", "add", "volume", -5) end 2282 | 2283 | -- playback speed 2284 | ne = new_element("playback_speed", "button") 2285 | 2286 | ne.content = function() 2287 | local speed = mp.get_property_number("speed", 1.0) 2288 | return string.format("%.2fx", speed) 2289 | end 2290 | 2291 | ne.eventresponder["mbtn_left_up"] = 2292 | function () 2293 | local speeds = {1.0, 1.25, 1.5, 1.75, 2.0} -- List of playback speeds 2294 | local current_speed = mp.get_property_number("speed", 1.0) 2295 | local next_speed = speeds[1] -- Default to the first speed in case current speed isn't found 2296 | 2297 | for i = 1, #speeds do 2298 | if current_speed == speeds[i] then 2299 | next_speed = speeds[(i % #speeds) + 1] 2300 | break 2301 | end 2302 | end 2303 | 2304 | mp.set_property("speed", next_speed) 2305 | end 2306 | 2307 | ne.eventresponder["mbtn_right_up"] = 2308 | function () 2309 | mp.set_property("speed", 1.0) 2310 | end 2311 | 2312 | ne.eventresponder["wheel_up_press"] = 2313 | function () mp.commandv("osd-auto", "add", "speed", 0.25) end 2314 | ne.eventresponder["wheel_down_press"] = 2315 | function () mp.commandv("osd-auto", "add", "speed", -0.25) end 2316 | -- load layout 2317 | layout() 2318 | 2319 | -- load window controls 2320 | if window_controls_enabled() then 2321 | window_controls() 2322 | end 2323 | 2324 | -- do something with the elements 2325 | prepare_elements() 2326 | end 2327 | 2328 | function update_margins() 2329 | local margins = osc_param.video_margins 2330 | 2331 | -- Don't use margins if it's visible only temporarily. 2332 | if (not state.osc_visible) or 2333 | (state.fullscreen and not user_opts.showfullscreen) or 2334 | (not state.fullscreen and not user_opts.showwindowed) 2335 | then 2336 | margins = {l = 0, r = 0, t = 0, b = 0} 2337 | end 2338 | 2339 | if user_opts.userdataAvail then 2340 | mp.set_property_native("user-data/osc/margins", { 2341 | l = margins.l, r = margins.r, t = margins.t, b = margins.b, 2342 | }) 2343 | else 2344 | utils.shared_script_property_set("osc-margins", 2345 | string.format("%f,%f,%f,%f", margins.l, margins.r, margins.t, margins.b)) 2346 | end 2347 | end 2348 | 2349 | -- 2350 | -- Other important stuff 2351 | -- 2352 | 2353 | function show_osc() 2354 | -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding 2355 | if not state.enabled then return end 2356 | 2357 | msg.trace("show_osc") 2358 | --remember last time of invocation (mouse move) 2359 | state.showtime = mp.get_time() 2360 | 2361 | osc_visible(true) 2362 | 2363 | if (user_opts.fadeduration > 0) then 2364 | state.anitype = nil 2365 | end 2366 | end 2367 | 2368 | function hide_osc() 2369 | msg.trace("hide_osc") 2370 | if not state.enabled then 2371 | -- typically hide happens at render() from tick(), but now tick() is 2372 | -- no-op and won't render again to remove the osc, so do that manually. 2373 | state.osc_visible = false 2374 | update_subpos(false) 2375 | render_wipe() 2376 | elseif (user_opts.fadeduration > 0) then 2377 | if not(state.osc_visible == false) then 2378 | state.anitype = "out" 2379 | request_tick() 2380 | end 2381 | else 2382 | osc_visible(false) 2383 | end 2384 | end 2385 | 2386 | function osc_visible(visible) 2387 | if state.osc_visible ~= visible then 2388 | state.osc_visible = visible 2389 | if user_opts.movesub then 2390 | observe_subpos(visible) 2391 | update_subpos(visible) 2392 | end 2393 | update_margins() 2394 | end 2395 | request_tick() 2396 | end 2397 | 2398 | function pause_state(name, enabled) 2399 | state.paused = enabled 2400 | mp.add_timeout(0.1, function() state.osd:update() end) 2401 | if user_opts.showonpause then 2402 | if enabled then 2403 | state.lastvisibility = user_opts.visibility 2404 | visibility_mode("always", true) 2405 | show_osc() 2406 | else 2407 | visibility_mode(state.lastvisibility, true) 2408 | end 2409 | end 2410 | request_tick() 2411 | end 2412 | 2413 | function cache_state(name, st) 2414 | state.cache_state = st 2415 | request_tick() 2416 | end 2417 | 2418 | -- Request that tick() is called (which typically re-renders the OSC). 2419 | -- The tick is then either executed immediately, or rate-limited if it was 2420 | -- called a small time ago. 2421 | function request_tick() 2422 | if state.tick_timer == nil then 2423 | state.tick_timer = mp.add_timeout(0, tick) 2424 | end 2425 | 2426 | if not state.tick_timer:is_enabled() then 2427 | local now = mp.get_time() 2428 | local timeout = tick_delay - (now - state.tick_last_time) 2429 | if timeout < 0 then 2430 | timeout = 0 2431 | end 2432 | state.tick_timer.timeout = timeout 2433 | state.tick_timer:resume() 2434 | end 2435 | end 2436 | 2437 | function mouse_leave() 2438 | if get_hidetimeout() >= 0 then 2439 | hide_osc() 2440 | end 2441 | -- reset mouse position 2442 | state.last_mouseX, state.last_mouseY = nil, nil 2443 | state.mouse_in_window = false 2444 | end 2445 | 2446 | function request_init() 2447 | state.initREQ = true 2448 | request_tick() 2449 | end 2450 | 2451 | -- Like request_init(), but also request an immediate update 2452 | function request_init_resize() 2453 | request_init() 2454 | -- ensure immediate update 2455 | state.tick_timer:kill() 2456 | state.tick_timer.timeout = 0 2457 | state.tick_timer:resume() 2458 | end 2459 | 2460 | function render_wipe() 2461 | msg.trace("render_wipe()") 2462 | state.osd.data = "" -- allows set_osd to immediately update on enable 2463 | state.osd:remove() 2464 | end 2465 | 2466 | function render() 2467 | msg.trace("rendering") 2468 | local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size() 2469 | local mouseX, mouseY = get_virt_mouse_pos() 2470 | local now = mp.get_time() 2471 | 2472 | -- check if display changed, if so request reinit 2473 | if not (state.mp_screen_sizeX == current_screen_sizeX 2474 | and state.mp_screen_sizeY == current_screen_sizeY) then 2475 | 2476 | request_init_resize() 2477 | 2478 | state.mp_screen_sizeX = current_screen_sizeX 2479 | state.mp_screen_sizeY = current_screen_sizeY 2480 | end 2481 | 2482 | -- init management 2483 | if state.active_element then 2484 | -- mouse is held down on some element - keep ticking and igore initReq 2485 | -- till it's released, or else the mouse-up (click) will misbehave or 2486 | -- get ignored. that's because osc_init() recreates the osc elements, 2487 | -- but mouse handling depends on the elements staying unmodified 2488 | -- between mouse-down and mouse-up (using the index active_element). 2489 | request_tick() 2490 | elseif state.initREQ then 2491 | osc_init() 2492 | state.initREQ = false 2493 | 2494 | -- store initial mouse position 2495 | if (state.last_mouseX == nil or state.last_mouseY == nil) 2496 | and not (mouseX == nil or mouseY == nil) then 2497 | 2498 | state.last_mouseX, state.last_mouseY = mouseX, mouseY 2499 | end 2500 | end 2501 | 2502 | 2503 | -- fade animation 2504 | if not(state.anitype == nil) then 2505 | 2506 | if (state.anistart == nil) then 2507 | state.anistart = now 2508 | end 2509 | 2510 | if (now < state.anistart + (user_opts.fadeduration/1000)) then 2511 | 2512 | if (state.anitype == "in") then --fade in 2513 | osc_visible(true) 2514 | state.animation = scale_value(state.anistart, 2515 | (state.anistart + (user_opts.fadeduration/1000)), 2516 | 255, 0, now) 2517 | elseif (state.anitype == "out") then --fade out 2518 | state.animation = scale_value(state.anistart, 2519 | (state.anistart + (user_opts.fadeduration/1000)), 2520 | 0, 255, now) 2521 | end 2522 | 2523 | else 2524 | if (state.anitype == "out") then 2525 | osc_visible(false) 2526 | end 2527 | kill_animation() 2528 | end 2529 | else 2530 | kill_animation() 2531 | end 2532 | 2533 | -- mouse show/hide area 2534 | for k,cords in pairs(osc_param.areas["showhide"]) do 2535 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide") 2536 | end 2537 | if osc_param.areas["showhide_wc"] then 2538 | for k,cords in pairs(osc_param.areas["showhide_wc"]) do 2539 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc") 2540 | end 2541 | else 2542 | set_virt_mouse_area(0, 0, 0, 0, "showhide_wc") 2543 | end 2544 | do_enable_keybindings() 2545 | 2546 | -- mouse input area 2547 | local mouse_over_osc = false 2548 | 2549 | for _,cords in ipairs(osc_param.areas["input"]) do 2550 | if state.osc_visible then -- activate only when OSC is actually visible 2551 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input") 2552 | end 2553 | if state.osc_visible ~= state.input_enabled then 2554 | if state.osc_visible then 2555 | mp.enable_key_bindings("input") 2556 | else 2557 | mp.disable_key_bindings("input") 2558 | end 2559 | state.input_enabled = state.osc_visible 2560 | end 2561 | 2562 | if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then 2563 | mouse_over_osc = true 2564 | end 2565 | end 2566 | 2567 | if osc_param.areas["window-controls"] then 2568 | for _,cords in ipairs(osc_param.areas["window-controls"]) do 2569 | if state.osc_visible then -- activate only when OSC is actually visible 2570 | set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls") 2571 | mp.enable_key_bindings("window-controls") 2572 | else 2573 | mp.disable_key_bindings("window-controls") 2574 | end 2575 | 2576 | if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then 2577 | mouse_over_osc = true 2578 | end 2579 | end 2580 | end 2581 | 2582 | if osc_param.areas["window-controls-title"] then 2583 | for _,cords in ipairs(osc_param.areas["window-controls-title"]) do 2584 | if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then 2585 | mouse_over_osc = true 2586 | end 2587 | end 2588 | end 2589 | 2590 | -- autohide 2591 | if not (state.showtime == nil) and (get_hidetimeout() >= 0) then 2592 | local timeout = state.showtime + (get_hidetimeout()/1000) - now 2593 | if timeout <= 0 then 2594 | if (state.active_element == nil) and not (mouse_over_osc) then 2595 | hide_osc() 2596 | end 2597 | else 2598 | -- the timer is only used to recheck the state and to possibly run 2599 | -- the code above again 2600 | if not state.hide_timer then 2601 | state.hide_timer = mp.add_timeout(0, tick) 2602 | end 2603 | state.hide_timer.timeout = timeout 2604 | -- re-arm 2605 | state.hide_timer:kill() 2606 | state.hide_timer:resume() 2607 | end 2608 | end 2609 | 2610 | 2611 | -- actual rendering 2612 | local ass = assdraw.ass_new() 2613 | 2614 | -- Messages 2615 | render_message(ass) 2616 | 2617 | -- actual OSC 2618 | if state.osc_visible then 2619 | render_elements(ass) 2620 | else 2621 | hide_thumbnail() 2622 | end 2623 | 2624 | -- submit 2625 | set_osd(osc_param.playresy * osc_param.display_aspect, 2626 | osc_param.playresy, ass.text) 2627 | end 2628 | 2629 | -- 2630 | -- Event handling 2631 | -- 2632 | 2633 | local function element_has_action(element, action) 2634 | return element and element.eventresponder and 2635 | element.eventresponder[action] 2636 | end 2637 | 2638 | function process_event(source, what) 2639 | local action = string.format("%s%s", source, 2640 | what and ("_" .. what) or "") 2641 | 2642 | if what == "down" or what == "press" then 2643 | 2644 | for n = 1, #elements do 2645 | 2646 | if mouse_hit(elements[n]) and 2647 | elements[n].eventresponder and 2648 | (elements[n].eventresponder[source .. "_up"] or 2649 | elements[n].eventresponder[action]) then 2650 | 2651 | if what == "down" then 2652 | state.active_element = n 2653 | state.active_event_source = source 2654 | end 2655 | -- fire the down or press event if the element has one 2656 | if element_has_action(elements[n], action) then 2657 | elements[n].eventresponder[action](elements[n]) 2658 | end 2659 | 2660 | end 2661 | end 2662 | 2663 | elseif what == "up" then 2664 | 2665 | if elements[state.active_element] then 2666 | local n = state.active_element 2667 | 2668 | if n == 0 then 2669 | --click on background (does not work) 2670 | elseif element_has_action(elements[n], action) and 2671 | mouse_hit(elements[n]) then 2672 | 2673 | elements[n].eventresponder[action](elements[n]) 2674 | end 2675 | 2676 | --reset active element 2677 | if element_has_action(elements[n], "reset") then 2678 | elements[n].eventresponder["reset"](elements[n]) 2679 | end 2680 | 2681 | end 2682 | state.active_element = nil 2683 | state.mouse_down_counter = 0 2684 | 2685 | elseif source == "mouse_move" then 2686 | 2687 | state.mouse_in_window = true 2688 | 2689 | local mouseX, mouseY = get_virt_mouse_pos() 2690 | if (user_opts.minmousemove == 0) or 2691 | (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and 2692 | ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove) 2693 | or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove) 2694 | ) 2695 | ) then 2696 | show_osc() 2697 | end 2698 | state.last_mouseX, state.last_mouseY = mouseX, mouseY 2699 | 2700 | local n = state.active_element 2701 | if element_has_action(elements[n], action) then 2702 | elements[n].eventresponder[action](elements[n]) 2703 | end 2704 | end 2705 | 2706 | -- ensure rendering after any (mouse) event - icons could change etc 2707 | request_tick() 2708 | end 2709 | 2710 | 2711 | local logo_lines = { 2712 | -- White border 2713 | "{\\c&HE5E5E5&\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 {\\p0}", 2714 | -- Purple fill 2715 | "{\\c&H682167&\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42{\\p0}", 2716 | -- Darker fill 2717 | "{\\c&H430142&\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}", 2718 | -- White fill 2719 | "{\\c&HDDDBDD&\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}", 2720 | -- Triangle 2721 | "{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}", 2722 | } 2723 | 2724 | local santa_hat_lines = { 2725 | -- Pompoms 2726 | "{\\c&HC0C0C0&\\p6}m 500 -323 b 491 -322 481 -318 475 -311 465 -312 456 -319 446 -318 434 -314 427 -304 417 -297 410 -290 404 -282 395 -278 390 -274 387 -267 381 -265 377 -261 379 -254 384 -253 397 -244 409 -232 425 -228 437 -228 446 -218 457 -217 462 -216 466 -213 468 -209 471 -205 477 -203 482 -206 491 -211 499 -217 508 -222 532 -235 556 -249 576 -267 584 -272 584 -284 578 -290 569 -305 550 -312 533 -309 523 -310 515 -316 507 -321 505 -323 503 -323 500 -323{\\p0}", 2727 | "{\\c&HE0E0E0&\\p6}m 315 -260 b 286 -258 259 -240 246 -215 235 -210 222 -215 211 -211 204 -188 177 -176 172 -151 170 -139 163 -128 154 -121 143 -103 141 -81 143 -60 139 -46 125 -34 129 -17 132 -1 134 16 142 30 145 56 161 80 181 96 196 114 210 133 231 144 266 153 303 138 328 115 373 79 401 28 423 -24 446 -73 465 -123 483 -174 487 -199 467 -225 442 -227 421 -232 402 -242 384 -254 364 -259 342 -250 322 -260 320 -260 317 -261 315 -260{\\p0}", 2728 | -- Main cap 2729 | "{\\c&H0000F0&\\p6}m 1151 -523 b 1016 -516 891 -458 769 -406 693 -369 624 -319 561 -262 526 -252 465 -235 479 -187 502 -147 551 -135 588 -111 1115 165 1379 232 1909 761 1926 800 1952 834 1987 858 2020 883 2053 912 2065 952 2088 1000 2146 962 2139 919 2162 836 2156 747 2143 662 2131 615 2116 567 2122 517 2120 410 2090 306 2089 199 2092 147 2071 99 2034 64 1987 5 1928 -41 1869 -86 1777 -157 1712 -256 1629 -337 1578 -389 1521 -436 1461 -476 1407 -509 1343 -507 1284 -515 1240 -519 1195 -521 1151 -523{\\p0}", 2730 | -- Cap shadow 2731 | "{\\c&H0000AA&\\p6}m 1657 248 b 1658 254 1659 261 1660 267 1669 276 1680 284 1689 293 1695 302 1700 311 1707 320 1716 325 1726 330 1735 335 1744 347 1752 360 1761 371 1753 352 1754 331 1753 311 1751 237 1751 163 1751 90 1752 64 1752 37 1767 14 1778 -3 1785 -24 1786 -45 1786 -60 1786 -77 1774 -87 1760 -96 1750 -78 1751 -65 1748 -37 1750 -8 1750 20 1734 78 1715 134 1699 192 1694 211 1689 231 1676 246 1671 251 1661 255 1657 248 m 1909 541 b 1914 542 1922 549 1917 539 1919 520 1921 502 1919 483 1918 458 1917 433 1915 407 1930 373 1942 338 1947 301 1952 270 1954 238 1951 207 1946 214 1947 229 1945 239 1939 278 1936 318 1924 356 1923 362 1913 382 1912 364 1906 301 1904 237 1891 175 1887 150 1892 126 1892 101 1892 68 1893 35 1888 2 1884 -9 1871 -20 1859 -14 1851 -6 1854 9 1854 20 1855 58 1864 95 1873 132 1883 179 1894 225 1899 273 1908 362 1910 451 1909 541{\\p0}", 2732 | -- Brim and tip pompom 2733 | "{\\c&HF8F8F8&\\p6}m 626 -191 b 565 -155 486 -196 428 -151 387 -115 327 -101 304 -47 273 2 267 59 249 113 219 157 217 213 215 265 217 309 260 302 285 283 373 264 465 264 555 257 608 252 655 292 709 287 759 294 816 276 863 298 903 340 972 324 1012 367 1061 394 1125 382 1167 424 1213 462 1268 482 1322 506 1385 546 1427 610 1479 662 1510 690 1534 725 1566 752 1611 796 1664 830 1703 880 1740 918 1747 986 1805 1005 1863 991 1897 932 1916 880 1914 823 1945 777 1961 725 1979 673 1957 622 1938 575 1912 534 1862 515 1836 473 1790 417 1755 351 1697 305 1658 266 1633 216 1593 176 1574 138 1539 116 1497 110 1448 101 1402 77 1371 37 1346 -16 1295 15 1254 6 1211 -27 1170 -62 1121 -86 1072 -104 1027 -128 976 -133 914 -130 851 -137 794 -162 740 -181 679 -168 626 -191 m 2051 917 b 1971 932 1929 1017 1919 1091 1912 1149 1923 1214 1970 1254 2000 1279 2027 1314 2066 1325 2139 1338 2212 1295 2254 1238 2281 1203 2287 1158 2282 1116 2292 1061 2273 1006 2229 970 2206 941 2167 938 2138 918{\\p0}", 2734 | } 2735 | 2736 | -- called by mpv on every frame 2737 | function tick() 2738 | if state.marginsREQ == true then 2739 | update_margins() 2740 | state.marginsREQ = false 2741 | end 2742 | 2743 | if (not state.enabled) then return end 2744 | 2745 | if (state.idle) then 2746 | 2747 | -- render idle message 2748 | msg.trace("idle message") 2749 | local icon_x, icon_y = 320 - 26, 140 2750 | local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y) 2751 | 2752 | local ass = assdraw.ass_new() 2753 | -- mpv logo 2754 | if user_opts.idlescreen then 2755 | for i, line in ipairs(logo_lines) do 2756 | ass:new_event() 2757 | ass:append(line_prefix .. line) 2758 | end 2759 | end 2760 | 2761 | -- Santa hat 2762 | if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then 2763 | for i, line in ipairs(santa_hat_lines) do 2764 | ass:new_event() 2765 | ass:append(line_prefix .. line) 2766 | end 2767 | end 2768 | 2769 | if user_opts.idlescreen then 2770 | ass:new_event() 2771 | ass:pos(320, icon_y+65) 2772 | ass:an(8) 2773 | ass:append("Drop files or URLs to play here.") 2774 | end 2775 | set_osd(640, 360, ass.text) 2776 | 2777 | if state.showhide_enabled then 2778 | mp.disable_key_bindings("showhide") 2779 | mp.disable_key_bindings("showhide_wc") 2780 | state.showhide_enabled = false 2781 | end 2782 | 2783 | 2784 | elseif (state.fullscreen and user_opts.showfullscreen) 2785 | or (not state.fullscreen and user_opts.showwindowed) then 2786 | 2787 | -- render the OSC 2788 | render() 2789 | else 2790 | -- Flush OSD 2791 | render_wipe() 2792 | end 2793 | 2794 | state.tick_last_time = mp.get_time() 2795 | 2796 | if state.anitype ~= nil then 2797 | -- state.anistart can be nil - animation should now start, or it can 2798 | -- be a timestamp when it started. state.idle has no animation. 2799 | if not state.idle and 2800 | (not state.anistart or 2801 | mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000) 2802 | then 2803 | -- animating or starting, or still within 1s past the deadline 2804 | request_tick() 2805 | else 2806 | kill_animation() 2807 | end 2808 | end 2809 | end 2810 | 2811 | function do_enable_keybindings() 2812 | if state.enabled then 2813 | if not state.showhide_enabled then 2814 | mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor") 2815 | mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor") 2816 | end 2817 | state.showhide_enabled = true 2818 | end 2819 | end 2820 | 2821 | function enable_osc(enable) 2822 | state.enabled = enable 2823 | if enable then 2824 | do_enable_keybindings() 2825 | else 2826 | hide_osc() -- acts immediately when state.enabled == false 2827 | if state.showhide_enabled then 2828 | mp.disable_key_bindings("showhide") 2829 | mp.disable_key_bindings("showhide_wc") 2830 | end 2831 | state.showhide_enabled = false 2832 | end 2833 | end 2834 | 2835 | -- duration is observed for the sole purpose of updating chapter markers 2836 | -- positions. live streams with chapters are very rare, and the update is also 2837 | -- expensive (with request_init), so it's only observed when we have chapters 2838 | -- and the user didn't disable the livemarkers option (update_duration_watch). 2839 | function on_duration() request_init() end 2840 | 2841 | local duration_watched = false 2842 | function update_duration_watch() 2843 | local want_watch = user_opts.livemarkers and 2844 | (mp.get_property_number("chapters", 0) or 0) > 0 and 2845 | true or false -- ensure it's a boolean 2846 | 2847 | if (want_watch ~= duration_watched) then 2848 | if want_watch then 2849 | mp.observe_property("duration", nil, on_duration) 2850 | else 2851 | mp.unobserve_property(on_duration) 2852 | end 2853 | duration_watched = want_watch 2854 | end 2855 | end 2856 | 2857 | validate_user_opts() 2858 | update_duration_watch() 2859 | 2860 | local function set_tick_delay(_, display_fps) 2861 | -- may be nil if unavailable or 0 fps is reported 2862 | if not display_fps or not user_opts.tick_delay_follow_display_fps then 2863 | tick_delay = user_opts.tick_delay 2864 | return 2865 | end 2866 | tick_delay = 1 / display_fps 2867 | end 2868 | 2869 | mp.register_event("start-file", request_init) 2870 | if user_opts.showonstart then mp.register_event("file-loaded", show_osc) end 2871 | if user_opts.showonseek then mp.register_event("seek", show_osc) end 2872 | 2873 | mp.observe_property("track-list", nil, request_init) 2874 | mp.observe_property("playlist", nil, request_init) 2875 | mp.observe_property("chapter-list", "native", function(_, list) 2876 | list = list or {} -- safety, shouldn't return nil 2877 | table.sort(list, function(a, b) return a.time < b.time end) 2878 | state.chapter_list = list 2879 | update_duration_watch() 2880 | request_init() 2881 | end) 2882 | mp.observe_property("sub-pos", "number", observe_subpos) 2883 | 2884 | mp.register_script_message("osc-message", show_message) 2885 | mp.register_script_message("osc-chapterlist", function(dur) 2886 | show_message(get_chapterlist(), dur) 2887 | end) 2888 | mp.register_script_message("osc-playlist", function(dur) 2889 | show_message(get_playlist(), dur) 2890 | end) 2891 | mp.register_script_message("osc-tracklist", function(dur) 2892 | local msg = {} 2893 | for k,v in pairs(nicetypes) do 2894 | table.insert(msg, get_tracklist(k)) 2895 | end 2896 | show_message(table.concat(msg, '\n\n'), dur) 2897 | end) 2898 | 2899 | mp.observe_property("fullscreen", "bool", function(_, val) 2900 | state.fullscreen = val 2901 | state.marginsREQ = true 2902 | request_init_resize() 2903 | end) 2904 | mp.observe_property("border", "bool", function(_, val) 2905 | state.border = val 2906 | request_init_resize() 2907 | end) 2908 | mp.observe_property("window-maximized", "bool", function(_, val) 2909 | state.maximized = val 2910 | request_init_resize() 2911 | end) 2912 | mp.observe_property("idle-active", "bool", function(_, val) 2913 | state.idle = val 2914 | request_tick() 2915 | end) 2916 | 2917 | mp.observe_property("display-fps", "number", set_tick_delay) 2918 | mp.observe_property("pause", "bool", pause_state) 2919 | mp.observe_property("demuxer-cache-state", "native", cache_state) 2920 | mp.observe_property("vo-configured", "bool", function(name, val) 2921 | request_tick() 2922 | end) 2923 | mp.observe_property("playback-time", "number", function(name, val) 2924 | request_tick() 2925 | end) 2926 | mp.observe_property("osd-dimensions", "native", function(name, val) 2927 | -- (we could use the value instead of re-querying it all the time, but then 2928 | -- we might have to worry about property update ordering) 2929 | request_init_resize() 2930 | end) 2931 | 2932 | -- mouse show/hide bindings 2933 | mp.set_key_bindings({ 2934 | {"mouse_move", function(e) process_event("mouse_move", nil) end}, 2935 | {"mouse_leave", mouse_leave}, 2936 | }, "showhide", "force") 2937 | mp.set_key_bindings({ 2938 | {"mouse_move", function(e) process_event("mouse_move", nil) end}, 2939 | {"mouse_leave", mouse_leave}, 2940 | }, "showhide_wc", "force") 2941 | do_enable_keybindings() 2942 | 2943 | --mouse input bindings 2944 | mp.set_key_bindings({ 2945 | {"mbtn_left", function(e) process_event("mbtn_left", "up") end, 2946 | function(e) process_event("mbtn_left", "down") end}, 2947 | {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end, 2948 | function(e) process_event("shift+mbtn_left", "down") end}, 2949 | {"mbtn_right", function(e) process_event("mbtn_right", "up") end, 2950 | function(e) process_event("mbtn_right", "down") end}, 2951 | -- alias to shift_mbtn_left for single-handed mouse use 2952 | {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end, 2953 | function(e) process_event("shift+mbtn_left", "down") end}, 2954 | {"wheel_up", function(e) process_event("wheel_up", "press") end}, 2955 | {"wheel_down", function(e) process_event("wheel_down", "press") end}, 2956 | {"mbtn_left_dbl", "ignore"}, 2957 | {"shift+mbtn_left_dbl", "ignore"}, 2958 | {"mbtn_right_dbl", "ignore"}, 2959 | }, "input", "force") 2960 | mp.enable_key_bindings("input") 2961 | 2962 | mp.set_key_bindings({ 2963 | {"mbtn_left", function(e) process_event("mbtn_left", "up") end, 2964 | function(e) process_event("mbtn_left", "down") end}, 2965 | }, "window-controls", "force") 2966 | mp.enable_key_bindings("window-controls") 2967 | 2968 | function get_hidetimeout() 2969 | if user_opts.visibility == "always" then 2970 | return -1 -- disable autohide 2971 | end 2972 | return user_opts.hidetimeout 2973 | end 2974 | 2975 | function always_on(val) 2976 | if state.enabled then 2977 | if val then 2978 | show_osc() 2979 | else 2980 | hide_osc() 2981 | end 2982 | end 2983 | end 2984 | 2985 | -- mode can be auto/always/never/cycle 2986 | -- the modes only affect internal variables and not stored on its own. 2987 | function visibility_mode(mode, no_osd) 2988 | if mode == "cycle" then 2989 | if not state.enabled then 2990 | mode = "auto" 2991 | elseif user_opts.visibility ~= "always" then 2992 | mode = "always" 2993 | else 2994 | mode = "never" 2995 | end 2996 | end 2997 | 2998 | if mode == "auto" then 2999 | always_on(false) 3000 | enable_osc(true) 3001 | elseif mode == "always" then 3002 | enable_osc(true) 3003 | always_on(true) 3004 | elseif mode == "never" then 3005 | enable_osc(false) 3006 | else 3007 | msg.warn("Ignoring unknown visibility mode '" .. mode .. "'") 3008 | return 3009 | end 3010 | 3011 | user_opts.visibility = mode 3012 | if user_opts.userdataAvail then 3013 | mp.set_property_native("user-data/osc/visibility", mode) 3014 | else 3015 | utils.shared_script_property_set("osc-visibility", mode) 3016 | end if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then 3017 | mp.osd_message("OSC visibility: " .. mode) 3018 | end 3019 | 3020 | -- Reset the input state on a mode change. The input state will be 3021 | -- recalculated on the next render cycle, except in 'never' mode where it 3022 | -- will just stay disabled. 3023 | mp.disable_key_bindings("input") 3024 | mp.disable_key_bindings("window-controls") 3025 | state.input_enabled = false 3026 | 3027 | update_margins() 3028 | request_tick() 3029 | end 3030 | 3031 | function idlescreen_visibility(mode, no_osd) 3032 | if mode == "cycle" then 3033 | if user_opts.idlescreen then 3034 | mode = "no" 3035 | else 3036 | mode = "yes" 3037 | end 3038 | end 3039 | 3040 | if mode == "yes" then 3041 | user_opts.idlescreen = true 3042 | else 3043 | user_opts.idlescreen = false 3044 | end 3045 | 3046 | if user_opts.userdataAvail then 3047 | mp.set_property_native("user-data/osc/idlescreen", mode) 3048 | else 3049 | utils.shared_script_property_set("osc-idlescreen", mode) 3050 | end 3051 | 3052 | if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then 3053 | mp.osd_message("OSC logo visibility: " .. tostring(mode)) 3054 | end 3055 | 3056 | request_tick() 3057 | end 3058 | 3059 | visibility_mode(user_opts.visibility, true) 3060 | mp.register_script_message("osc-visibility", visibility_mode) 3061 | mp.register_script_message("osc-show", show_osc) 3062 | mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end) 3063 | 3064 | mp.register_script_message("osc-idlescreen", idlescreen_visibility) 3065 | 3066 | mp.register_script_message("thumbfast-info", function(json) 3067 | local data = utils.parse_json(json) 3068 | if type(data) ~= "table" or not data.width or not data.height then 3069 | msg.error("thumbfast-info: received json didn't produce a table with thumbnail information") 3070 | else 3071 | thumbfast = data 3072 | mp.command_native({"script-message", message.osc.finish, format_json(osc_reg)}) 3073 | end 3074 | end) 3075 | 3076 | set_virt_mouse_area(0, 0, 0, 0, "input") 3077 | set_virt_mouse_area(0, 0, 0, 0, "window-controls") 3078 | --------------------------------------------------------------------------------