Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.20)
# macOS: the oldest release the library loads on. Without it the build machine's SDK version is used.
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum macOS version")
project(ModAVDExtras VERSION 0.1.3 LANGUAGES C)
project(ModAVDExtras VERSION 0.2.0 LANGUAGES C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
Expand All @@ -28,13 +28,21 @@ if(NOT WIN32)
endif()
endif()

set(MX_SOURCES src/core.c src/shm.c src/console.c src/log.c)
set(MX_SOURCES src/core.c src/shm.c src/console.c src/console_command.c src/input.c src/log.c)

if(NOT WIN32)
# POSIX: the per-handle input lock.
find_package(Threads REQUIRED)
endif()

function(mx_link_system target)
if(WIN32)
target_link_libraries(${target} PRIVATE ws2_32)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(${target} PRIVATE rt)
else()
target_link_libraries(${target} PRIVATE Threads::Threads)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(${target} PRIVATE rt)
endif()
endif()
endfunction()

Expand All @@ -57,6 +65,11 @@ target_link_libraries(test_core PRIVATE mx_core)
mx_link_system(test_core)
add_test(NAME test_core COMMAND test_core)

add_executable(test_input tests/test_input.c)
target_link_libraries(test_input PRIVATE mx_core)
mx_link_system(test_input)
add_test(NAME test_input COMMAND test_input)

if(NOT WIN32)
add_executable(test_open_posix tests/test_open_posix.c)
target_link_libraries(test_open_posix PRIVATE mx_core)
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

## 这是什么

一个 C 库:从 [ModAVD](https://github.com/ModAVD/ModAVD) 实例直接取画面,不经 adb。给 MAA(MaaCore)、MaaFramework 这类自动化框架接入用。以后还会加宿主侧的点击和按键。
一个 C 库:从 [ModAVD](https://github.com/ModAVD/ModAVD) 实例直接取画面、在宿主这边点击,不经 adb。给 MAA(MaaCore)、MaaFramework 这类自动化框架接入用。

- 画面来自模拟器自己的共享内存:`screenrecord webrtc start`(或启动参数 `-share-vid`)之后,模拟器把每一帧写进一块按控制台端口命名的共享内存。
- Windows 上叫 `SHM_videmulator<端口>`,Linux、macOS 上叫 `videmulator<端口>`;
Expand All @@ -21,12 +21,13 @@
- 拷出一帧,换成调用方要的格式;
- 遇到新帧写到一半时重拷;
- 告诉调用方现在能不能用。
- 点击走模拟器控制台的 `event send`,发标准的多点触控事件(槽位、跟踪编号、坐标、压力),到达模拟器自己的虚拟触屏:安卓看到的是屏幕上的手指,和在模拟器窗口里点一样,不是应用注入的事件,客体里也不用装东西。最多 10 根手指。
- 库**不自己退回 adb**:出错时返回错误码,让调用方按自己的规则换别的办法。
- 只依赖系统库:Windows 上是 `KERNEL32`、`WS2_32`,C 运行库静态链接;POSIX 上是 `shm_open`、`mmap` 和套接字。

## 状态

- v0.1:只有截图。点击、按键、文字的函数已经声明,现在返回 `MODAVD_E_UNSUPPORTED`。
- 截图(接口版本 1)和点击(接口版本 2,`MODAVD_CAP_TOUCH`)。按键、文字的函数已经声明,现在返回 `MODAVD_E_UNSUPPORTED`:控制台发的按键会进触屏设备、被丢掉,`event text` 会悄悄丢掉键盘映射里没有的字。
- Windows:截图这套逻辑在 ModAVD 上已经用了一个多月,形式是下面的兼容外壳。
- macOS(Apple Silicon):11-09 有贡献者用官方 Emulator 37.2.13、API 35 ARM64 实测过截图。
- Linux:能编,单元测试在 CI 上跑;还没有在模拟器上实测。
Expand Down Expand Up @@ -106,11 +107,12 @@ ctest --test-dir build --output-on-failure

## English

ModAVDExtras is a small C library that reads the screen of a [ModAVD](https://github.com/ModAVD/ModAVD) instance directly, without adb, for automation frameworks such as MaaCore (MAA) and MaaFramework. Host-side touch and key input will follow.
ModAVDExtras is a small C library that reads the screen of a [ModAVD](https://github.com/ModAVD/ModAVD) instance directly, without adb, for automation frameworks such as MaaCore (MAA) and MaaFramework, and touches it from the host.

- Frames come from the emulator's own shared memory: after `screenrecord webrtc start` (or `-share-vid`), the emulator writes every frame to a region named after its console port: `SHM_videmulator<port>` on Windows, `videmulator<port>` elsewhere. The layout is a 24-byte header (width, height, fps, frame number, timestamp) followed by BGRA pixels.
- The library finds the console port from an adb address, starts frame sharing through the console when it is off, copies a frame in the format the caller asks for (copying again if a new frame arrived meanwhile), and reports whether capture is usable. It never falls back to adb by itself.
- Version 0.1 does screen capture only; the input functions are declared and return `MODAVD_E_UNSUPPORTED`. See [include/modavd_extras.h](include/modavd_extras.h).
- Touch (API version 2, `MODAVD_CAP_TOUCH`) goes through the console's `event send` as standard multi-touch events (slot, tracking id, position, pressure) to the emulator's own virtual touch screen: Android sees a finger on the screen, as with a click on the emulator window, not an injected event, and nothing is installed in the guest. Up to 10 contacts.
- Keys and text are declared and return `MODAVD_E_UNSUPPORTED`: keys sent through the console reach the touch screen device and are dropped, and `event text` silently drops characters missing from the keyboard map. See [include/modavd_extras.h](include/modavd_extras.h).
- `external_renderer_ipc.dll` is a Windows-only compatibility shell. It exports the four capture functions that MaaCore looks up in MuMu's DLL, on top of the same code, so MaaCore versions that do not know ModAVD can still capture from it. AUTO-MAS uses it today. It will be retired once MaaCore can connect to ModAVD directly.
- Status: Windows has used this capture code for over a month through the shell below; on Apple Silicon macOS a contributor tested capture with the official Emulator 37.2.13 and API 35 ARM64; Linux builds and passes the unit tests in CI but has not been tested with an emulator. From v0.1.2 the [releases](https://github.com/ModAVD/ModAVDExtras/releases) carry `modavd_extras.dll` (Windows x64), `libmodavd_extras.dylib` (macOS arm64) and `libmodavd_extras.so` (Linux x64), plus the Windows-only shell.
- Build: `build.ps1` on Windows (Visual Studio 2022 Build Tools, CMake, Ninja); CMake on Linux and macOS. The unit tests need no emulator.
Expand Down
32 changes: 25 additions & 7 deletions include/modavd_extras.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* ModAVDExtras: screen capture (and, later, input) for ModAVD instances,
* ModAVDExtras: screen capture and touch input for ModAVD instances,
* for automation frameworks that want to skip adb.
*
* Copyright 2026 qiyinxi
Expand Down Expand Up @@ -37,8 +37,10 @@ extern "C" {
#define MODAVD_API __attribute__((visibility("default")))
#endif

/* Bumped whenever a function or field is added. Nothing is ever removed. */
#define MODAVD_API_VERSION 1
/* Bumped whenever a function or field is added, or a declared one starts to work.
* Nothing is ever removed.
* 1: capture. 2: touch (modavd_touch_*). */
#define MODAVD_API_VERSION 2

typedef struct modavd_instance* modavd_handle;

Expand All @@ -57,7 +59,7 @@ enum {
/* modavd_capabilities() */
enum {
MODAVD_CAP_SCREENCAP = 1u << 0,
MODAVD_CAP_TOUCH = 1u << 1, /* not yet */
MODAVD_CAP_TOUCH = 1u << 1, /* API version 2 */
MODAVD_CAP_KEY = 1u << 2, /* not yet */
MODAVD_CAP_TEXT = 1u << 3, /* not yet */
};
Expand Down Expand Up @@ -154,14 +156,30 @@ MODAVD_API int modavd_console_port(modavd_handle h); /* > 0, or MODAVD_E_IN
MODAVD_API int modavd_capture(modavd_handle h, int display, uint32_t format, uint32_t flags, void* buf,
size_t buf_size, modavd_frame_info* info);

/* ---- input: declared for the next version, MODAVD_E_UNSUPPORTED for now ---- */
/* ---- touch (API version 2, MODAVD_CAP_TOUCH) ---- */

/* Coordinates are display pixels; contact is 0-based (at most 10 at once). */
/* Touches on the emulator's own touch screen, sent through its console: Android
* sees them as a finger on the screen, not as injected events. Needs nothing in
* the guest. The console session opens on the first call and reopens once if it
* broke (the emulator restarted).
*
* Coordinates are display pixels of the shared frame (clamped to it); contact is
* 0-based, at most 10 at once; pressure 0 = the default (what a mouse click on
* the emulator window uses). Only display 0.
* - touch_down on a contact that is already down moves it.
* - touch_move on a contact that is not down: MODAVD_E_INVALID.
* - touch_up on a contact that is not down sends nothing and returns MODAVD_OK.
* MODAVD_E_NOT_READY: no frame was published yet, so the display size is unknown.
* MODAVD_E_NOT_FOUND: the console does not answer. MODAVD_E_IO: auth failed or
* the console refused the command. */
MODAVD_API int modavd_touch_down(modavd_handle h, int display, int contact, int x, int y, int pressure);
MODAVD_API int modavd_touch_move(modavd_handle h, int display, int contact, int x, int y, int pressure);
MODAVD_API int modavd_touch_up(modavd_handle h, int display, int contact);
/* Lifts every contact this handle pressed. modavd_close() will do the same. */
/* Lifts every contact this handle pressed. modavd_close() does the same. */
MODAVD_API int modavd_touch_release_all(modavd_handle h);

/* ---- keys and text: declared, MODAVD_E_UNSUPPORTED for now ---- */

/* Android key code (KEYCODE_*); down = 1 press, 0 release. */
MODAVD_API int modavd_key(modavd_handle h, int display, int android_keycode, int down);
MODAVD_API int modavd_input_text(modavd_handle h, const char* utf8);
Expand Down
4 changes: 4 additions & 0 deletions release/0.2.0.SHA256SUMS.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
7ce6ea8c0c01c14dcb5ce3cd4271e2fa9a147b3f3c4c57878e8cec8dff52f89f external_renderer_ipc.dll
b6ea821c5710d07411412011ac32e62718b8f9412451017609411ecb958a9c9a libmodavd_extras.dylib
9e870a02f864c9072111b89df77c2747ac5fabd4a4b2a2ee150d093c558cc8d0 libmodavd_extras.so
6fe84a40a5aa8da31d1a24492bf860b6912bf322e29d3bdba73bd80a34fa61da modavd_extras.dll
49 changes: 49 additions & 0 deletions release/0.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ModAVDExtras v0.2.0

- `modavd_extras.dll`:`6fe84a40a5aa8da31d1a24492bf860b6912bf322e29d3bdba73bd80a34fa61da`
- `external_renderer_ipc.dll`:`7ce6ea8c0c01c14dcb5ce3cd4271e2fa9a147b3f3c4c57878e8cec8dff52f89f`
- `libmodavd_extras.dylib`:`b6ea821c5710d07411412011ac32e62718b8f9412451017609411ecb958a9c9a`
- `libmodavd_extras.so`:`9e870a02f864c9072111b89df77c2747ac5fabd4a4b2a2ee150d093c558cc8d0`

这一版加上点击:头文件里预留的 `modavd_touch_*` 能用了,接口版本升到 2。截图和 MuMu 外壳的行为不变。

## 新增

- 点击(`MODAVD_CAP_TOUCH`):`modavd_touch_down`、`modavd_touch_move`、`modavd_touch_up`、`modavd_touch_release_all`(#15)。
- 走模拟器控制台,按多点触控协议发到模拟器自己的虚拟触屏:安卓看到的是手指在屏幕上,客体里不用装任何东西。
- 坐标是共享画面的像素,最多 10 根手指同时按下;`pressure` 给 0 就用模拟器窗口鼠标点击时的默认值。只支持显示屏 0。
- 第一次点击时连上控制台,之后一直用这条连接;模拟器重启、连接断了,下一次点击重连一次。`modavd_close()` 会先抬起这个句柄按下的手指。
- 还没有画面时(不知道屏幕尺寸)返回 `MODAVD_E_NOT_READY`。
- `modavd_api_version()` 返回 2,`modavd_capabilities()` 多了 `MODAVD_CAP_TOUCH`。
- `modavd_version_string()` 返回 `"0.2.0"`。

## 说明

- 按键和输入文字(`modavd_key`、`modavd_text`)照旧返回 `MODAVD_E_UNSUPPORTED`:控制台发的按键会进触屏设备、被丢掉,输入文字会丢掉键盘映射里没有的字,这两样以后另做。
- 只认接口版本 1 的程序不受影响:接口版本 1 的函数和行为都没变。
- `external_renderer_ipc.dll` 的源码没改,但它静态链接了库的代码,所以文件和 v0.1.3 不一样。

## 许可证与源码

- LGPL-3.0-or-later。正文是 tag 下的 [LICENSE](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/LICENSE)(LGPL-3.0)和 [COPYING](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/COPYING)(GPL-3.0);转发这些库时请一起带上。
- 源码:<https://github.com/ModAVD/ModAVDExtras/tree/v0.2.0>。

## 下载与核对

四个文件都由本仓库的 release workflow 在 GitHub 上编出,和 tag 下的 `release/0.2.0.SHA256SUMS.txt` 核对过才上传。编译环境和 v0.1.3 一样:

- Windows:`windows-2025`,`cmake -A x64`,`--config Release`。两个 DLL 都静态链接 C 运行库,只依赖 `KERNEL32`、`WS2_32`,编译带 `/Brepro`。
- macOS:`macos-15`(Apple Silicon),`cmake -DCMAKE_BUILD_TYPE=Release`。
- Linux:`ubuntu-24.04`(x64),`cmake -DCMAKE_BUILD_TYPE=Release`。

请用附件 `SHA256SUMS.txt` 核对:Windows 上 `Get-FileHash .\modavd_extras.dll -Algorithm SHA256`,macOS 上 `shasum -a 256 libmodavd_extras.dylib`,Linux 上 `sha256sum libmodavd_extras.so`。

## English

This release adds touch: the `modavd_touch_*` functions reserved in the header now work, and the API version is 2. Capture and the MuMu shim behave as before.

- Touch (`MODAVD_CAP_TOUCH`): `modavd_touch_down`, `modavd_touch_move`, `modavd_touch_up`, `modavd_touch_release_all` (#15). They go through the emulator console as multi-touch events on the emulator's own virtual touch screen, so Android sees a finger on the screen and nothing is needed in the guest. Coordinates are pixels of the shared frame; up to 10 contacts at once; `pressure` 0 uses the default of a mouse click on the emulator window; display 0 only. The console session opens on the first touch and is kept; if it broke (the emulator restarted) the next touch reconnects once. `modavd_close()` lifts the contacts the handle pressed. Before the first frame (display size unknown) the calls return `MODAVD_E_NOT_READY`.
- `modavd_api_version()` returns 2, `modavd_capabilities()` adds `MODAVD_CAP_TOUCH`, `modavd_version_string()` returns `"0.2.0"`.
- Keys and text (`modavd_key`, `modavd_text`) still return `MODAVD_E_UNSUPPORTED`. API version 1 functions are unchanged. `external_renderer_ipc.dll` has no source changes, but it links the library code statically, so the file differs from v0.1.3.
- License: LGPL-3.0-or-later; the texts are [LICENSE](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/LICENSE) (LGPL-3.0) and [COPYING](https://github.com/ModAVD/ModAVDExtras/blob/v0.2.0/COPYING) (GPL-3.0) at the tag. Ship them with the libraries. Source: <https://github.com/ModAVD/ModAVDExtras/tree/v0.2.0>.
- All four files are built by this repository's release workflow on GitHub, the same way as v0.1.3, and checked against `release/0.2.0.SHA256SUMS.txt` at the tag before upload. Check them with the attached `SHA256SUMS.txt`.
111 changes: 73 additions & 38 deletions src/console.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* A minimal client for the emulator console (telnet-style text on 127.0.0.1).
* Version 0 only uses it to start frame sharing; input will use it too.
* Used to start frame sharing (one-off connections) and to send touch input
* (a session kept open per handle).
*
* Copyright 2026 qiyinxi
* SPDX-License-Identifier: LGPL-3.0-or-later
Expand Down Expand Up @@ -97,8 +98,9 @@ static int send_line(mx_socket s, const char* line)
return send(s, cmd, n, MX_SEND_FLAGS) == n ? 0 : -1;
}

int mx_console_command(int port, const char* command, int timeout_ms)
int mx_console_open(int port, int timeout_ms, mx_console* c)
{
c->sock = (intptr_t)MX_BAD_SOCKET;
char token[256];
read_auth_token(token, sizeof(token));

Expand All @@ -108,55 +110,88 @@ int mx_console_command(int port, const char* command, int timeout_ms)
return MODAVD_E_IO;
}
#endif
int rc = MODAVD_E_NOT_FOUND;
mx_socket s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s != MX_BAD_SOCKET) {
if (s == MX_BAD_SOCKET) {
#ifdef _WIN32
DWORD tv = (DWORD)timeout_ms;
WSACleanup();
#endif
return MODAVD_E_IO;
}
#ifdef _WIN32
DWORD tv = (DWORD)timeout_ms;
#else
struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 };
struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 };
#endif
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
#if defined(SO_NOSIGPIPE)
int one = 1;
setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
int one = 1;
setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
#endif
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((unsigned short)port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
char buf[4096];
if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0 && read_until_ok(s, buf, sizeof(buf)) == 1) {
int authed = 1;
if (token[0]) {
char auth[300];
snprintf(auth, sizeof(auth), "auth %s", token);
authed = send_line(s, auth) == 0 && read_until_ok(s, buf, sizeof(buf)) == 1;
}
if (!authed) {
mx_log(MODAVD_LOG_WARN, "console %d auth failed", port);
rc = MODAVD_E_IO;
}
else {
int ok = send_line(s, command) == 0 && read_until_ok(s, buf, sizeof(buf)) == 1;
mx_log(MODAVD_LOG_INFO, "console %d %s -> %s", port, command, ok ? "OK" : "fail");
rc = ok ? MODAVD_OK : MODAVD_E_IO;
}
send_line(s, "quit");
}
else {
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((unsigned short)port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
char buf[4096];
int rc = MODAVD_OK;
if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) != 0 || read_until_ok(s, buf, sizeof(buf)) != 1) {
#ifdef _WIN32
int err = WSAGetLastError();
int err = WSAGetLastError();
#else
int err = 0;
int err = 0;
#endif
mx_log(MODAVD_LOG_WARN, "console %d connect failed (%d)", port, err);
mx_log(MODAVD_LOG_WARN, "console %d connect failed (%d)", port, err);
rc = MODAVD_E_NOT_FOUND;
}
else if (token[0]) {
char auth[300];
snprintf(auth, sizeof(auth), "auth %s", token);
if (send_line(s, auth) != 0 || read_until_ok(s, buf, sizeof(buf)) != 1) {
mx_log(MODAVD_LOG_WARN, "console %d auth failed", port);
rc = MODAVD_E_IO;
}
}
if (rc != MODAVD_OK) {
mx_close_socket(s);
#ifdef _WIN32
WSACleanup();
#endif
return rc;
}
c->sock = (intptr_t)s;
return MODAVD_OK;
}

int mx_console_send(mx_console* c, const char* command)
{
if (c->sock == (intptr_t)MX_BAD_SOCKET) {
return MODAVD_E_NOT_FOUND;
}
mx_socket s = (mx_socket)c->sock;
char buf[4096];
if (send_line(s, command) != 0) {
mx_console_close(c);
return MODAVD_E_NOT_FOUND;
}
int r = read_until_ok(s, buf, sizeof(buf));
if (r < 0) {
/* The emulator went away or stopped answering: the session is unusable. */
mx_console_close(c);
return MODAVD_E_NOT_FOUND;
}
return r == 1 ? MODAVD_OK : MODAVD_E_IO;
}

void mx_console_close(mx_console* c)
{
if (c->sock == (intptr_t)MX_BAD_SOCKET) {
return;
}
mx_socket s = (mx_socket)c->sock;
send_line(s, "quit");
mx_close_socket(s);
c->sock = (intptr_t)MX_BAD_SOCKET;
#ifdef _WIN32
WSACleanup();
#endif
return rc;
}
Loading
Loading