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