首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用WorkBuddy排查360方案HDMI CEC播放暂停问题全过程分享

用WorkBuddy排查360方案HDMI CEC播放暂停问题全过程分享

原创
作者头像
用户12817859
发布于 2026-10-13 21:31:34
发布于 2026-10-13 21:31:34
680
举报

一名嵌入式 Android TV 工程师,用 AI 智能体 WorkBuddy 从代码定位到根因闭环,2 小时解决了一个困扰多日的 CEC 按键 bug。本文完整复盘排查全过程,包括走过的弯路、修正的方向、以及最终钉死的根因链路。

一、背景

我是一名嵌入式 Android TV 行业的软件工程师,主要在 HiSilicon 平台上做 TV/STB 软件开发。自从使用了 AI 智能体 WorkBuddy 参与解决项目开发中遇到的 bug 后,工作效率得到了大幅提高——它能直接 SSH 到开发服务器读代码、跑命令、做 diff,不只是聊天。

下面用一个真实案例说明:360 方案 HDMI CEC Play/Pause 键只能暂停、无法恢复播放 的完整排查过程。

问题现象

  • 平台:Hi3751V360,Android 15,代码库 Android15_Hisi360_Dev(服务器 10.10.11.170:/data/ren_xd/)
  • 现象:开启 CEC 功能后,HDMI 通道下按遥控器 Play/Pause 键,只能实现暂停,不能从暂停转回播放
  • 对照:同样代码结构的 570 方案(Android15_Hisi570_Dev),Play/Pause 键功能完全正常

涉及代码目录

目录

说明

vendor/huanglong/modules/tvmw/

中间件 CEC 底层实现(C++)

vendor/cultraview/

CultraView 定制层(Java)

frameworks/base/services/core/java/com/android/server/hdmi/

AOSP HDMI CEC 框架层


二、排查过程

第一步:锁定代码范围

我:登录 170 服务器找到 Android15_Hisi360_Dev 代码,目前遇到一个 HDMI 通道 CEC 方面的问题,打开 CEC 功能后,按遥控器上的 play/pause 键,只能实现暂停,不能从暂停转到播放了。主要看 vendor/huanglong 和 vendor/cultraview 这两个目录。

WorkBuddy:代码库找到了(/data/ren_xd/Android15_Hisi360_Dev)。开始定位 CEC 相关实现。

关键突破:MwRxCecCommonCmd.cpp 里有主动发送 USER_CONTROL_PRESSED 的代码。顺着读下去,找到了 RX 处理链,重点看按键映射表和发送侧。

第二步:对比 570 正常方案,初步怀疑

我:同样代码结构的 570 方案,刚测试了,play/pause 键功能是好的。我一会儿把 360 方案的 log 抓给你。

WorkBuddy 已定位到高度可疑点:

结论先说:360 的 CEC 发送等待逻辑比 570 少了一道校验,570 正好有这道补丁。

文件:vendor/huanglong/modules/tvmw/source/hal/tv_mw_framework_rxcecctrl.cpp

函数:MwRxCecCtrl::SendCommandResending()

570(正常)的发送等待循环:

代码语言:cpp
复制
for (; u32Count < CEC_CMD_STATE_CNT; u32Count++) {
    s32Ret = uapi_hdmirx_cec_get_cur_msg_state((uapi_hdmirx_cec_status *)&m_stCecTx);
    if (s32Ret != TD_SUCCESS || m_stCecTx.u8CurTxLogAddr != pstCecCmd->enDstAddr ||
        m_stCecTx.enCurTxOpcode != pstCecCmd->enOpcode) {   // ← 校验本条消息真的发出去了
        MW_MSLEEP(10);
    } else { break; }
}

360(异常)的发送等待循环:

代码语言:cpp
复制
if (s32Ret != TD_SUCCESS) { MW_MSLEEP(10); }   // ← 只判返回值,不校验 opcode/地址
else { break; }                                   //   读到驱动残留状态就 break

初步分析:m_stCecTx 是成员变量,会残留上一条消息的状态。第二次按键时可能读到残留状态就提前判定"已送达",配合 CecResendMsgTimes = 3(T360/cfg.ini:374),实际发送次数不确定。对一个 toggle 语义的 Play/Pause 来说,多发/漏发直接表现为"回不到播放"。

⚠️ 后来证明这个方向是错的——但这正是真实排查的过程,先记下来。

第三步:抓日志验证(第一次走了弯路)

WorkBuddy 给出了抓 log 的过滤条件:

代码语言:bash
复制
adb shell logcat -c
adb shell logcat -v threadtime TvMW_MwRxCecCtrl:V TvMW_MwRxCecManager:V TvMW_MwRxCecRemoteCtrlHandler:V *:S

还建议了一个快速验证:把 T360/cfg.ini:374 的 CecResendMsgTimes 临时改成 1(关闭重发),如果症状消失就锁定重发逻辑。

我:先抓了一份 570 的,你看一下,桌面 570-good.txt。

WorkBuddy:日志看完了——但结论不在日志里。

第四步:日志不能用,根因转向

日志为什么不能用

这份 570-good.txt 有两个致命问题:

  1. 时间窗只有 1.6 秒(01:32:14.331 — 01:32:15.949),logcat -c 之后马上就 Ctrl+C 了
  2. 全文 0 条含 cec 的记录,连 Cec 大小写都搜了,里面只有 AconfigFlags(1150 行)、DolDTV_stack_ssu(240 行)这类开机噪声,当前还是 DVBT 通道

等 log 这段时间,WorkBuddy 把两套代码 diff 完了,根因已经找到,而且不需要日志佐证。

根因:360 换了一条发送路径,撞上 AOSP 的固定映射

先撤回上一轮的猜测——SendCommandResending 缺校验那处是 360 新增的防御性改动,跟这个 bug 无关。

真正的链路(全部实测确认):

① 遥控器 play/pause 键 = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE

② 发送路径差异——这是根因所在

570 的 CtvCommon.sendCecCommand():

代码语言:java
复制
if (event.getAction() == ACTION_UP) return HalApi.setUICommand(keyCode);

360 的同一函数(1597 行起):这三行被注释掉了,改走 AOSP 原生 HdmiTvClient.sendKeyEvent(keyCode, true/false)。

③ framework 收到后查表

文件:frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecKeycode.java:322

代码语言:java
复制
// 317-319 行 AOSP 原注释:
// Note that we map both MEDIA_PAUSE and MEDIA_PLAY_PAUSE to CEC PAUSE key.
new KeycodeEntry(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, CEC_KEYCODE_PAUSE),  // = 0x46,固定映射

同文件 368-369 行:PAUSE_PLAY_FUNCTION(0x61) 对应 UNSUPPORTED_KEYCODE——没有任何 Android keycode 能产出 0x61。

④ 结果

每次按都发 <User Control Pressed>(0x44) + UI cmd 恒为 0x46 Pause → 机顶盒永远暂停,回不到播放。症状 100% 吻合。

⑤ 570 为什么正常

570 的 HalApi.java:720 mediaPlayPause() 用静态 isStop 在 CEC_MENU_PLAY(→0x44) / CEC_MENU_PAUSE(→0x46) 之间交替,所以第二次能发 Play。而 g_keyCode[] 映射表、MW_CEC_DEV_MENU_COMMAND 枚举、MwRxCecRemoteCtrlHandler.cpp、MwRxCecCommonCmd.cpp 两方案逐字节一致,已彻底排除。

第五步:360 日志实锤

我:又抓了一份 360 的,你看一下有没有用,桌面 360-bad.txt。

WorkBuddy:有用,而且是完整的实锤。这份 log 把代码分析闭环了。

4 次按键,4 次都发的是 Pause:

时间

源地址

目的地址

Opcode

UI Command

含义

16:39:53.273

0x0 (TV)

0x4 (Playback 1)

0x44 USER_CONTROL_PRESSED

0x46

Pause

16:39:56.629

0x0

0x4

0x44

0x46

Pause

16:40:04.390

0x0

0x4

0x44

0x46

Pause

16:40:08.071

0x0

0x4

0x44

0x46

Pause

0x44 Play 一次都没出现。 四次按下、四次都是 Pause——和 HdmiCecKeycode.java:322 那条 KEYCODE_MEDIA_PLAY_PAUSE → CEC_KEYCODE_PAUSE 的固定映射逐字对上。

三条旁证
  1. 发送者是 PID 2200 android.hardware.tv.cec@1.0-service——这是 HAL 服务,坐实走的是 framework → HAL 标准链路
  2. 厂商 tag 是 0 条:TvMW_MwRxCecCtrl、TvMW_cecctrl、TvMW_MwRxCecManager、TvMW_rxtifcec_manager 一个都没出现——说明按键根本没走厂商 CEC 发送路径(上一轮让抓 TvMW_MwRxCecCtrl 是错的方向,已修正)
  3. CtvCommon 有 16 条日志——sendCecCommand() 确实被调用了,getHdmiCecMode() 门控也过了

根因已经钉死。源地址 0x0(TV)、目的地址 0x4(Playback Device 1) 及 ACK 均正常,纯粹是 UI Command 操作数错误。不用怀疑寻址、不用怀疑 ACK、不用怀疑重发。

顺带修一个日志抓取的坑

360 上 CEC 问题要看 android.hardware.tv.cec@1.0-service 这个 tag,别抓 TvMW_*:

代码语言:bash
复制
adb shell logcat -c
adb shell logcat -v threadtime "android.hardware.tv.cec@1.0-service":V CtvCommon:V *:S

570 反而要看 TvMW_* 那组(因为 570 走 HalApi)。两方案的日志位置是反的,这个值得记一下。


三、修复方案

方案 B2 —— 推荐,与 570 行为对齐,不碰 framework

在 360 CtvCommon.sendCecCommand() 里把 KEYCODE_MEDIA_PLAY_PAUSE 单独分流,用本地 bool 交替翻译成 KEYCODE_MEDIA_PLAY / KEYCODE_MEDIA_PAUSE 再交给 hdmiTvClient.sendKeyEvent()。

这两个 keycode 在 framework 表里映射是对的:

  • KEYCODE_MEDIA_PLAY → 315 行 → 0x44 Play ✓
  • KEYCODE_MEDIA_PAUSE → 321 行 → 0x46 Pause ✓

关键点:翻转必须放在 DOWN 的 debounce 通过之后(CEC_KEY_INTERVAL = 350ms,CtvCommon.java:207/1642),DOWN/UP 复用同一个翻译结果,否则会在 DOWN/UP 各翻一次。

570 的"摇摆"分两级

我:570 是在哪个文件解析成 0x44 和 0x46 摇摆的,你想在 360 代码哪个文件里做修改呢?

WorkBuddy:570 的"摇摆"是纯 Java 逻辑,就在 HalApi.mediaPlayPause()(701~709 行),一个 static boolean + 两行 setUICommand。C++ 层只是把它翻成协议码,两边枚举索引 7/12 完全对齐。

360 要改的文件

唯一一处:vendor/cultraview/common/apps/CtvMiddleware/CultraviewTvService/src/android/cultraview/tv/service/common/CtvCommon.java

位置

改动内容

成员变量区 208 行

在 private KeyEvent lastKey = null; 旁新增 private boolean mCecPlayPausePaused = false;

主改动 1642~1647 行

DOWN 分支 debounce 通过后做翻译:翻转 mCecPlayPausePaused,根据状态输出 KEYCODE_MEDIA_PLAY 或 KEYCODE_MEDIA_PAUSE

配套 1657 行

UP 分支改用翻译后的 keycode(复用 DOWN 翻译结果)

语义核对:mCecPlayPausePaused 初值 false → 第一次按下翻成 true → 发 PAUSE;第二次翻回 false → 发 PLAY。和 570 的 isStop 完全一致。

两个要提醒的点
  1. HalApi.java 不要改,改了不生效。 360 的 CtvCommon.sendCecCommand() 已经不调用它了(1590-1592 那三行被注释),HalApi.mediaPlayPause() 在 360 上对 CEC 透传而言是死代码。除非把 CtvCommon 整个改回 570 的写法——但那会丢掉 360 改用 framework 的初衷(认证要求的 DOWN/UP 间隔、active source 由切源链路保证),不建议。
  2. getRepeatCount() == 0 守卫别省。 因为翻转点在 DOWN 分支,而 DOWN 会随长按反复上来。现有 debounce 是 350ms,不拦这个的话长按 2 秒会来回翻 6 次,比原 bug 还难看。570 之所以没这问题,是因为它只在 ACTION_UP 响应,长按不重复。

另外 CtvCommon.java 属于独立子仓库 CultraviewTvService(.git 软链到 .repo/projects/...),当前 git status 是干净的,改完 diff 和回滚都好办。

方案 B1 —— 备选,1 行改动

HdmiCecKeycode.java:322 改成 CEC_KEYCODE_PAUSE_PLAY_FUNCTION,由 sink 自己 toggle,TV 侧零状态。更符合 CEC 规范,但 0x61 属 CEC 1.4+ Function 命令、设备兼容性未验证,且与 570 实现不一致。


四、效果验证

修复后实机验证:Play/Pause 键播放/暂停切换正常,bug 结案。

Mantis 提交记录

以下是提交到公司 Mantis 系统的原因分析(纯文本排版,可直接粘贴):

【问题现象】

开启 CEC 功能后,HDMI 通道下按遥控器 Play/Pause 键只能暂停,无法从暂停恢复播放。

【根本原因】

360 方案整机切换为 TIF CEC 模式(vendor/device/tv/T360/properties/vendor_properties.mk:persist.vendor_im.tvmw.cec.tifmode=true),厂商中间件 CEC 发送通道被整体旁路——modules/tvmw/source/hal/tv_mw_framework_rxcecctrl.cpp:97,MwRxCecCtrl::setCommand() 在 TIF 模式下直接 return(Enable()/RegisterRxcecCmdCallback() 同样 bypass),且 360 未提供 570 方案具有的 tv_mw_logic_rxtifcec_manager.cpp,厂商侧无替代实现。

因此上层 CEC 发送改由 Android framework 承接:vendor/cultraview/.../CtvCommon.java 的 sendCecCommand() 中原 HalApi.setUICommand() 已注释,改调 hdmiTvClient.sendKeyEvent()。

而 AOSP 映射表 frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecKeycode.java:322 将 KEYCODE_MEDIA_PLAY_PAUSE 固定映射为 CEC Pause:

代码语言:java
复制
new KeycodeEntry(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, CEC_KEYCODE_PAUSE),  // 0x46

该表原注释已说明其为接收方向设计("both MEDIA_PAUSE and MEDIA_PLAY_PAUSE map to CEC PAUSE key"),且 0x61 Pause Play Function 在表中为 UNSUPPORTED_KEYCODE,无任何 Android keycode 可产出。故每次按键发出的 UI Command 操作数恒为 0x46 Pause,toggle 语义丢失。

【日志证据】(tag: android.hardware.tv.cec@1.0-service,连续按键 4 次)

代码语言:bash
复制
16:39:53.273   0x0 -> 0x4   opcode:0x44 USER_CONTROL_PRESSED   arg[0]=0x46 Pause
16:39:56.629   0x0 -> 0x4   opcode:0x44                        arg[0]=0x46 Pause
16:40:04.390   0x0 -> 0x4   opcode:0x44                        arg[0]=0x46 Pause
16:40:08.071   0x0 -> 0x4   opcode:0x44                        arg[0]=0x46 Pause

4 次全为 Pause,0x44 Play 一次未出现;每次伴随 0x45 USER_CONTROL_RELEASED。源地址 0x0(TV)、目的地址 0x4(Playback Device 1) 及 ACK 均正常,仅 UI Command 操作数错误。

【修复方案】

在 CtvCommon.sendCecCommand() 中为 KEYCODE_MEDIA_PLAY_PAUSE 补回 toggle 语义:维护本地状态 mCecPlayPausePaused,ACTION_DOWN 通过 350ms debounce 后翻转(加 getRepeatCount()==0 守卫,避免长按重复翻转),交替翻译为 KEYCODE_MEDIA_PLAY / KEYCODE_MEDIA_PAUSE 后交由 sendKeyEvent 发送,ACTION_UP 复用同一 keycode。行为与 570 方案 HalApi.mediaPlayPause()(isStop 交替发 CEC_MENU_PLAY / CEC_MENU_PAUSE)保持一致,不修改 framework。

【修改文件】

vendor/cultraview/common/apps/CtvMiddleware/CultraviewTvService/src/android/cultraview/tv/service/common/CtvCommon.java

【验证结果】

实机验证通过,Play/Pause 键播放/暂停切换正常。


五、经验总结

这次排查走过的弯路

阶段

做了什么

对不对

第一步

定位 vendor/huanglong + vendor/cultraview 代码

✅ 范围对了

第二步

怀疑 SendCommandResending 缺校验

❌ 方向错了,那是防御性改动

第三步

抓 570 的 log 验证

❌ log 没用上(时间窗太短、通道不对)

第四步

diff 两套代码,定位到 framework 固定映射

✅ 根因在这

第五步

抓 360 的 log 实锤

✅ 4 次全 Pause,闭环

WorkBuddy 在其中发挥的作用

  1. 快速代码定位:SSH 到服务器,直接 grep/read,比我手动翻快得多
  2. 双方案 diff:同时对比 360 和 570 的实现差异,精准锁定改动点
  3. 日志分析:虽然第一次抓的 log 不能用,但它没有停下来等日志,而是利用等待时间把代码 diff 做完,提前找到了根因
  4. 修复方案设计:给出了推荐方案(B2)和备选方案(B1),还提醒了两个容易踩的坑(HalApi 改了不生效、getRepeatCount 守卫不能省)
  5. Mantis 文档输出:直接生成符合公司提交格式的纯文本原因分析

关于日志抓取的经验

360 和 570 两套方案的 CEC 日志位置是反的:

  • 360(TIF 模式):看 android.hardware.tv.cec@1.0-service tag
  • 570(厂商模式):看 TvMW_MwRxCecCtrl / TvMW_MwRxCecManager 等 tag

如果照搬 570 的抓 log 方式去抓 360,会一无所获——这个坑值得所有做跨方案对比的同事注意。


本文基于 Hi3751V360 Android 15 平台真实排查记录整理,代码路径和行号对应 Android15_Hisi360_Dev 代码库。

#WorkBuddy #CodeBuddy #AndroidTV #HDMI #CEC #嵌入式开发 #AI辅助开发

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、背景
    • 问题现象
    • 涉及代码目录
  • 二、排查过程
    • 第一步:锁定代码范围
    • 第二步:对比 570 正常方案,初步怀疑
    • 第三步:抓日志验证(第一次走了弯路)
    • 第四步:日志不能用,根因转向
      • 日志为什么不能用
      • 根因:360 换了一条发送路径,撞上 AOSP 的固定映射
    • 第五步:360 日志实锤
      • 三条旁证
      • 顺带修一个日志抓取的坑
  • 三、修复方案
    • 方案 B2 —— 推荐,与 570 行为对齐,不碰 framework
      • 570 的"摇摆"分两级
      • 360 要改的文件
      • 两个要提醒的点
    • 方案 B1 —— 备选,1 行改动
  • 四、效果验证
    • Mantis 提交记录
  • 五、经验总结
    • 这次排查走过的弯路
    • WorkBuddy 在其中发挥的作用
    • 关于日志抓取的经验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档