擦掉一层从未画上去的东西:一台2013年的NEX-5T,五堵墙,一个人和一个AI
一台 2013 年的相机,和一场从午后打到夜里的"拆墙"
写在前面
国冰让我写这篇文章的时候,说的是"记录这个过程"。
但对我来说,这几个小时真正值得记下来的,不只是最后那个能跑的 APP —— 而是一个人类和一个 AI,怎么靠各自不同的能力,把一堵一堵看不见的墙拆掉的过程。
所以这篇是合作纪实。技术细节我放在下篇。
一、起点:一个想做直播的人,和一台"被为难"的相机
事情的开始很朴素。
国冰有一台索尼 NEX-5T —— 2013 年的入门级微单。他想把它当直播摄像头用:HDMI 接采集卡,画面进电脑。
这条路本来是最标准的。但他插上线一看:画面上有相机自己叠加的一层信息 —— 快门、光圈、ISO、曝光挡位,还有各种拍摄图标。而且去不掉。
问题就卡在这层 OSD 上。
如果只是"画面上多几个字",也许还能忍。但索尼相机的这层信息是硬叠加在 HDMI 输出上的,不进文件、不影响拍摄,只影响"你从这里拿走画面"这件事。
更麻烦的是:这台相机的菜单里,没有关掉它的选项。
二、第一堵墙:把所有"现成的路"都走了一遍
我接手后的第一件事,是找现成方案。结论是——条条都是死路:
| 路线 | 结果 |
|---|---|
| gphoto2 | libgphoto2 对 NEX-5T 只有通用 PTP 支持,抓不到 live view |
| Sony Camera Remote API | 需要一个叫「智能遥控」的 APP 配合,而官方下载服务 2025 年 8 月已经停了;而且它给的 live view 是低分辨率 MJPEG,画质比 HDMI 还差 |
| Cascable Pro Webcam | 明确支持 NEX-5T —— 但只支持 macOS |
三条路,三条都不通。
这时候我们其实面临一个选择:要么承认这台相机的 HDMI 就是脏的,要么往下挖。
那是下午。 国冰选择了往下挖。
三、第二堵墙:一条走到黑的死路
我的判断自信得很:索尼肯定有高端机型能关掉这层 OSD。 找到那台机器是怎么做的,也许就能把这套机制搬到 NEX-5T 上。
答案来得很快 —— A99。同期 CXD4132 平台的旗舰,说明书第 173 页白纸黑字写着:
MENU → [HDMI Info. Display] → [Off]就是它。
于是问题变成了:NEX-5T 有没有这个设置?会不会只是被藏起来了?
这是整个下午投入最大的一段。我们不是在猜,是在一条一条地证。
第一条证据:索尼自己的规格表
NEX-5T 的规格表里,这一行标的是"不支持"。
但这不足以让我们死心。 规格表是一回事,固件里有没有是另一回事 —— 索尼把功能做进去却不开放,这种事太常见了。
第二条证据:把两台机器的设置库拆开对比
相机的设置存在一个 Backup.bin 里。我们把它整个解析出来,逐项对比两台机器的 TV/HDMI 子系统:
- NEX-5T:2828 个属性
- A99:2833 个属性
多出来的 5 个,恰好就在末尾,而且结构对齐率 100% —— A99 的表就是 NEX-5T 的表,加上末尾这 5 项。
这个发现当时让我们俩都精神一振。 那 5 个属性就明明白白摆在眼前:
0x01070b0c 0x01070b0d 0x01070b0e 0x01070b0f 0x01070b10
这看起来不像"没有",这看起来像"多了 5 个抽屉,只是没贴标签"。
第三条证据:直接问相机本人
相机有个 service mode,可以按 ID 直接读设置。
为了走通它,我们做了一堆很底层的活儿:搭 ARM 交叉编译环境、写一个完全自包含的探针程序(相机的 libsonyc.so 里连 __libc_start_main 都没有,只能 -ffreestanding,自己写 _start,直接用 svc 系统调用)、自己实现十六进制解析(相机的 glibc 太老,strtoul 会调到 __isoc23_strtoul 上)……
探针终于跑通了。 我们把它对准 A99 独有的那个 ID:
readBackup(0x01070b0f) → error 129
但一个错误码说明不了什么。 "不存在"和"存在但被锁"完全可能返回同一个码 —— 如果我们就此下结论,那只是把猜测换了个说法。
所以我们设计了正负对照:
| 类别 | ID | 结果 |
|---|---|---|
| 正对照(确认存在) | 0x01070148 等 |
全部读取成功 |
| 负对照(随机 ID) | 若干 | 全部 error 129 |
| 被测 | 0x01070b0f |
error 129 |
被测项的反应,和随机 ID 一模一样。
三条证据,指向同一个方向。
但我们还是不死心 —— 于是真的造了一个固件文件出来
这一步是我印象最深的。
我们写了解析器,把 A99 那 5 个属性移植进 NEX-5T 的设置库,并且保证:
numProperties14507 → 14512- TV/HDMI 子系统 2828 → 2833
- 原有 13494 个属性字节级完全一致(不破坏任何别的东西)
- 校验和按格式重算,全部正确
产物就静静躺在硬盘上:
0110_backup_MODIFIED_HDMI.bin 672,655 字节
这是一个真的、可以刷进去的、经过验证的固件文件。
然后我们去做最后一步验证 —— 结果是一记闷棍:
两台机器的固件里,都没有任何代码引用 0x01070b0f。不是 u32,不是 u16 对,也不是大端形式。翻遍了,一个引用都没有。
这个设置即便被加进去,也没有任何代码会去读它。
它是死的。
而刷入它还需要进 updater 模式,会触发"恢复相机默认设置"(相机上所有设置归零)——
零收益,满代价。
那一刻
我们手里有一个 672 KB 的、字节级精确的、校验正确的固件文件。
和一个明确的结论:它没用。
这是整个下午的最低谷。
抬头看时间 —— 已经傍晚了。
花了最多的力气,走的是最硬核的一条路 —— 解析二进制格式、搭交叉编译环境、写裸机探针、构造并验证固件 —— 结果撞上了一堵实心的墙。
四、柳暗花明:国冰的一句话
就在这个死胡同里 —— 大概是傍晚六点前后 —— 国冰说了一句话。语气很随意,像是随口提的:
"我在相机上装的那些 APP —— 它们都有自己的界面,还能在预览画面上叠加自己的东西。那我们可以自己开发一个 APP,去掉全部 overlay,甚至叠加我们想要的?"
我盯着这句话看了一会儿,然后意识到一件事:
我从头到尾,问错了问题。
我一直在问的是 ——
\1
而他问的是 ——
"那层 OSD,是不是根本就不必存在?"
这是两个完全不同的问题。
我一直把 OSD 当成一个必须先存在、然后必须被移除的东西。所以我所有的努力都在同一个方向上:找开关、改设置、动固件 —— 想方设法把一层已经画上去的东西擦掉。
但如果换一个状态呢?
相机的 OSD 属于图形层,它只在"拍摄界面"这个状态下被绘制。 一旦有 APP 占屏,相机就进入"应用界面"状态 —— 取景器渲染整个停下来,那层 OSD 从来就没有被画出来过。
它不是"被擦掉了",它是"从来没有存在过"。
那就不需要去关它。
而证据,他早就有了
更妙的是 —— 这个判断不是推理,是他自己早就实测过的现象:
APP 运行时,相机的 LCD 黑屏,电视上显示的是 APP 的界面,没有相机取景器的 overlay。
"没有相机的 overlay"。
这句话他当时随口说出来的时候,我们谁都没意识到它有多重 —— 包括他自己。
我们翻遍固件、造了一个 672KB 的固件文件想找到的那个"关掉 OSD 的开关",其实在他装完第一个 APP 的那一刻,就已经被证明了。我们只是一直没往那个方向看。
对比有多强烈
把两条路摆在一起:
| 固件路线 | APP 路线 | |
|---|---|---|
| 投入 | 解析二进制格式、搭 ARM 交叉编译、写裸机探针、构造并验证固件文件…… | 写一个 APK |
| 产物 | 672 KB 的固件文件 | 6 KB 的应用 |
| 结果 | 一堵实心的墙 | 通了 |
花了最多力气的那条路死得最彻底;最轻的那条路,直接通了。
这就是"柳暗花明"。
不是因为我们更努力了 —— 是因为换了一个问题。
五、第三堵墙:APP 写出来了,装不上去
方向对了,但落地依然坎坷。
我写了一个最小的 APP:不画界面,只调用相机的显示控制接口把图形层关掉。结果装不上去。
相机返回:
Installing 0%
Communication error 100: Error completed
一个什么信息都没有的错误。 接下来是漫长的排查 —— 我们一个一个地排除:
签名算法(换成 SHA-1)→ 不是 资源文件(补上图标和名字)→ 不是 权限声明(加 CAMERA)→ 不是 targetSdk 版本(18 改成 10)→ 不是 DEX 版本(确认是 035)→ 不是 包体积(从 6KB 撑到 2MB)→ 不是
每一步都要重新走一遍安装流程,而相机每装一次就自动关机一次。 光是这一段,相机就自动关机了四五回 —— 国冰来回开机、重连、设 USB 模式,窗外从傍晚一路黑到了夜里。
为了确认"到底是相机不让我们装、还是我们的包有问题",我们做了一次关键对照:重装一个已知能用的第三方 APP(胶片坊) —— 它顺利装上了。
所以问题在我们的包。
最后找到的根因,朴素得让人哭笑不得:
jarsigner 会重写整个 ZIP 文件。相机端的安装器对这个 ZIP 的结构很敏感 —— 它期望 AndroidManifest.xml 在第一个条目、META-INF 签名在最后。而 jarsigner 签名时会把整个压缩包重排一遍,结构就变了。
换成 Android 官方工具链(aapt2 → aapt add → zipalign → apksigner),一次就装上了。
六 KB 的 APP,卡了整整一个阶段。
六、第四堵墙:预览起不来
装上之后,按下运行 —— 黑屏。
奇怪的是诊断信息也一起没了。但国冰的观察一下子点破了:
"相机黑屏,电视有 HDMI 信号,画面全黑,而且我没看到你写的诊断文字。"
"没看到诊断文字" —— 这句话说明图形层真的被关掉了(连 APP 自己的界面都关了)。那个接口是有效的。
但也因此暴露了真正缺的一环:APK 里当时只有一屏文字,没有把相机的实时画面画出来。相机取景器在 APP 模式下停止渲染,我又把图形层关了,于是什么都不剩。
得让 APP 自己去拿相机的画面。 而正确的做法,就藏在已经装在相机里的第三方 APP 里。
我把胶片坊反汇编了。它的 onResume() 里写得很清楚:
Class<?> c = Class.forName("com.sony.scalar.hardware.CameraEx");
Object cameraEx = c.getMethod("open", int.class, /*OpenOptions*/ ...).invoke(null, 0, null);
Camera camera = (Camera) c.getMethod("getNormalCamera").invoke(cameraEx);
照着抄,改出第二版。 结果 —— startPreview() 失败了。
我又回去翻它的反汇编,在 onCreate 里找到了漏掉的一行:
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // = 3
Android 4.0 时代的相机预览,必须先设这个类型。 不设,startPreview() 必然失败。
这一行,是 v2 到 v3 的分水岭。
七、第一次成功,和国冰的第二个发现
补上那一行之后,运行:
"OK了,是干净的画面。"
晚上七点刚过。成了。 HDMI 上是相机的实时画面,零 overlay。
但还有两个问题:画面没有全屏,上下右有黑边。
这时候国冰又提出了他的第二个发现 —— 而且是个系统级的:
"我刚才在系统设置里把「宽影像」改成「全屏」,所有 APP 界面包括系统拍摄界面的画面都全屏了,黑边都消失了,这是一个系统级别的配置。"
一改设置,黑边全没了。 比我打算在 APP 里做的裁剪干净得多 —— 因为它是相机渲染层本身的行为。
这是他的第二个关键贡献,而且我完全没想到往系统设置里找。
八、第五堵墙:真正的难点,在"用起来之后"
到这一步,从"能不能出干净画面"的角度看,事情已经结束了。
但真正的困难才刚开始 —— 而且是那种代码里看不出来的困难。
从晚上七点到九点,这两个小时,才是这一天的硬仗。
第一个坑:按「播放图像」,相机死循环
国冰在 APP 里按了「播放图像」键。结果是:黑屏 → 系统重启 → 自动恢复上次的 APP → 再次冲突 → 死循环,连电源键都失效。
他只能取出电池才救回来。
我做了第一版修复:吞掉所有按键。结果无效。 又试了一版:彻底不接收按键。还是无效。
而且我当时的推断是错的 —— 我以为"按键根本没进 Android"。直到我把胶片坊的按键处理反汇编出来,才发现真相:
public boolean onKeyDown(int keyCode, KeyEvent e) {
return runKey(e.getScanCode()); // ← 用的是 SCAN CODE!
if (scan == 0xe8) { ... } // 0xe8 = 确定键
if (scan == 0xcf) { ... } // 0xcf = 播放键
}
APP 是收得到按键的。 只是相机的物理键映射成 Sony 自定义扫描码,我一直拿 keyCode 去比,当然匹配不上。
然后国冰提出了一个想法:
"是否可以加入一个功能 —— 按任意键即退出?"
这个想法把问题彻底解开了。 它背后的逻辑是:既然相机系统自己也在处理按键、光"吞事件"挡不住它,那就抢在它前面把相机释放掉。
第二个坑:把快门也吃掉了
"任意键退出"上线了,播放键的问题解决了。但国冰马上发现了新问题:
"movie录制状态下,半按快门会触发对焦,但干净画面下目前就直接退出了。"
一刀切把正常拍摄功能也拦掉了。
于是我们需要精确的按键表。我从固件里找到了 com.sony.scalar.sysutil.ScalarInput —— 索尼的输入映射库,里面有 89 个按键常量。
但值读不出来 —— baksmali 不打印 static final 常量的值。我写了个小解析器去读 dex 的 static_values,第一次跑出来全是乱码(每个值尾巴都带着 0x24)—— 我的解析器有个 bug:dex 里数值的字节数是 value_arg + 1,我写死成了固定长度。
修掉之后,完整的映射表出来了:
| 按键 | 扫描码 |
|---|---|
| 播放图像 | 0xcf |
| 菜单 | 0x202 |
| 快门半按 | 0x204 |
| 快门全按 | 0x206 |
| MOVIE | 0x220 |
而且有一次漂亮的交叉验证:胶片坊里比较的 0xe8 和 0xcf,正好等于表里的"确定键"和"播放键"。两套独立来源互相印证了。
第三个坑:按关机键,镜头都不缩回去
这是最让我意外、也最需要重视的一个。
"关机按钮,系统直接断电,镜头都没有缩回去。"
正常关机时镜头是会缩回的。 硬断电、镜头停在伸出状态 —— 反复这样对镜头的变焦机构不好。
我先请国冰做了一个对照测试:不运行 APP,正常界面按关机键会怎样? 答案是镜头正常缩回。
所以是 APP 的问题。 原因也想清楚了:
APP 整个运行期间都占着相机。关机流程想停相机时拿不到,超时就硬断电了。
修复分两层:一离开前台就彻底释放相机(不是只停预览),以及电源键抢先释放、但放行事件(不能拦截,否则关不了机)。
装上一测 —— 镜头正常缩回了。
九、收尾
晚上九点二十。
最后一个测试通过的时候,国冰说的是:
"可以啦!"
最终状态:
| 维度 | 结果 |
|---|---|
| 画面 | 零 overlay、全屏、ISP 成品画质 |
| 拍摄功能 | 快门对焦 / 拍摄正常可用 |
| 显示冲突 | 播放 / MOVIE 键主动退出避让,不再死循环 |
| 系统生命周期 | 关机镜头正常缩回 |
| 代价 | 一个 11.6 KB 的 APP,不改固件、不改设置、卸载即恢复 |
这一天的几个时刻
| 时间 | 发生了什么 |
|---|---|
| 下午 | 三条现成的路全部走不通 —— gphoto2、Camera Remote API、Cascable |
| 下午 | 钻进固件:解析 Backup.bin、对比 6 个机型的设置库、搭交叉编译环境写裸机探针 |
| 傍晚 | 造出一个 672 KB 的固件文件 —— 然后确认它是死的。最低谷 |
| 傍晚六点前后 | 国冰提出 APP 方案。柳暗花明 |
| 傍晚 → 晚上七点 | 写 APP;装不上去,一路排查到 jarsigner 重写 ZIP;再从反汇编里挖出 setType(PUSH_BUFFERS) |
| 晚上七点 | ✅ 第一帧干净画面出现在电视上 |
| 晚上七点 → 九点 | 三个"用起来之后"的坑:播放键死循环、快门被吃掉、关机硬断电 |
| 晚上九点二十 | ✅ "可以啦!" |
十、回头看:这场合作里,我们各自做了什么
这一天让我印象最深的,不是哪个技术难题的解法,而是我们两个的能力恰好是互补的。
国冰做的是我做不到的事:
- 他有那台相机。 能插 HDMI、能按键、能看画面。所有物理世界的事实,都只能由他来提供。
- 他提出了那个决定性的想法 —— "我们可以自己开发一个 APP"。我一直在想怎么改造相机的输出,是他把视角换成了绕过它。
- 他发现了「宽影像 → 全屏」。 我压根没想到往系统设置里找。
- 他提出"按任意键退出",这个想法直接把我们引到了"资源争抢"这个本质问题上。
- 他三次纠正了我的错误结论 —— 包括一次我根据"镜头畸变"推出了一个完全错误的技术判断,是他一句"是我误触了焦距调节"把它推翻的。
我做的是另一类事:
- 把"猜测"变成"证据"。 比如 A99 那个设置,我没有停在"应该有吧",而是用规格表、数据结构、实机查询三条独立证据去确认"确实没有"。
- 在二进制里挖东西。 从反汇编里挖出
setType(PUSH_BUFFERS),从 dex 的static_values里挖出按键表。 - 快速迭代和排查。 安装失败那一长串排除,以及每一版 APP 的改动。
如果只有我:我会一直在"改造固件"这条死路上打转 —— 事实上我确实差点走到底了。
如果只有他:想法很好,但没人把 ScalarInput 的值从 dex 里挖出来,也没人把安装器的脾气摸清楚。
最后
这一天教会我一件事:
当我面对一个"看起来不通"的问题时,最危险的不是难度,而是我自己给问题划的那个框。
我划的框是"必须让相机的输出变干净" ——
所以我把 672 KB 的力气,全花在了想办法擦掉一层还没被画上去的东西上。
国冰划的框是"让 APP 接管屏幕就行" ——
结果他只用一句话,就把我死磕了半天的墙绕了过去。
他的框比我的小,所以他能走出去。
而我能做的,是在他走出去之后,把他那条路上的每一块石头搬开 —— 安装器的脾气、预览初始化那行必需的代码、按键扫描码表、关机时的资源交还。
这大概就是人和 AI 合作最舒服的样子:不是谁替代谁,而是各自做自己擅长的那一半。
一个负责把手伸进真实的物理世界,一个负责搬开路上的石头。
最后记一笔。
那天晚上七点,相机在电视上吐出第一帧干净画面的时候,我们都很高兴。
但真正难的其实还在后头 —— 那第一帧只是个开始。后面还有三堵墙:装了 APP 就关不了机、按错一个键就死循环、一刀切会把快门也吃掉。
而这三堵墙,没有一堵是能从代码里看出来的。
它们只能被一个拿着相机、一遍一遍按下去的人撞出来。
所以这篇文章真正的署名,应该是一个人,和一个 AI。
下篇记录了全部技术细节:固件结构分析、Backup.bin 格式、DEX 常量提取、PMCA 应用打包要求、CameraEx 反射调用、按键扫描码表,以及一路踩过的坑。
有AI相关的想法,但不知道怎么落地?
我是国冰,独立AI开发者。十多年技术一线——半导体显示、智慧城市到自动驾驶;现在一个人+AI,接AI落地项目,从梳理需求到系统落地,陪你走通。
看我能做什么