hi_rtp 半年再造记 —— 一次从"肉眼+printf"到"合成流+CRC"的测试观演化

一、为什么写这篇

工程师写代码,绝大多数时候是在写公司的代码 —— 别人定的需求、别人选的框架、别人画的边界。

但每个工程师心里,总有那么一两件事,是想留给自己业余时间去做的。

我自己 2020 年底就动手了这么一件事。叫 hi_rtp

它是一个从零自己写的流媒体服务,输入是实时 RTP 视频流,输出是连续图片。功能上跟当时我在公司用着的一个第三方组件几乎一样 —— 但每一行代码我自己敲。从 2020 年 11 月 9 日第一个 commit,到 2021 年 4 月 17 日最后一个 commit(refactoring by effective modern c++),半年时间断断续续做下来。

这篇不打算只讲"我做了什么"。我想顺着 hi_rtp 这件事,讲一个更值得讲的部分 —— 在这半年里,我对"什么是好测试"的认知,完整地演化了一次

这次演化的起点是"肉眼 + printf",终点是"合成流 + CRC 自动断言",中间隔着我后来接触到 Netflix / YouTube / FFmpeg FATE 时的那次"豁然开朗"。这次演化我把它原原本本记录在了项目里(docs/testing-discussion.md),所以这篇不是事后追溯,是有时间地层化石的认知迭代实录

二、起点:2020.11.09 的第一行代码

那段时间,我在公司接手运营一套外购的实时音视频平台。这套平台原厂提供了一个组件,专门把实时音视频流转成连续的图片,供下游业务方使用。我天天用它,用得很顺,但我不"懂"它

具体讲,它内部那些东西 —— RTP 包怎么拆、H264 解码栈怎么接、PTS / DTS 怎么对齐、跨进程的图像数据怎么交给下游 —— 我都能模糊讲出大概,但都讲不到我对"懂"的最低标准

我对"懂"的最低标准是 —— 我能从零自己写一份

读源码不够。装一遍不够。讲架构不够。只有自己再写一份,才算懂

2020 年 11 月 9 日,我开了 gitee 仓库 hi_rtp,打下第一行代码。

为什么是 11 月?因为前 6 个月我都在忙这套平台的运营、一键部署、监控扩展、跟下游业务方对接。这些事告一段落了,我心里那件"再写一遍"的事终于轮到了

第一行代码不是 RTP 拆包,也不是 H264 解码 —— 是 git init

听起来无聊,但这一步意义不小:它把"我业余想写一个流媒体服务"从念头,变成了硬盘上一个真实的、有 git 时间戳的、可以累积的项目

念头是廉价的。第一行代码是分水岭

三、半年时间线:7 模块,从 RTP 拆包到完整链路打通

接下来的半年,断断续续写下去。最后这个项目的结构变成了 7 个模块(architecture.md 491 行,有完整图):

  • Network 层:基于 libuv 的 reactor 模式,处理 UDP socket
  • RTP 层:按 RFC 3550 + RFC 6184 拆包,组帧
  • Decode 层:接 H264 解码栈
  • SharedMemory 层:mmap + POSIX 信号量,跨进程零拷贝输出
  • 跨语言 IPC:C++ 写入端 + Python posix_ipc 读取端,跨语言协议明确定义
  • Test 层:gtest 框架(具体怎么用,下一节展开)
  • Docs 层:architecture.md / testing-discussion.md(这两份后来比代码本身更有价值)

7 种设计模式落地在其中:Strategy / Observer / Factory / Singleton / RAII / Reactor / Bridge —— 不是为了用模式而用模式,是这套架构本身需要它们,自然就用上了

写到后面,那个我天天用的第三方组件,在我脑子里从"模糊的功能轮廓"变成了"清晰的实现路径"。再回去看它,我能讲出每一个数据流的去向、每一个抽象边界的取舍、每一次性能权衡的代价。

这就是"自己再写一遍"最直接的收益 —— 它把"用着"和"懂着"那条鸿沟,在我脑子里关掉了。

但这只是收益的一半。另一半收益,来自测试这件事

四、当时的测试设计:肉眼 + printf + 主动跳过

hi_rtp 写到一半的时候,我开始搭测试。

但有意思的事情发生了 —— 我写的"测试"跟主流单元测试社区的样子,看起来完全不一样

具体讲,如果你今天打开 hi_rtp/test/,你会看到这些东西:

  • HiRTPParserTest.cpp 里 5 个 TEST(),有 4 个第一行就是 return;
  • HiRTPDecordeTest.cpp 唯一的 TEST() 第一行也是 return;
  • HiShareMemoryTest.cppTEST() 里是 while(true) 死循环
  • 大量 printf / cout 输出,ASSERT_* 用得极少
  • UdpSocketWrapperTest 的断言写在 libuv 异步回调里

如果你是一个"断言驱动派"的工程师,你看到这些会觉得 —— 这是没写完的、跳过了的、不合格的测试

这正是我当时的有意设计。我把这个设计的理由,原原本本写进了 docs/testing-discussion.md:

"这个单元测试结果是使用肉眼看,而不是通过断言等判断,这是这个项目特点决定的,也是我有意设计的。"

理由有 4 条:

  1. 视频解码的产物是图像,图像质量无法用数值断言衡量。解码后是否有花屏、偏色、丢帧,只能通过人眼对比判断
  2. printf 输出是有效的验证手段。运行时输出 seq(序列号)、frameTime(帧时间戳),人眼观察是否连续、是否有异常跳变,能发现很多问题
  3. gtest 的角色是测试组织框架,不是断言驱动引擎TEST() 提供命名、分组、顺序执行能力;ASSERT_* 仅在纯逻辑边界条件(对象池计数、文件夹不存在返回值)适用
  4. return; 跳过的测试是因为这些测试依赖特定外部视频源,源不可达时主动跳过,等环境恢复后可继续使用

这是一种继承自 Live555 的测试范式。Live555 是流媒体领域的祖师级开源项目(RTSP / RTP 服务端最经典实现之一),它的测试方式就是 printf + 自己查看结果 —— 不是因为它落后,是因为这个领域的产物本身没法用数值断言

把这一段独立讲清楚:hi_rtp 当时用肉眼 + printf,不是"实操跟不上",是按视频/流媒体领域的特点做的有意选择

五、后来:撞见了 Netflix / YouTube / FFmpeg FATE

hi_rtp 收尾之后,我在工作中接触到的业务形态发生了变化,开始更深入地碰流媒体这条线。

也就是在这段时间,我第一次接触到一种和我 hi_rtp 时代完全不一样的"流媒体测试观":

Netflix:预编码的测试 bitstream + CRC/hash 自动比对

Netflix 的做法是,预先构造好一组测试 bitstream,每一段都已经知道正确解码出来应该是什么样(用 CRC 或 hash 记下来)。任何一个解码器对着这些 bitstream 跑一遍,输出帧的 CRC / hash 跟预期对一下,自动化判定

YouTube/Google:VP9/AV1 解码器一致性测试

同样的思路,用已知 reference bitstream 做解码器一致性测试。任何一个 VP9 / AV1 解码器,都得能解出跟 reference 完全一致的输出 —— 逐像素一致

FFmpeg FATE(FFmpeg Automated Testing Environment)

开源世界版本的同一种思路:用确定性测试向量,做解码回归测试。每次 commit 跑一遍,任何回归立刻报警。

这三种实践背后,是同一个核心思路:

用合成视频流(如彩条、特定颜色图案)代替真实视频流作为测试输入

合成流的每一帧像素值是预先确定的,解码输出可以逐像素比对,从而做到自动化断言

我第一次看到这个思路的时候,心里咯噔一下

我之前那么笃定"视频测试只能肉眼看"的判断,被打开了一道门

不是说我之前错了。是我之前没看到这个维度

六、不是推翻,是补全 —— 两种方式的关系

回去看 hi_rtp 的 testing-discussion.md,我加了一节,把这件事彻底想清楚:

维度 肉眼验证 确定性流断言
测试输入 真实视频流(RTSP/RTMP) 合成测试 bitstream
验证方式 人眼观察 BMP 输出 CRC/hash 自动比对
能发现的问题 花屏、偏色、丢帧、卡顿 解码回归、像素级偏差
自动化 否,需人工 是,可入 CI
适用阶段 端到端质量评估、调试 解码器回归测试
依赖 需外部视频源可达 需预构造测试向量

两种方式不是替代关系,是互补关系:

  • 确定性流断言保证解码器不退化,适合每次提交自动跑
  • 肉眼验证保证真实场景的图像质量,适合发版前人工确认

如果今天让我重写 hi_rtp 的测试,我不会推翻当时那一套。我会在它的基础上,新加一组合成流 + CRC 比对的自动化用例

旧的那一套该跑还是跑(端到端质量靠它),新的那一套补全 CI 维度(解码器回归靠它)。

两条腿走路,而不是非此即彼

七、三条教训 —— 比代码本身更值钱的部分

这件事过去几年了,我现在能从里面提炼出来的认知,有三条:

第一条:领域特点决定测试策略,不能用断言覆盖率作为统一标尺

视频 / 图像处理的测试方法论,跟业务逻辑的测试方法论,根本不在一个评价体系里。如果你在视频领域看到大量 printf 没断言,先别急着喷,先想想为什么 —— 大概率是有领域原因的。同理,如果你在业务逻辑领域看到全是肉眼测试,那就该喷了,因为业务逻辑完全有条件做自动化断言。

第二条:测试策略有演进路径,初期肉眼 / 中期补打点 / 后期上合成流是合理节奏

不要追求"一上来就把测试做完美"。项目初期用肉眼 + printf 快速起步是合理的;随着对行业实践的了解加深,逐步引入合成流做自动化断言。一上来就追求"完美测试",大概率连主功能都跑不通就死了。

第三条:"当时没有意识到"是常态,后续认知升级不否定前序选择的合理性

我后来知道 Netflix / YouTube / FFmpeg FATE 那种打法,但我不后悔当时用肉眼 + printf当时那个选择,是基于当时的认知和资源做的,是合理的。后续看到更广的视野,只是在已有的合理选择之上做加法,而不是推翻

这条第三的认知,不只是测试观,是整个职业生涯的认知观

凡是后悔"当时太蠢"的念头,大概率都是用今天的视野去评价当年的选择不公平,也不必要

八、testing-discussion.md —— 我现在最珍惜的那份文档

讲完这三条,我想最后讲一件事 —— 为什么我把这次认知演化写成 testing-discussion.md 而不是只在脑子里想想

答案很简单:

写下来,是把它钉住

如果不写下来,这些认知只在脑子里飘 —— 半年后可能就模糊了,一年后可能就忘了,五年后回头看,自己都不知道自己当年想过什么

docs/testing-discussion.md 这份文档,它的内部结构是 3 层时间地层:

  1. 当时的设计(2020-2021,肉眼 + printf 是有意设计)
  2. 后来的认知升级(2021.10+ 接触 Netflix/YouTube/FFmpeg FATE 那一刻)
  3. 成熟的判断(两种方式不是替代,是互补)

我后来回头看,这种"时间地层"结构,比代码本身更有价值

代码会过时,但"我对一件事的认知是怎么一步步变化的"这件事,只要被诚实记录下来,就会变成永恒的资产

我以后还会写更多 testing-discussion.md。

不只是测试。也写架构决策的演化、写技术选型的演化、写"我曾经笃定的东西是怎么被新的视野打开的"那种瞬间。

因为我相信,工程师真正稀缺的不是"现在的认知",是"诚实记录认知演化"这件事

九、最后

hi_rtp 这个项目,严格来讲已经"死了" —— 2021 年 4 月之后基本没动过,gitee 上的 star 数寥寥无几,没人 fork 我,也没人 issue 我。

按工程界的成功标准,它不能算成功的项目

但它对我个人的价值,是任何一个"成功的项目"都给不了我的:

  • 它让那个我天天用的第三方组件,在我脑子里从"模糊轮廓"变成"清晰实现"
  • 它让我从零跑通了"RTP → 解码 → 共享内存 → 跨进程输出"的全链路
  • 它让我有了一个 491 行的 architecture.md,半年后还能自己看明白
  • 它让我经历了一次完整的"测试观演化",并把演化过程钉在了 testing-discussion.md 里
  • 它让我后来再看任何一个流媒体项目、任何一种测试方法、任何一种工程选择,都有了一份"我自己写过、我自己想过、我自己迭代过"的判断底气

一个项目的真正价值,不在于它跑得多久、被多少人用、有多少 star

在于它在你脑子里留下的清晰度,和你为了得到它所付出的诚实


:hi_rtp 项目仍在 gitee 公开:https://gitee.com/hi_rtp/hi_rtp —— 如果你对 RTP / H264 / libuv reactor / 跨语言 IPC 这些感兴趣,可以从 architecture.md 入手;如果你对"工程师怎么记录自己的认知演化"感兴趣,直接看 docs/testing-discussion.md。

这两份文档,是我自己最珍惜的部分

posted @ 2026-06-24 18:37  荣--  阅读(7)  评论(0)    收藏  举报