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