1-- Compatibility shim for lua 5.2/5.3 2unpack = unpack or table.unpack 3 4-- these are used internally by lua.c 5mp.UNKNOWN_TYPE.info = "this value is inserted if the C type is not supported" 6mp.UNKNOWN_TYPE.type = "UNKNOWN_TYPE" 7 8mp.ARRAY.info = "native array" 9mp.ARRAY.type = "ARRAY" 10 11mp.MAP.info = "native map" 12mp.MAP.type = "MAP" 13 14function mp.get_script_name() 15 return mp.script_name 16end 17 18function mp.get_opt(key, def) 19 local opts = mp.get_property_native("options/script-opts") 20 local val = opts[key] 21 if val == nil then 22 val = def 23 end 24 return val 25end 26 27function mp.input_define_section(section, contents, flags) 28 if flags == nil or flags == "" then 29 flags = "default" 30 end 31 mp.commandv("define-section", section, contents, flags) 32end 33 34function mp.input_enable_section(section, flags) 35 if flags == nil then 36 flags = "" 37 end 38 mp.commandv("enable-section", section, flags) 39end 40 41function mp.input_disable_section(section) 42 mp.commandv("disable-section", section) 43end 44 45function mp.get_mouse_pos() 46 local m = mp.get_property_native("mouse-pos") 47 return m.x, m.y 48end 49 50-- For dispatching script-binding. This is sent as: 51-- script-message-to $script_name $binding_name $keystate 52-- The array is indexed by $binding_name, and has functions like this as value: 53-- fn($binding_name, $keystate) 54local dispatch_key_bindings = {} 55 56local message_id = 0 57local function reserve_binding() 58 message_id = message_id + 1 59 return "__keybinding" .. tostring(message_id) 60end 61 62local function dispatch_key_binding(name, state, key_name, key_text) 63 local fn = dispatch_key_bindings[name] 64 if fn then 65 fn(name, state, key_name, key_text) 66 end 67end 68 69-- "Old", deprecated API 70 71-- each script has its own section, so that they don't conflict 72local default_section = "input_dispatch_" .. mp.script_name 73 74-- Set the list of key bindings. These will override the user's bindings, so 75-- you should use this sparingly. 76-- A call to this function will remove all bindings previously set with this 77-- function. For example, set_key_bindings({}) would remove all script defined 78-- key bindings. 79-- Note: the bindings are not active by default. Use enable_key_bindings(). 80-- 81-- list is an array of key bindings, where each entry is an array as follow: 82-- {key, callback_press, callback_down, callback_up} 83-- key is the key string as used in input.conf, like "ctrl+a" 84-- 85-- callback can be a string too, in which case the following will be added like 86-- an input.conf line: key .. " " .. callback 87-- (And callback_down is ignored.) 88function mp.set_key_bindings(list, section, flags) 89 local cfg = "" 90 for i = 1, #list do 91 local entry = list[i] 92 local key = entry[1] 93 local cb = entry[2] 94 local cb_down = entry[3] 95 local cb_up = entry[4] 96 if type(cb) ~= "string" then 97 local mangle = reserve_binding() 98 dispatch_key_bindings[mangle] = function(name, state) 99 local event = state:sub(1, 1) 100 local is_mouse = state:sub(2, 2) == "m" 101 local def = (is_mouse and "u") or "d" 102 if event == "r" then 103 return 104 end 105 if event == "p" and cb then 106 cb() 107 elseif event == "d" and cb_down then 108 cb_down() 109 elseif event == "u" and cb_up then 110 cb_up() 111 elseif event == def and cb then 112 cb() 113 end 114 end 115 cfg = cfg .. key .. " script-binding " .. 116 mp.script_name .. "/" .. mangle .. "\n" 117 else 118 cfg = cfg .. key .. " " .. cb .. "\n" 119 end 120 end 121 mp.input_define_section(section or default_section, cfg, flags) 122end 123 124function mp.enable_key_bindings(section, flags) 125 mp.input_enable_section(section or default_section, flags) 126end 127 128function mp.disable_key_bindings(section) 129 mp.input_disable_section(section or default_section) 130end 131 132function mp.set_mouse_area(x0, y0, x1, y1, section) 133 mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1) 134end 135 136-- "Newer" and more convenient API 137 138local key_bindings = {} 139local key_binding_counter = 0 140local key_bindings_dirty = false 141 142function mp.flush_keybindings() 143 if not key_bindings_dirty then 144 return 145 end 146 key_bindings_dirty = false 147 148 for i = 1, 2 do 149 local section, flags 150 local def = i == 1 151 if def then 152 section = "input_" .. mp.script_name 153 flags = "default" 154 else 155 section = "input_forced_" .. mp.script_name 156 flags = "force" 157 end 158 local bindings = {} 159 for k, v in pairs(key_bindings) do 160 if v.bind and v.forced ~= def then 161 bindings[#bindings + 1] = v 162 end 163 end 164 table.sort(bindings, function(a, b) 165 return a.priority < b.priority 166 end) 167 local cfg = "" 168 for _, v in ipairs(bindings) do 169 cfg = cfg .. v.bind .. "\n" 170 end 171 mp.input_define_section(section, cfg, flags) 172 -- TODO: remove the section if the script is stopped 173 mp.input_enable_section(section, "allow-hide-cursor+allow-vo-dragging") 174 end 175end 176 177local function add_binding(attrs, key, name, fn, rp) 178 if (type(name) ~= "string") and (name ~= nil) then 179 rp = fn 180 fn = name 181 name = nil 182 end 183 rp = rp or "" 184 if name == nil then 185 name = reserve_binding() 186 end 187 local repeatable = rp == "repeatable" or rp["repeatable"] 188 if rp["forced"] then 189 attrs.forced = true 190 end 191 local key_cb, msg_cb 192 if not fn then 193 fn = function() end 194 end 195 if rp["complex"] then 196 local key_states = { 197 ["u"] = "up", 198 ["d"] = "down", 199 ["r"] = "repeat", 200 ["p"] = "press", 201 } 202 key_cb = function(name, state, key_name, key_text) 203 if key_text == "" then 204 key_text = nil 205 end 206 fn({ 207 event = key_states[state:sub(1, 1)] or "unknown", 208 is_mouse = state:sub(2, 2) == "m", 209 key_name = key_name, 210 key_text = key_text, 211 }) 212 end 213 msg_cb = function() 214 fn({event = "press", is_mouse = false}) 215 end 216 else 217 key_cb = function(name, state) 218 -- Emulate the same semantics as input.c uses for most bindings: 219 -- For keyboard, "down" runs the command, "up" does nothing; 220 -- for mouse, "down" does nothing, "up" runs the command. 221 -- Also, key repeat triggers the binding again. 222 local event = state:sub(1, 1) 223 local is_mouse = state:sub(2, 2) == "m" 224 if event == "r" and not repeatable then 225 return 226 end 227 if is_mouse and (event == "u" or event == "p") then 228 fn() 229 elseif (not is_mouse) and (event == "d" or event == "r" or event == "p") then 230 fn() 231 end 232 end 233 msg_cb = fn 234 end 235 if key and #key > 0 then 236 attrs.bind = key .. " script-binding " .. mp.script_name .. "/" .. name 237 end 238 attrs.name = name 239 -- new bindings override old ones (but do not overwrite them) 240 key_binding_counter = key_binding_counter + 1 241 attrs.priority = key_binding_counter 242 key_bindings[name] = attrs 243 key_bindings_dirty = true 244 dispatch_key_bindings[name] = key_cb 245 mp.register_script_message(name, msg_cb) 246end 247 248function mp.add_key_binding(...) 249 add_binding({forced=false}, ...) 250end 251 252function mp.add_forced_key_binding(...) 253 add_binding({forced=true}, ...) 254end 255 256function mp.remove_key_binding(name) 257 key_bindings[name] = nil 258 dispatch_key_bindings[name] = nil 259 key_bindings_dirty = true 260 mp.unregister_script_message(name) 261end 262 263local timers = {} 264 265local timer_mt = {} 266timer_mt.__index = timer_mt 267 268function mp.add_timeout(seconds, cb) 269 local t = mp.add_periodic_timer(seconds, cb) 270 t.oneshot = true 271 return t 272end 273 274function mp.add_periodic_timer(seconds, cb) 275 local t = { 276 timeout = seconds, 277 cb = cb, 278 oneshot = false, 279 } 280 setmetatable(t, timer_mt) 281 t:resume() 282 return t 283end 284 285function timer_mt.stop(t) 286 if timers[t] then 287 timers[t] = nil 288 t.next_deadline = t.next_deadline - mp.get_time() 289 end 290end 291 292function timer_mt.kill(t) 293 timers[t] = nil 294 t.next_deadline = nil 295end 296mp.cancel_timer = timer_mt.kill 297 298function timer_mt.resume(t) 299 if not timers[t] then 300 local timeout = t.next_deadline 301 if timeout == nil then 302 timeout = t.timeout 303 end 304 t.next_deadline = mp.get_time() + timeout 305 timers[t] = t 306 end 307end 308 309function timer_mt.is_enabled(t) 310 return timers[t] ~= nil 311end 312 313-- Return the timer that expires next. 314local function get_next_timer() 315 local best = nil 316 for t, _ in pairs(timers) do 317 if (best == nil) or (t.next_deadline < best.next_deadline) then 318 best = t 319 end 320 end 321 return best 322end 323 324function mp.get_next_timeout() 325 local timer = get_next_timer() 326 if not timer then 327 return 328 end 329 local now = mp.get_time() 330 return timer.next_deadline - now 331end 332 333-- Run timers that have met their deadline at the time of invocation. 334-- Return: time>0 in seconds till the next due timer, 0 if there are due timers 335-- (aborted to avoid infinite loop), or nil if no timers 336local function process_timers() 337 local t0 = nil 338 while true do 339 local timer = get_next_timer() 340 if not timer then 341 return 342 end 343 local now = mp.get_time() 344 local wait = timer.next_deadline - now 345 if wait > 0 then 346 return wait 347 else 348 if not t0 then 349 t0 = now -- first due callback: always executes, remember t0 350 elseif timer.next_deadline > t0 then 351 -- don't block forever with slow callbacks and endless timers. 352 -- we'll continue right after checking mpv events. 353 return 0 354 end 355 356 if timer.oneshot then 357 timer:kill() 358 else 359 timer.next_deadline = now + timer.timeout 360 end 361 timer.cb() 362 end 363 end 364end 365 366local messages = {} 367 368function mp.register_script_message(name, fn) 369 messages[name] = fn 370end 371 372function mp.unregister_script_message(name) 373 messages[name] = nil 374end 375 376local function message_dispatch(ev) 377 if #ev.args > 0 then 378 local handler = messages[ev.args[1]] 379 if handler then 380 handler(unpack(ev.args, 2)) 381 end 382 end 383end 384 385local property_id = 0 386local properties = {} 387 388function mp.observe_property(name, t, cb) 389 local id = property_id + 1 390 property_id = id 391 properties[id] = cb 392 mp.raw_observe_property(id, name, t) 393end 394 395function mp.unobserve_property(cb) 396 for prop_id, prop_cb in pairs(properties) do 397 if cb == prop_cb then 398 properties[prop_id] = nil 399 mp.raw_unobserve_property(prop_id) 400 end 401 end 402end 403 404local function property_change(ev) 405 local prop = properties[ev.id] 406 if prop then 407 prop(ev.name, ev.data) 408 end 409end 410 411-- used by default event loop (mp_event_loop()) to decide when to quit 412mp.keep_running = true 413 414local event_handlers = {} 415 416function mp.register_event(name, cb) 417 local list = event_handlers[name] 418 if not list then 419 list = {} 420 event_handlers[name] = list 421 end 422 list[#list + 1] = cb 423 return mp.request_event(name, true) 424end 425 426function mp.unregister_event(cb) 427 for name, sub in pairs(event_handlers) do 428 local found = false 429 for i, e in ipairs(sub) do 430 if e == cb then 431 found = true 432 break 433 end 434 end 435 if found then 436 -- create a new array, just in case this function was called 437 -- from an event handler 438 local new = {} 439 for i = 1, #sub do 440 if sub[i] ~= cb then 441 new[#new + 1] = sub[i] 442 end 443 end 444 event_handlers[name] = new 445 if #new == 0 then 446 mp.request_event(name, false) 447 end 448 end 449 end 450end 451 452-- default handlers 453mp.register_event("shutdown", function() mp.keep_running = false end) 454mp.register_event("client-message", message_dispatch) 455mp.register_event("property-change", property_change) 456 457-- called before the event loop goes back to sleep 458local idle_handlers = {} 459 460function mp.register_idle(cb) 461 idle_handlers[#idle_handlers + 1] = cb 462end 463 464function mp.unregister_idle(cb) 465 local new = {} 466 for _, handler in ipairs(idle_handlers) do 467 if handler ~= cb then 468 new[#new + 1] = handler 469 end 470 end 471 idle_handlers = new 472end 473 474-- sent by "script-binding" 475mp.register_script_message("key-binding", dispatch_key_binding) 476 477mp.msg = { 478 log = mp.log, 479 fatal = function(...) return mp.log("fatal", ...) end, 480 error = function(...) return mp.log("error", ...) end, 481 warn = function(...) return mp.log("warn", ...) end, 482 info = function(...) return mp.log("info", ...) end, 483 verbose = function(...) return mp.log("v", ...) end, 484 debug = function(...) return mp.log("debug", ...) end, 485 trace = function(...) return mp.log("trace", ...) end, 486} 487 488_G.print = mp.msg.info 489 490package.loaded["mp"] = mp 491package.loaded["mp.msg"] = mp.msg 492 493function mp.wait_event(t) 494 local r = mp.raw_wait_event(t) 495 if r and r.file_error and not r.error then 496 -- compat; deprecated 497 r.error = r.file_error 498 end 499 return r 500end 501 502_G.mp_event_loop = function() 503 mp.dispatch_events(true) 504end 505 506local function call_event_handlers(e) 507 local handlers = event_handlers[e.event] 508 if handlers then 509 for _, handler in ipairs(handlers) do 510 handler(e) 511 end 512 end 513end 514 515mp.use_suspend = false 516 517local suspend_warned = false 518 519function mp.dispatch_events(allow_wait) 520 local more_events = true 521 if mp.use_suspend then 522 if not suspend_warned then 523 mp.msg.error("mp.use_suspend is now ignored.") 524 suspend_warned = true 525 end 526 end 527 while mp.keep_running do 528 local wait = 0 529 if not more_events then 530 wait = process_timers() or 1e20 -- infinity for all practical purposes 531 if wait ~= 0 then 532 local idle_called = nil 533 for _, handler in ipairs(idle_handlers) do 534 idle_called = true 535 handler() 536 end 537 if idle_called then 538 -- handlers don't complete in 0 time, and may modify timers 539 wait = mp.get_next_timeout() or 1e20 540 if wait < 0 then 541 wait = 0 542 end 543 end 544 end 545 -- Resume playloop - important especially if an error happened while 546 -- suspended, and the error was handled, but no resume was done. 547 mp.resume_all() 548 if allow_wait ~= true then 549 return 550 end 551 end 552 local e = mp.wait_event(wait) 553 more_events = false 554 if e.event ~= "none" then 555 call_event_handlers(e) 556 more_events = true 557 end 558 end 559end 560 561mp.register_idle(mp.flush_keybindings) 562 563-- additional helpers 564 565function mp.osd_message(text, duration) 566 if not duration then 567 duration = "-1" 568 else 569 duration = tostring(math.floor(duration * 1000)) 570 end 571 mp.commandv("show-text", text, duration) 572end 573 574local hook_table = {} 575 576local hook_mt = {} 577hook_mt.__index = hook_mt 578 579function hook_mt.cont(t) 580 if t._id == nil then 581 mp.msg.error("hook already continued") 582 else 583 mp.raw_hook_continue(t._id) 584 t._id = nil 585 end 586end 587 588function hook_mt.defer(t) 589 t._defer = true 590end 591 592mp.register_event("hook", function(ev) 593 local fn = hook_table[tonumber(ev.id)] 594 local hookobj = { 595 _id = ev.hook_id, 596 _defer = false, 597 } 598 setmetatable(hookobj, hook_mt) 599 if fn then 600 fn(hookobj) 601 end 602 if (not hookobj._defer) and hookobj._id ~= nil then 603 hookobj:cont() 604 end 605end) 606 607function mp.add_hook(name, pri, cb) 608 local id = #hook_table + 1 609 hook_table[id] = cb 610 -- The C API suggests using 0 for a neutral priority, but lua.rst suggests 611 -- 50 (?), so whatever. 612 mp.raw_hook_add(id, name, pri - 50) 613end 614 615local async_call_table = {} 616local async_next_id = 1 617 618function mp.command_native_async(node, cb) 619 local id = async_next_id 620 async_next_id = async_next_id + 1 621 local res, err = mp.raw_command_native_async(id, node) 622 if not res then 623 cb(false, nil, err) 624 return res, err 625 end 626 local t = {cb = cb, id = id} 627 async_call_table[id] = t 628 return t 629end 630 631mp.register_event("command-reply", function(ev) 632 local id = tonumber(ev.id) 633 local t = async_call_table[id] 634 local cb = t.cb 635 t.id = nil 636 async_call_table[id] = nil 637 if ev.error then 638 cb(false, nil, ev.error) 639 else 640 cb(true, ev.result, nil) 641 end 642end) 643 644function mp.abort_async_command(t) 645 if t.id ~= nil then 646 mp.raw_abort_async_command(t.id) 647 end 648end 649 650local overlay_mt = {} 651overlay_mt.__index = overlay_mt 652local overlay_new_id = 0 653 654function mp.create_osd_overlay(format) 655 overlay_new_id = overlay_new_id + 1 656 local overlay = { 657 format = format, 658 id = overlay_new_id, 659 data = "", 660 res_x = 0, 661 res_y = 720, 662 } 663 setmetatable(overlay, overlay_mt) 664 return overlay 665end 666 667function overlay_mt.update(ov) 668 local cmd = {} 669 for k, v in pairs(ov) do 670 cmd[k] = v 671 end 672 cmd.name = "osd-overlay" 673 cmd.res_x = math.floor(cmd.res_x) 674 cmd.res_y = math.floor(cmd.res_y) 675 return mp.command_native(cmd) 676end 677 678function overlay_mt.remove(ov) 679 mp.command_native { 680 name = "osd-overlay", 681 id = ov.id, 682 format = "none", 683 data = "", 684 } 685end 686 687-- legacy API 688function mp.set_osd_ass(res_x, res_y, data) 689 if not mp._legacy_overlay then 690 mp._legacy_overlay = mp.create_osd_overlay("ass-events") 691 end 692 if mp._legacy_overlay.res_x ~= res_x or 693 mp._legacy_overlay.res_y ~= res_y or 694 mp._legacy_overlay.data ~= data 695 then 696 mp._legacy_overlay.res_x = res_x 697 mp._legacy_overlay.res_y = res_y 698 mp._legacy_overlay.data = data 699 mp._legacy_overlay:update() 700 end 701end 702 703function mp.get_osd_size() 704 local prop = mp.get_property_native("osd-dimensions") 705 return prop.w, prop.h, prop.aspect 706end 707 708function mp.get_osd_margins() 709 local prop = mp.get_property_native("osd-dimensions") 710 return prop.ml, prop.mt, prop.mr, prop.mb 711end 712 713local mp_utils = package.loaded["mp.utils"] 714 715function mp_utils.format_table(t, set) 716 if not set then 717 set = { [t] = true } 718 end 719 local res = "{" 720 -- pretty expensive but simple way to distinguish array and map parts of t 721 local keys = {} 722 local vals = {} 723 local arr = 0 724 for i = 1, #t do 725 if t[i] == nil then 726 break 727 end 728 keys[i] = i 729 vals[i] = t[i] 730 arr = i 731 end 732 for k, v in pairs(t) do 733 if not (type(k) == "number" and k >= 1 and k <= arr and keys[k]) then 734 keys[#keys + 1] = k 735 vals[#keys] = v 736 end 737 end 738 for i = 1, #keys do 739 if #res > 1 then 740 res = res .. ", " 741 end 742 if i > arr then 743 res = res .. mp_utils.to_string(keys[i], set) .. " = " 744 end 745 res = res .. mp_utils.to_string(vals[i], set) 746 end 747 res = res .. "}" 748 return res 749end 750 751function mp_utils.to_string(v, set) 752 if type(v) == "string" then 753 return "\"" .. v .. "\"" 754 elseif type(v) == "table" then 755 if set then 756 if set[v] then 757 return "[cycle]" 758 end 759 set[v] = true 760 end 761 return mp_utils.format_table(v, set) 762 else 763 return tostring(v) 764 end 765end 766 767function mp_utils.getcwd() 768 return mp.get_property("working-directory") 769end 770 771function mp_utils.getpid() 772 return mp.get_property_number("pid") 773end 774 775function mp_utils.format_bytes_humanized(b) 776 local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"} 777 local i = 1 778 while b >= 1024 do 779 b = b / 1024 780 i = i + 1 781 end 782 return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1)) 783end 784 785function mp_utils.subprocess(t) 786 local cmd = {} 787 cmd.name = "subprocess" 788 cmd.capture_stdout = true 789 for k, v in pairs(t) do 790 if k == "cancellable" then 791 k = "playback_only" 792 elseif k == "max_size" then 793 k = "capture_size" 794 end 795 cmd[k] = v 796 end 797 local res, err = mp.command_native(cmd) 798 if res == nil then 799 -- an error usually happens only if parsing failed (or no args passed) 800 res = {error_string = err, status = -1} 801 end 802 if res.error_string ~= "" then 803 res.error = res.error_string 804 end 805 return res 806end 807 808function mp_utils.subprocess_detached(t) 809 mp.commandv("run", unpack(t.args)) 810end 811 812function mp_utils.shared_script_property_set(name, value) 813 if value ~= nil then 814 -- no such thing as change-list with mpv_node, so build a string value 815 mp.commandv("change-list", "shared-script-properties", "append", 816 name .. "=" .. value) 817 else 818 mp.commandv("change-list", "shared-script-properties", "remove", name) 819 end 820end 821 822function mp_utils.shared_script_property_get(name) 823 local map = mp.get_property_native("shared-script-properties") 824 return map and map[name] 825end 826 827-- cb(name, value) on change and on init 828function mp_utils.shared_script_property_observe(name, cb) 829 -- it's _very_ wasteful to observe the mpv core "super" property for every 830 -- shared sub-property, but then again you shouldn't use this 831 mp.observe_property("shared-script-properties", "native", function(_, val) 832 cb(name, val and val[name]) 833 end) 834end 835 836return {} 837