-
Notifications
You must be signed in to change notification settings - Fork 0
/
hook.lua
275 lines (241 loc) · 10.5 KB
/
hook.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
-- https://github.com/qwerty12/mpv-taskbar-buttons
--[[
Copyright (C) 2022 Faheem Pervez
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
https://www.gnu.org/licenses/gpl-2.0.html
--]]
-- can't use mp.get_script_directory() because this is loaded through load-script, which lacks context
local script_dir = debug.getinfo(1, "S").source:sub(2):match("(.*/)") -- https://stackoverflow.com/a/23535333
package.path = script_dir .. "\\?.lua;" .. package.path
local ffi = require("ffi")
local common = require("common")
local C = ffi.C
local options = nil
local callbacks = {
[common.button_ids[C.BUTTON_PREV]] = function() mp.command(options.prev_command == "" and "playlist-prev" or options.prev_command) end,
[common.button_ids[C.BUTTON_PLAY_PAUSE]] = function()
if options.play_pause_command == "" then
mp.commandv("cycle", "pause")
else
mp.command(options.play_pause_command)
end
end,
[common.button_ids[C.BUTTON_NEXT]] = function() mp.command(options.next_command == "" and "playlist-next" or options.next_command) end
}
ffi.cdef [[
// From lua.c @ e686297ecf3928b768c674bb10faa6f352b999b8
struct script_ctx {
const char *name;
const char *filename;
const char *path;
void *lua_State;
void *mp_log;
void *mpv_handle_client;
void *MPContext;
size_t lua_malloc_size;
uintptr_t lua_allocf;
void *lua_alloc_ud;
void *stats_ctx;
};
typedef void (*mpv_set_wakeup_callback) (void *mpv_handle_ctx, void (*cb)(void *q12), const void *d);
bool __stdcall CloseHandle(const void *hObject);
void* __stdcall GetModuleHandleA(const char *lpModuleName);
void* __stdcall GetProcAddress(void *hModule, const char *lpProcName);
void* __stdcall CreateEventW(void *lpEventAttributes, bool bManualReset, bool bInitialState, const wchar_t *lpName);
unsigned long __stdcall WaitForMultipleObjects(unsigned long nCount, const void **lpHandles, bool bWaitAll, unsigned long dwMilliseconds);
void* __stdcall GlobalAlloc(unsigned int uFlags, size_t dwBytes);
void* __stdcall GlobalFree(void *hMem);
bool __stdcall VirtualProtect(void *lpAddress, size_t dwSize, unsigned long flNewProtect, unsigned long *lpflOldProtect);
typedef __int64 (__stdcall *HOOKPROC)(int code, unsigned __int64 wParam, __int64 lParam);
void* __stdcall SetWindowsHookExW(int idHook, HOOKPROC lpfn, void *hmod, unsigned long dwThreadId);
bool __stdcall UnhookWindowsHookEx(void *hhk);
bool __stdcall ChangeWindowMessageFilterEx(void *hwnd, unsigned int message, unsigned long action, void *pChangeFilterStruct);
]]
local WH_GETMESSAGE = 3
local WM_COMMAND = 0x0111
local MSGFLT_ADD = 1
local INFINITE, WAIT_OBJECT_0 = 0xFFFFFFFF, 0x00000000
local GPTR = 0x0040
local PAGE_EXECUTE_READWRITE = 0x40
local SetEvent = C.GetProcAddress(C.GetModuleHandleA("kernel32.dll"), "SetEvent")
local mpv_set_wakeup_callback = ffi.cast("mpv_set_wakeup_callback", C.GetProcAddress(C.GetModuleHandleA(nil), "mpv_set_wakeup_callback"))
local last_button_hit = ffi.cast("int*", C.GlobalAlloc(GPTR, ffi.sizeof("int"))) -- not actually thread safe...
local script_ctx = ffi.cast("struct script_ctx*", debug.getregistry()["ctx"]) -- hell yeah, Lua(JIT)
local nEventCount = 2
local hEvents = ffi.cast("const void**", C.GlobalAlloc(GPTR, ffi.sizeof("void*[?]", nEventCount)))
local mpv_hwnd, mpv_tid
local lpCompiledCallback, lpGetMsgProc
local hHook = nil
local function generate_hook_callback()
-- Taken from https://github.com/nucular/tcclua
ffi.cdef [[
typedef struct TCCState TCCState;
TCCState *tcc_new(void);
void tcc_delete(TCCState *s);
int tcc_set_options(TCCState *s, const char *str);
int tcc_compile_string(TCCState *s, const char *buf);
int tcc_set_output_type(TCCState *s, int output_type);
int tcc_relocate(TCCState *s1, void *ptr);
void *tcc_get_symbol(TCCState *s, const char *name);
]]
local tcc = ffi.load(options.tcc_dll_path)
assert(tcc)
local state = tcc.tcc_new()
assert(state)
tcc.tcc_set_output_type(state, 1) -- TCC_OUTPUT_MEMORY
tcc.tcc_set_options(state, "-nostdinc -nostdlib")
local hook = [[
void _start(){}
#define NULL ((void *)0)
#define __int64 long long
#define __stdcall __attribute__((__stdcall__))
typedef struct tagMSG {
void *hwnd;
unsigned int message;
unsigned __int64 wParam;
__int64 lParam;
unsigned long time;
long pt[2];
} MSG, *LPMSG;
typedef __int64 (__stdcall *CALLNEXTHOOKEX)(void*, int, unsigned __int64, __int64);
typedef int (__stdcall *SETEVENT)(void*);
__int64 __stdcall GetMsgProc(int code, unsigned __int64 wParam, __int64 lParam)
{
volatile const CALLNEXTHOOKEX CallNextHookEx = (CALLNEXTHOOKEX)#CallNextHookEx#;
if (code < 0 || wParam != 0x0001) // HC_ACTION, PM_REMOVE; remove PM_REMOVE comparison if all button presses aren't being caught
goto cont;
const LPMSG msg = (LPMSG)lParam;
if (msg && msg->message == #WM_COMMAND# && msg->hwnd == (void*)#mpv_hwnd#) {
int const wmId = ((unsigned short)(((unsigned __int64)(msg->wParam)) & 0xffff)); // LOWORD
if (wmId < #BUTTON_FIRST# || wmId > #BUTTON_LAST#)
goto cont;
int volatile *const last_button_hit = (int*)#last_button_hit#;
void* volatile const hCommandReceivedEvent = (void*)#hCommandReceivedEvent#;
volatile const SETEVENT SetEvent = (SETEVENT)#SetEvent#;
*last_button_hit = wmId;
SetEvent(hCommandReceivedEvent);
msg->message = 0x0000; // WM_NULL
return 0;
}
cont:
return CallNextHookEx(NULL, code, wParam, lParam);
}
]]
for name, value in pairs({
["mpv_hwnd"] = mpv_hwnd,
["WM_COMMAND"] = WM_COMMAND,
["BUTTON_FIRST"] = common.button_ids[C.BUTTON_FIRST],
["BUTTON_LAST"] = common.button_ids[C.BUTTON_LAST - 1],
["last_button_hit"] = last_button_hit,
["hCommandReceivedEvent"] = hEvents[1],
["SetEvent"] = SetEvent,
["CallNextHookEx"] = C.GetProcAddress(C.GetModuleHandleA("user32.dll"), "CallNextHookEx")
}) do
value = type(value) == "cdata" and tostring(value):match("^cdata<.+>: (0x.+)") or tostring(value)
hook = hook:gsub("#" .. name .. "#", value)
end
assert(tcc.tcc_compile_string(state, hook) == 0)
local size = tcc.tcc_relocate(state, nil)
assert(size > 0)
-- a buffer allocated with ffi.new here eventually causes a crash
local lpCompiled = C.GlobalAlloc(GPTR, size)
assert(lpCompiled)
assert(C.VirtualProtect(lpCompiled, size, PAGE_EXECUTE_READWRITE, ffi.new("int[1]")))
assert(tcc.tcc_relocate(state, lpCompiled) == 0)
local lpGetMsgProc = tcc.tcc_get_symbol(state, "_GetMsgProc@" .. tostring(3 * ffi.sizeof("void*")))
assert(lpGetMsgProc)
tcc.tcc_delete(state)
tcc = nil
return lpCompiled, lpGetMsgProc
end
for i = 0, nEventCount - 1 do
hEvents[i] = C.CreateEventW(nil, false, i == 0, nil)
end
mpv_set_wakeup_callback(script_ctx.mpv_handle_client, SetEvent, hEvents[0])
local function start()
mp.unregister_idle(start)
mpv_hwnd, mpv_tid = common.get_mpv_hwnd()
if mpv_tid == 0 then
return
end
options = common.read_options()
lpCompiledCallback, lpGetMsgProc = generate_hook_callback()
hHook = C.SetWindowsHookExW(WH_GETMESSAGE, lpGetMsgProc, nil, mpv_tid)
if hHook then
-- allow unelevated Explorer to post WM_COMMAND to mpv running as adminstrator
C.ChangeWindowMessageFilterEx(mpv_hwnd, WM_COMMAND, MSGFLT_ADD, nil)
end
collectgarbage("collect") -- force TCC unload
end
mp.register_idle(start)
_G.mp_event_loop = function()
while mp.keep_running do
local dwStart, dwElapsed, dwTimeout = 0, 0, mp.get_next_timeout()
if dwTimeout == nil then
dwTimeout = INFINITE
else
dwStart = mp.get_time() * 1000
dwTimeout = dwTimeout * 1000
--dwElapsed = (mp.get_time() * 1000) - dwStart
end
while mp.keep_running do
local dwStatus = C.WaitForMultipleObjects(nEventCount, hEvents, false, dwTimeout - dwElapsed)
if dwStatus == WAIT_OBJECT_0 then --hMpvWakeupEvent signalled
mp.dispatch_events(false)
-- break? new timers might have been introduced
elseif dwStatus == WAIT_OBJECT_0 + 1 then -- hCommandReceivedEvent signalled
local wmId = last_button_hit[0]
if callbacks[wmId] then
callbacks[wmId]()
-- if your modified callbacks introduce timers, break here
end
end
if dwTimeout == INFINITE then
break
end
dwElapsed = (mp.get_time() * 1000) - dwStart
if dwElapsed < dwTimeout then
-- re-call mp.get_next_timeout() here and break if less than dwTimeout?
-- continue
else -- timed out
mp.dispatch_events(false)
break
end
end
end
-- shutdown
if hHook ~= nil then
C.UnhookWindowsHookEx(hHook)
hHook = nil
end
if lpCompiledCallback ~= nil then
lpGetMsgProc = nil
C.GlobalFree(lpCompiledCallback)
lpCompiledCallback = nil
end
mpv_set_wakeup_callback(script_ctx.mpv_handle_client, nil, nil)
if hEvents ~= nil then
for i = 0, nEventCount - 1 do
if hEvents[i] ~= nil then
C.CloseHandle(hEvents[i])
hEvents[i] = nil
end
end
C.GlobalFree(hEvents)
hEvents = nil
end
if last_button_hit ~= nil then
C.GlobalFree(last_button_hit)
last_button_hit = nil
end
end