乐趣区

源码级解析阅读源码分析并解决scrcpy无法正常输入中文的问题

移动互联网时代下,手机能干的事情越来越多,但如果想要让工作更高效,鼠标键盘依旧是必不可少的。可许多软件(点名阿里系)并没有提供对应的桌面版本,也不兼容基于 x86 架构的 Android 模拟器,这就使得我们要用投屏软件来在电脑上操作手机。scrcpy 就是众多投屏软件中最具特色的一款,作为一款开源软件,它拥有极佳的性能和丰富的功能,但这款软件在中文输入方面却存在较大的问题。本文将为读者介绍如何让 scrcpy 正常输入中文,让这款非常好用的投屏软件变得更好用。
本文原载于未命名小站,由作者本人同步至知乎,转载请注明原作者博客地址或本链接,谢谢!

本文撰写时 scrcpy 最新为 1.14 版本,依旧存在下文所述的问题,当你阅读本文时也许 scrcpy 已经解决了这一问题,因此本文内容仅供思路参考和技术分享。

0x01 问题重现

scrcpy 相对于其他仅依靠 adb shell screencapadb shell input进行设备控制的软件,拥有更加优秀的性能,这得益于它的系统架构:

其中 Server 在每次启动 scrcpy 的时候运行于 Android 端,使用 MediaCodec 的 API 对采集到的画面进行编码,并使用多线程,通过 Socket 传输到 PC。PC 端则使用 ffmpeg 和 SDL2 对画面进行实时解码显示。其中 Server 使用 Java 开发,Client 使用 C 开发。具体技术细节可以参考官方文档,此处不再赘述。

言归正传,scrcpy 在 Unicode 文字输入方面一直存在巨大问题,从很久之前就有用户反馈无法输入 ascii 以外的文字(如 #632,表现为 PC 端输入文字后,手机不显示,终端报错,见图 1),而作者则在最近正式加入了对 ascii 字符的支持,但尚未合并到 master 分支(见#1426)。

图 1. 无法正常输入汉字,并提示无法插入字符

笔者尝试拉取代码库,并按照开发文档将 #1426 所在分支 d613b10efcdf0d1cf76e30871e136ba0ff444e6e 进行构建。

构建之后运行,我们会发现问题略有改善,但输入过程中的字母也被传入 scrcpy,如图 2 所示:

图 2. 笔者想输入“测试”,但输入过程中的 ceshi 也被输入到了 Android 中

因为是尚未发布的功能,没有相关的资料可供参考,我们只能自行阅读源码查找原因。

0x02 问题分析

首先我们要了解一下 SDL 处理用户输入(以及其他事件)的流程:

  1. 使用 SDL_WaitEvent(&event) 获取事件队列中的事件
  2. 根据 event->type 对不同事件进行区分处理

这里的事件类型可以参考 SDL2 的官方文档:SDL_Event。

顺着这样的思路,我们很快就能找到 scrcpy 处理事件的相关代码:app/src/scrcpy.c,这里截取一段:

static enum event_result
handle_event(SDL_Event *event, bool control) {switch (event->type) {
        case EVENT_STREAM_STOPPED:
            LOGD("Video stream stopped");
            return EVENT_RESULT_STOPPED_BY_EOS;
        case SDL_QUIT:
            LOGD("User requested to quit");
            return EVENT_RESULT_STOPPED_BY_USER;
        case EVENT_NEW_FRAME:
            if (!screen.has_frame) {
                screen.has_frame = true;
                // this is the very first frame, show the window
                screen_show_window(&screen);
            }
            if (!screen_update_frame(&screen, &video_buffer)) {return EVENT_RESULT_CONTINUE;}
            break;
        case SDL_WINDOWEVENT:
            screen_handle_window_event(&screen, &event->window);
            break;
        case SDL_TEXTINPUT:
            if (!control) {break;}
            input_manager_process_text_input(&input_manager, &event->text);
            break;
        case SDL_KEYDOWN:
        case SDL_KEYUP:
            // some key events do not interact with the device, so process the
            // event even if control is disabled
            input_manager_process_key(&input_manager, &event->key, control);
            break;
...

结合代码和 SDL 文档,可以发现 SDL_KEYDOWNSDK_KEYUP事件会被传递到 input_manager_process_key() 函数,这两个事件只针对按键,不针对输入法;而 SDL_TEXTINPUT 事件则会被传递到 input_manager_process_text_input() 函数,这个事件处理的是输入法确认输入后所发送的文本。

这里我们可以使用调试工具(或万能的 printf)了解上面『测试』这个文字的输入过程中,事件传递的流程。最终得到如下所示的事件顺序:

2020-06-17 17:16:49.831 scrcpy[57759:5396589] INFO: KEYINPUT # c
2020-06-17 17:16:49.919 scrcpy[57759:5396589] INFO: KEYINPUT # c 抬起
2020-06-17 17:16:50.731 scrcpy[57759:5396589] INFO: KEYINPUT # e
2020-06-17 17:16:50.840 scrcpy[57759:5396589] INFO: KEYINPUT # e 抬起
2020-06-17 17:16:51.341 scrcpy[57759:5396589] INFO: KEYINPUT # s
2020-06-17 17:16:51.440 scrcpy[57759:5396589] INFO: KEYINPUT # s 抬起
2020-06-17 17:16:51.657 scrcpy[57759:5396589] INFO: KEYINPUT # h
2020-06-17 17:16:51.719 scrcpy[57759:5396589] INFO: KEYINPUT # h 抬起
2020-06-17 17:16:51.933 scrcpy[57759:5396589] INFO: KEYINPUT # i
2020-06-17 17:16:52.041 scrcpy[57759:5396589] INFO: KEYINPUT # i 抬起
2020-06-17 17:16:52.408 scrcpy[57759:5396589] INFO: KEYINPUT # 空格
2020-06-17 17:16:52.408 scrcpy[57759:5396589] INFO: TEXTINPUT # 测试
2020-06-17 17:16:52.519 scrcpy[57759:5396589] INFO: KEYINPUT # 空格抬起

可以发现,除了倒数第二行的 TEXTINPUT 事件,其他均为 KEYDOWNKEYUP事件。那么问题来了,我们如何屏蔽掉这些多余的事件呢?

继续阅读 input_manager_process_key() 函数和 input_manager_process_text_input() 函数,我们会发现它们都对『某种特殊情况』做了判断,并会抛弃掉特殊情况下的一些输入:

// input_manager_process_key()
...
struct control_msg msg;
// convert_input_key()返回 true 才会真正插入字符,可是为什么会存在返回 false 的情况?if (convert_input_key(event, &msg, im->prefer_text)) {if (!controller_push_msg(controller, &msg)) {LOGW("Could not request'inject keycode'");
    }
}
...

// input_manager_process_text_input()
...
void
input_manager_process_text_input(struct input_manager *im,
                                 const SDL_TextInputEvent *event) {
    // 为什么要在 prefer_text 为假的时候提前返回呢?if (!im->prefer_text) {char c = event->text[0];
        if (isalpha(c) || c == ' ') {assert(event->text[1] == '\0');
            // letters and space are handled as raw key event
            return;
        }
    }
...

是的!这两处可疑的地方都与 prefer_text 这个参数有关,其值为真或为假会对 scrcpy 处理按键输入和文本输入的行为进行截然相反的控制:

  1. prefer_text 为假:

    • TEXTINPUT事件所得到的字符长度如果为 1,直接抛弃事件
    • KEYDOWNKEYUP 不受影响
  2. prefer_text 为真:

    • TEXTINPUT事件不受影响
    • KEYDOWNKEYUP 事件对应的字符如果是字母或空格,直接抛弃事件

需要补充说明的是,根据 SDL 官方文档和我们的实际测试,在输入英文(不使用输入法)的时候,按下一个键会触发三个事件:

  1. KEYDOWN事件
  2. TEXTINPUT事件
  3. KEYUP事件

也就是说如果没有 prefer_text 参数的控制,按下一个键我们将会得到两个键。继续翻阅代码,我们会发现 prefer_text 参数的默认值为 false,即满足上文第一种情况。

结合我们上面输入中文的测试,可以发现 KEYDOWNKEYUP事件在 prefer_text 默认为 false 情况下是会被直接输入到 Android 端的;而如果我们想屏蔽这两个事件,就必须保证 prefer_text 为真。

怎么样,读到这里,相信读者们已经发现一些端倪了吧?

0x03 问题解决

继续阅读源码,会发现 prefer_text 参数来源于启动时传入的 --prefer-text 选项,文档中这样描述这个选项:

Text injection preference
There are two kinds of events generated when typing text:

key events, signaling that a key is pressed or released;
text events, signaling that a text has been entered.
By default, letters are injected using key events, so that the keyboard behaves as expected in games (typically for WASD keys).

But this may cause issues. If you encounter such a problem, you can avoid it by:

scrcpy --prefer-text

(but this will break keyboard behavior in games)

关于这个参数的解释写得非常抽象,仅描述了这一参数的行为和特殊情况下后果,并没有介绍这一参数的一般用途,难怪笔者一开始并未发现。

这里我们使用上文提到的分支 d613b10efcdf0d1cf76e30871e136ba0ff444e6e 进行重新构建,并在启动时携带 --prefer-text 参数:

此时问题不再出现!

0x04 后续

鉴于 scrcpy 这部分文档过于简略,且不容易被注意到,笔者向作者提交了一个 Issue(#1516)说明这一情况,希望引起作者重视,完善相关文档(或者考虑默认启用这一选项?)。如果作者有意,笔者也希望能够为其撰写中文文档,方便其他有同样需求的用户快速上手,毕竟在国内互联网生态下,太多软件只有手机版本,有此需求的中国用户数不胜数,而能够读懂源码,并能在机缘巧合之下读到本文的人确实少之又少。

也许当读者读到本文的时候,这一软件版本已经高于 1.14,相关问题也早已得以解决,但笔者依旧希望通过这篇文章,为读者提供一种解决问题的思路,即:

多读源码!

退出移动版