Files
curios/docs/research/harmony-screen-performance.md
T

21 KiB
Raw Blame History

Harmony Connector 实时投屏性能:官方资料与协议

调研日期:2026-09-10。范围:macOS、用户已授权的 USB 调试/HDC、官方 UITest 扩展;不要求开发者账号、HAP 安装、root 或修改设备安全策略。本报告仅做官方资料及官方分发包的静态分析,没有启动设备代理或测量手机帧率。用户要求优先完成编码,因而不在本报告中承诺实测 60 fps。

结论

应把实时画面从逐帧 JPEG 改为官方 Hypium recorder 的 H.264 视频流,再由 macOS 的持久视频图层显示。Hypium 26.0.0.500 官方包确实提供已带签名块的 Unix socket recorder,可通过 HDC 的 localabstract 转发实现设备端无 TCP 监听的连接。此前检查的 PyPI 6.1.0.210 包缺少这些 Unix recorder,不能据此判断鸿蒙没有这条路径。官方 Hypium 下载页、官方 26.0.0 发布说明

容易接错的 socket:实时 -m 0 使用 scrcpy_grpc_socket;文件录制 -m 1 使用 screen_record_grpc_socket。 官方 Python 的 record_agent.py 是文件录制封装,照抄它的 socket 名并把 mode 改为 0,会连不到实时服务。此区别直接来自 26 包 libscrcpy_server_unix_6.6-20260629.z.so 的 RpcServer::InitServer,见下文精确位置。

官方分发包和本地证据

来源是官方下载页当前列出的 DevEco Testing Hypium 26.0.0.500,118.7 MB,2026/08/29。通过用户现有网页登录状态点击正常下载取得官方 CDN 文件,未要求新账号或签名授权。临时签名下载 URL 不写入仓库。

研究文件保存在忽略目录 dist/harmony-screen-performance-research/;不是产品运行时路径。

文件 本地计算的 SHA256
devecotesting-hypium-26.0.0.500.zip 1c66fb288a291e3cd1743a4bf82708752bcef58c230748088194182f304a097f
hypium26/python/xdevice_devicetest-26.0.0.500-py3-none-any.whl 810b2d33bb86a6fe91c5f24fe9bc33cc3937e3af84ce1d760e3257bb648f9c06

外层 zip 包含 hypium-26.0.0.500.zip、hypium-driver-js-26.0.0500.zip 和 PyCharm 插件 zip。Python zip 包含 hypium、xdevice、xdevice_ohos、xdevice_devicetest 四个 wheel、API 文档和安装脚本。只解压读取,没有运行安装脚本。以上是下载原件的本地校验摘要,不声称已经另行比对官网公布的 SHA256。

以下资源的完整本地目录为:

dist/harmony-screen-performance-research/hypium26/python/
  xdevice_devicetest/devicetest/res/recorder/
Unix recorder 原件 字节 SHA256
libscrcpy_server_unix_6.3.1-20260113.z.so 272567 3e29e3a1d23ea6dbd4e6fb3b8f094ca5d4b15b4c580f421fba3d0e1f5429e3fe
libscrcpy_server_unix_6.4-20260113.z.so 264374 b698b41ae1351337eeea3dcc87bf0160552196d669ba321b4963c342fa5a47f4
libscrcpy_server_unix_6.5-20260313.z.so 268469 2c8f1ef94d581281bf8e86d54f5939af9b205d5db5aa2cfb27f2562215c87b0a
libscrcpy_server_unix_6.6-20260418.z.so 268471 98bbc964fef1c4d17b446ec717102b855d09224b23fc1872bf70e1f928d010dd
libscrcpy_server_unix_6.6-20260629.z.so 260278 b24231188ab0fffa84c2bd71cab6948c8ffad9d661c80301f32eaf580177ce8e

各 Unix 原件均含 elf sign block;旧 recorder 也带该块及 Huawei 软件签名证书链字符串。这证明分发文件带有签名结构,不等于本报告独立完成签名密码学验证,也不保证每个系统版本均接受每个原件。必须完整保留原件字节,不能 strip、改监听地址或重签名再声称是官方原件。

同 wheel 还提供 devicetest/res/prototype/native/uitest_agent_v1.2.3.so,以及 1.1.3、1.1.5、1.1.12 和 x86_64 资源。wheel 的 xdevice_devicetest-26.0.0.500.dist-info/licenses/LICENSE 是 Apache License 2.0;相关 Python 文件带 Huawei Device Co., Ltd. 的 Apache 2.0 版权头。打包时保留对应许可和来源,不把另一个独立下载包的授权凭空套用到这些文件。

uitest_agent_v1.2.3.so 原件为 600246 字节,SHA256 为 5dfbaca7e274a85bece8f225148ceb759b94d7319b956b9a7ea40d37944e4a9b。该 wheel 的 METADATA 明列 License-File: LICENSE;解包中未发现另外的 NOTICE 或逐二进制许可文件。产品随两种原件保留这份 LICENSE,并在自己的第三方声明中记录 Huawei 官方分发包、版本、原件路径、摘要及未修改状态;不要将自己补写的来源说明说成上游附带 NOTICE。

官方代理选择规则

直接证据文件:

hypium26/python/xdevice_devicetest/devicetest/controllers/tools/recorder/record_agent.py
hypium26/python/xdevice_ohos/ohos/environment/device.py

RecordAgent._start_scrcpy_server 首先读取 UITest 版本,要求至少 4.1.4.6,随后按 device.check_if_unix_socket_mode() 决定传输。后者实际代码判断为 UITest 版本元组大于 6.0.2.1 且架构不含 x86_64。旁边注释写的是“大于 6.0.2.2”,与代码不完全一致;客户端应使用实际能力和数值版本解析,不照抄字符串元组比较的问题。

Unix 分支只尝试文件名含 unix 的资源;非 Unix 分支只尝试不含 unix 的资源。官方按 os.walk 文件列表倒序逐个启动,没有把文件名的 6.3.1 / 6.6 映射为 HarmonyOS 产品版本。插件应固定已支持的原件,或采用明确的兼容性候选顺序;不能声称手机必须运行“鸿蒙 6.6”。

官方会覆盖 /data/local/tmp/libscreen_recorder.z.so,以进程命令和 /proc/net/unix 检查启动是否成功。产品应为自己的扩展文件与进程记录所有权,不复用或结束别的 UI 调试会话。Unix socket 名在原件中固定,因此冲突时应明确报告,而不是杀掉所有 uitest 进程。

实时 H.264 的最小连接流程

这是根据官方启动脚本和原件参数解析重建的编码接口,未在本研究中对手机执行。HDC、SERIAL、LOCAL_PORT、AGENT 为客户端已校验的值;远端扩展文件名使用本插件自己的固定名称。

"$HDC" -t "$SERIAL" file send "$AGENT" /data/local/tmp/libharmony_connector_screen.z.so
"$HDC" -t "$SERIAL" fport "tcp:$LOCAL_PORT" localabstract:scrcpy_grpc_socket
"$HDC" -t "$SERIAL" shell /system/bin/uitest start-daemon singleness \
  --extension-name libharmony_connector_screen.z.so \
  -m 0 -scale 2 -frameRate 60 -bitRate 8000000 -iFrameInterval 1000 -screenId 0

随后连接主机端 127.0.0.1:LOCAL_PORT,以 HTTP/2 gRPC 调用 /ScrcpyService/onStart,请求为 Empty,持续读取 server stream。无需创建 Driver、申请开发者证书、安装 HAP 或另发初始化 RPC。控制连接独立使用 UITest agent,避免视频读循环被输入 RPC 阻塞。

RpcServer::InitServer 在最新 Unix 原件中的入口是 0x1a960:

  • 0x1aa40–0x1aa74 默认初始化 scrcpy_grpc_socket,只在 mode 等于 1 时跳转。
  • 0x1ada8–0x1adb8 将 mode 1 的名字替换为 screen_record_grpc_socket。
  • 字符串地址 0xe001 是 scrcpy_grpc_socket,0xe014 是 screen_record_grpc_socket,0xeeca 是 unix-abstract:。
  • 0x1aaf0–0x1ab0c 拼接 unix-abstract: 后调用 gRPC AddListeningPort。没有在这个路径构建 IP:port。

官方 Python 的 -p 5001 -m 1 是录文件场景。新版 Unix 参数日志将 port 标记为 [deprecated];实时启动不必配置设备 TCP 端口。

设备端抽象 Unix socket 不在 WLAN IP 上监听;主机端仍需确保本地转发仅被期望的本机客户端访问。HDC 本身若被另行配置成网络调试,则不能仅因使用 localabstract 就宣称整个 HDC 会话来自 USB。

视频与控制同时运行

最简单的官方组合是 两个独立 singleness daemon,各加载一个扩展。ExecuteExtension 只读取第一个 --extension-name 并 dlopen 一个文件,没有一次加载两个代理的参数语义。官方扩展加载源码

server_main.cpp::StartDaemon 在 token == "singleness" 时直接执行扩展,该分支不调用普通 peer 连接的 InitAndConnectPeer,也没有以 singleness 命名的全局互斥锁。这里的字符串不是“设备只能有一个 UITest 进程”的代码证据。官方 daemon 源码

官方 Hypium 26 的 ohos/proxy/ui_proxy.py::start_uitest 启动 uitest start-daemon singleness,默认加载 /data/local/tmp/agent.so 并转发 uitest_socket;RecordAgent 另外启动带 --extension-name libscreen_recorder.z.so 的 daemon。其 UI 进程查找还特意排除命令中带 --extension-name 的进程,以区分录屏。devicetest/core/variables.py::_start_record_step_screen 在测试步骤期间启动 recorder,说明官方设计就是 UI 自动化和录制并行,而不是两个功能择一。

因此插件应采用:

控制:uitest start-daemon singleness --extension-name <本插件的控制原件文件名>
      HDC localabstract:uitest_socket → 主机控制端口
视频:uitest start-daemon singleness --extension-name <本插件的视频原件文件名> -m 0 ...
      HDC localabstract:scrcpy_grpc_socket → 主机视频端口

产品自己的会话检查必须识别这两个进程同属一个会话;不能看到任何其他 uitest 进程就拒绝。固定 socket 被其他应用占用时仍属于真实冲突,应保持现有不抢占策略。上述是官方源码和分发包的并行设计证据,不额外声称已对所有商业设备系统实现完成兼容测试。

参数

下表来自最新 Unix 原件 RpcServer::parse_params (0x19c70) 的静态分支,不是面向第三方的长期 API 稳定性承诺。

参数 已确认规则
-m 1 为 MP4 录制;其他值归为 0,实时回传编码 buffer
-scale 整数 1–5;小于等于 0 回 1,大于 5 限为 5。不是 JPEG 接口的 0.5 浮点比例
-frameRate 最新 Unix 6.6-20260629 接受 15、30、60;内部计算 120 / value 的虚拟屏刷新间隔。120 会回默认 60,不能沿用旧 recorder4 接受 120 的结论
-bitRate 至少 1048576 bit/s,否则恢复默认 31457280;按负载选择码率,不要把最大值当低延迟默认
-iFrameInterval 非负整数;非法值回 2000;需与编码器间隔单位契约一致
-screenId 非负显示 ID,通常主屏为 0
-repeatInterval 新参数,接受 8、16、33,其他值恢复 0;引用 OH_MD_KEY_VIDEO_ENCODER_REPEAT_PREVIOUS_FRAME_AFTER 和 ...MAX_COUNT。它用于重复之前的编码帧,不代表多采到了新画面

原件也存在 appPid、windowsId 和其他内部参数。当前需求不需要绕开安全窗口,不能把这些参数用作获取受保护内容的手段。

gRPC 消息

官方文件位于 .../controllers/tools/recorder/proto/scrcpy_pb2.py 和 scrcpy_pb2_grpc.py。26.0.0.500 的 scrcpy_pb2.py 与下载到的 6.1.0.210 版本内容完全相同。

syntax = "proto3";

service ScrcpyService {
  rpc onStart(Empty) returns (stream ReplyMessage);
  rpc onEnd(Empty) returns (ReplyEndMessage);
  rpc onRequestIDRFrame(Empty) returns (ReplyEndMessage);
}
message Empty {}
message ReplyEndMessage { int32 result = 1; }
message ReplyMessage {
  string data = 1;
  int32 reply_type = 2;
  map<string, ParamValue> payload = 3;
}
message ParamValue {
  oneof values {
    int64 val_int = 1;
    double val_double = 2;
    string val_string = 3;
    bool val_bool = 4;
    bytes val_bytes = 5;
    float val_float = 6;
  }
}

没有 protobuf package 前缀。三个路径分别为 /ScrcpyService/onStart、/ScrcpyService/onEnd、/ScrcpyService/onRequestIDRFrame。

官方 recorder4 的 ReplyVideoData 和静态全局键初始化能确认:payload["data"].val_bytes 才是编码数据;len、flags、pts 使用 val_int。外层 string data 不是视频 bytes。最新 Unix 原件仍有同签名的 ReplyVideoData 和同一协议;客户端应验证字段类型、长度和包大小,不根据日志字符串找图像。

reply_type 在 proto 中是普通 int32,没有枚举定义。本研究不猜测完整枚举含义。flags 来自媒体输出 buffer 属性,应按编码配置、关键帧和 EOS 标志处理,而不是把所有 payload 都当普通视频帧。

旧 recorder4 的 OnNeedOutputData 在 mode 0 里取 OH_AVMemory_GetAddr + offset、长度和 flags,调用 ReplyVideoData;mode 1 才调用 OH_AVMuxer_WriteSample。它发送的 pts 来自输出时的 system_clock.now(),静态除数和 SDK chrono 单位指向毫秒,而不是直接复制编码器 attr.pts。不要未经归一化将它当微秒或主机单调时钟;首版实时显示可采用立即显示,不等待设备绝对时间。 最新原件实际时间语义仍应通过客户端观察校准,不在这里声称已测过。

停止时,保持 stream reader 存活,用同一 channel 发 onEnd(Empty),然后释放流、关闭本插件通道并删除本插件 fport。官方 Python 将 ReplyEndMessage.result 用作录制帧数;不应把它一概当布尔成功码。onRequestIDRFrame 可以用于解码器重新同步;不要每帧请求 IDR。异常时只清理本插件拥有的进程。

官方 rpc_manager.py 建立 insecure gRPC channel,未设置应用层 token;其 5 秒 onStart 超时适合官方录制封装,不能直接复制为持续投屏生命周期。其最大消息大小 option 拼写为 grpc_max_receive_message_length,使用其他客户端时应按对应 gRPC 库的正确配置项设置大小上限。

为什么不采用旧 TCP recorder

已下载的官方 PyPI 6.1.0.210 资源只有 libscrcpy_server1.z.so 至 4.z.so。其中 recorder4 的 InitServer 使用固定 0.0.0.0 和 grpc::InsecureServerCredentials();完整参数解析没有 host、listen address 或 Unix 参数。它不是仅绑定 USB 的服务,HDC 转发本身不会把设备上已经存在的 TCP listener 限制到 USB。

未找到说明该无鉴权 gRPC 端口必然被系统网络隔离的官方保证。不能依赖这种假设作为默认生产方案,也不能修改签名原件或安全策略。26 包的 Unix 版本已经给出更合适的连接机制。官方 PyPI 分发原件

真正的按下、移动、抬起

在 26 官方包的 uitest_agent_v1.2.3.so 中,OHOS::uitest::HandleGesture (0x28c34) 直接确认了以下内层请求:

{"api":"touchDown","args":{"x":120,"y":360}}
{"api":"touchMove","args":{"x":125,"y":370}}
{"api":"touchUp","args":{"x":125,"y":370}}

沿用现有 UITest RPC 外层 envelope 和 method: "Gestures",不要把这些请求发往视频 gRPC 服务。

证据链:

  • 0x28ca0 取 api,确认是字符串;0x28cc4 取 args,确认是对象。
  • 0x28fd4–0x29028 的内联字节比较包含 touchDown 与 touchMove;0x28e88–0x28ec0 检查 touchUp。
  • 三者汇入 0x28ec4,读取 x (0xdee2) 和 y (0xdd4a);均要求整数类型,然后调用 EasyJSON::GetInteger。
  • 0x29700–0x2977c 将 down/move/up 分别映射为动作 1/2/3,经 Modules::GetUiTestLowLevelFunctions() 的触控函数注入。
  • 成功路径调用结果 callback,结果对象保持 SetNull(),不是截图或坐标返回值。错误路径包括 Illegal gesture api 和 Illegal touch point coordination。

没有发现 touchCancel 分支,也不能依赖断开连接自动抬起。 同原件的 GestureHandleSessionDeathBroadcast 指向 0x1d8f4,该入口只有一条 ret。客户端应持有自己按下的触点状态,在 mouse up、窗口失焦、取消和停止时尽力发送最后坐标的 touchUp,再关闭控制连接。只合并尚未发送的 move,不丢 down/up 或跨越它们重排。使用显示内容区域的坐标映射,扣除黑边并处理旋转,避免将窗口坐标直接当设备坐标。

该证据针对 1.2.3 原件;不凭版本相近就声称所有旧 agent 的行为完全一致。研究输出 .../res/prototype/native/gestures-key.disasm 保留了相关汇编。

JPEG 路径的性能上限来自哪里

OpenHarmony 官方 UITest screen_copy.cpp 的生产循环先调用 DisplayManager::GetScreenshot 取得全屏 PixelMap,再比较像素是否变化,再缩放并 JPEG 编码。缩放函数持有共享 frameLock;JPEG 质量设置为 75。缩小 scale 能减少缩放后编码和传输成本,但不省掉原始全屏截图读回。源码的 50 ms sleep 位于屏幕关闭处理,不是正常推流固定 20 fps 限制。官方源码固定版本

因此,仅改浏览器刷新周期、缩图或提高标称 fps 无法把逐帧截图机制变为 scrcpy 的编码视频链路。这里的开源实现解释了 UITest 公开截图机制,不声称商业 Hypium agent 每个内部细节都与该源码相同。

官方 recorder 则引用 OH_VideoEncoder_CreateByMime、H.264 MIME、OH_VideoEncoder_GetSurface 和虚拟显示 Surface,输出已经编码的媒体 buffer;这才是和 scrcpy 对应的性能方向。实际流畅度仍受设备编码器、虚拟屏刷新、USB/HDC、主机解码与呈现共同影响。

对照 scrcpy 的实现原则

Genymobile 官方文档和源码中,Surface 进入 MediaCodec,视频由独立连接传输,控制由独立线程处理;默认立即显示解码后的帧,额外视频缓冲是显式选项。帧率随画面变化,max-fps 是上限而不是静止画面也必须重复达到的最低值。scrcpy 开发文档、视频选项

其 frame_buffer.c 用 pending/tmp 交换,允许新解码帧覆盖未消费帧;先取得新引用,失败时保留旧帧。这适合用于修复画面闪烁和积压:维持同一个显示 surface,在下一帧确实可呈现时替换,不能先清空旧画面。scrcpy 帧缓冲源码

可移植到插件的设计建议:

  1. HDC + gRPC reader 独立运行,及时取出编码包,不在同一循环更新 UI 或等待触控。
  2. H.264 按解码依赖顺序送解码器;不能像独立 JPEG 那样随意丢 P 帧。解码后只保留最新可显示帧;编码队列溢出时明确重同步并请求 IDR。
  3. 持久 AppKit 视频视图,避免每帧重建窗口、WebView 图像节点、object URL 或黑底占位。
  4. 配置变更/旋转后重建必要的格式描述,普通帧不重建渲染层。停止期间保留最后有效画面直到状态视图接管。
  5. 分开统计收到编码帧、成功解码帧、实际显示帧、丢帧和队列长度。静止桌面的低更新数不等于卡顿;不能用接收包数冒充显示 fps。

这些是从官方机制推导的实现建议,不是本研究的真机性能结果。

macOS 呈现接口

AVSampleBufferDisplayLayer 能呈现压缩或未压缩视频。H.264 buffer 应正确组成 CMSampleBuffer,维护 SPS/PPS 和格式描述,交由系统视频解码/呈现;若使用 VideoToolbox 解码,可设置实时属性并检查实际是否使用硬件解码。Apple 图层文档、VideoToolbox 解压属性

kCMSampleAttachmentKey_DisplayImmediately = true 应设置在 sample attachments array,用于不按时间戳等待的实时显示。不是对样本调用普通 CMSetAttachment 就等价。新样本到达后立即呈现,有助于避免设备绝对时间与主机时间基不一致产生的积压。Apple DisplayImmediately、Apple enqueue

macOS 14 及以上可使用该层的 sampleBufferRenderer,保留项目要求的旧系统兼容路径。requestMediaDataWhenReady 必须与停止请求成对管理,窗口销毁后不能继续向已释放视图派发渲染工作。

证据边界

  • 已完成:官方 26 包获取、静态解包、资源摘要、启动/选型脚本、proto 对比、Unix 绑定与触控参数的汇编核查。
  • 未完成且未声称完成:最新 Unix 原件在用户手机的加载兼容性、连续视频解码结果、流畅度/耗电/延迟测量、旧 1.2.2 agent 的触控全量契约复核。
  • 不需要为采用本路径继续下载完整 IDE、创建 HAP 或申请开发者签名;用户的实际前置条件是可用 HDC 与设备调试授权。原件与系统 ABI 的兼容性仍需清楚报告,不能失败后偷偷切回慢截图却继续显示“实时视频”。