人物采访 / T SALON

编辑内容

莲叔谈音视频入门与移动开发成长:从一个播放器到完整的问题

整理 T Chat 第 8 期两段录播:莲叔以 Tiny Player 介绍跨平台工程、FFmpeg 解封装与解码、视频渲染、音频回调和同步,再分享创业与 UC 经历、学习方法、技术深度、团队招聘和职业选择。

T Chat 第 8 期莲叔分享音视频播放器实践与移动开发成长的访谈封面
T Chat 第 8 期莲叔分享音视频播放器实践与移动开发成长的访谈封面

本期视频发布于 2022 年。文中的技术状态、个人经历与观点均为当时情况。

从写一个能运行的小项目开始,是莲叔这次音视频入门分享的方式。T Chat 第 8 期中,他用 Tiny Player 把媒体文件一路送到屏幕和扬声器,让格式、编解码、线程与同步这些概念有了具体位置。当时他在 UC 负责短视频客户端相关工作,关注音视频、函数式编程与端侧智能等方向。

技术分享之后,对谈又回到开发者自身:兴趣怎样转化为持续投入,工作中的问题如何帮助建立深度,以及面对平台变化和职业迷茫时,怎样形成自己的判断。

技术分享:把音视频数据送到屏幕和扬声器

先认识生产和播放的整条链路

莲叔选择音视频作为入门主题,一方面是因为拍摄、播放、直播和图像处理都有实际应用;另一方面,这个领域的一些核心概念能够跨越工具与平台延续。标准和实现会演进,但压缩、格式、渲染和同步等知识,可以帮助理解后来的新工具。他以 OpenGL 与 Metal 等图形技术的变化作例子,强调基础理解的迁移价值。

他随后用短视频的生产与消费解释数据流。拍摄端从摄像头取得图像,按处理链的需要转换格式,在图形处理环节完成美颜等操作,再分别用于预览和编码;麦克风采集的音频则经过相应处理与编码。封装器把编码后的音视频按容器格式组织到文件里,MP4 是其中一种常见形式。

播放端大致反向进行:先从容器中解出不同媒体流,再分别解码,把画面交给渲染,把声音交给音频设备,最后维持两者在时间上的对应关系。示例中的 YUV 到 RGB 转换服务于所选渲染路径,不应据此认定所有设备都必须经过完全相同的内存转换过程。

他用自拍短视频把生产路径逐段串起来:摄像头的数据经处理后,一路送到屏幕,拍摄者才能看到自己的预览;另一路送入视频编码器。麦克风采集的声音则经音频编码器处理,再与视频汇入封装器。封装时两类数据要按播放顺序交错写入;如果文件前半段全是画面、后半段才是声音,播放时就需要频繁跳到文件的不同位置找数据。这个例子解释了“封装”为什么不只是给文件取一个 .mp4 后缀。

容器、编码与原始数据是不同层次

理解播放器之前,需要把几个概念分开。容器规定媒体数据怎样组织;编码标准决定音频或图像怎样压缩和还原;解码得到的原始数据,又有自己的采样或像素格式。一个 MP4 文件的扩展名,并不能代替其中视频、音频编码方式的说明。

分享提到 H.264、H.265、VP8、VP9 等视频编码,以及 AAC 音频编码。原始音频以 PCM 等形式表示,图像则常见 RGB 与 YUV 一类表示方式;具体内存排列、分量及采样方式还会进一步细分。莲叔借这些概念说明,音视频工程里相当多的工作,是在不同数据格式之间正确转换,并处理实际输入中的边界情况。

先搭好跨平台工程,再写播放器

这次演示有三个目标:搭起跨平台工程,实现一个最小播放器,并在 iOS 上运行。核心使用 C++,与平台有关的部分单独实现;Android 移植作为设计上的可能路径介绍,现场并没有演示完成 Android 版本。

平台隔离可以有不同做法。大量条件编译虽然直接,却容易把平台逻辑散落到公共代码里。莲叔采用抽象接口与各平台实现,让核心代码通过共同接口操作平台能力。目录也据此分出核心源码、平台源码、依赖库、示例工程和构建产物,减少不同职责混在一起的情况。

他在代码组织上把 Core 与 Platform 分开:公共播放器拿到的是抽象接口指针,具体平台各自提供实现。这样一来,iOS 端既可以借助 Objective-C++ 与系统视图、音频 API 通信,Android 端将来接入时也有明确的替换位置。条件编译仍可能出现在工程配置中,但不必把每个平台的判断塞进每段播放器逻辑。

构建还涉及目标平台的工具链。在 Mac 上为 iOS 生成产物,需要相应的 SDK、架构与工具链配置;这与简单地编译一个只在当前机器运行的程序不同。他选用 CMake 组织工程,并介绍了它与其他构建工具的背景,但没有要求所有跨平台项目都采用同一方案。

演示中的 CMake 配置包括声明目标、收集核心与平台源文件、设置头文件和库搜索路径、关联 iOS framework 与 FFmpeg 等依赖,以及处理演示所需的签名和编译设置。随后生成 Xcode 工程,把播放器目标加入示例 App 的依赖中,在熟悉的 IDE 里继续运行和调试。这一部分说明了从可复用核心到平台 App 的连接方式。

这也是他在搭工程时专门强调调试效率的原因:公共部分虽然用 C++,开发时仍要在 Xcode 里设置断点,观察 iOS 端行为。目录分层、交叉编译工具链和示例 App 的依赖关系,最终都要服务于这个实际工作过程。Android 移植仍需要对应的平台实现和构建环境,不能因为核心语言是 C++ 就认为已经完成双端交付。

最小播放器怎样划分组件

完整链路包含解封装、音视频解码、格式转换、渲染以及同步。教学项目为了控制规模,把解封装、解码和部分转换合到一个 Decoder 组件,再配合 Audio Render 和 Video Render。组件之间用队列缓冲数据,让生产与消费可以按各自节奏进行。

读取线程将解码结果分别放入音频帧和视频帧队列,渲染侧从各自队列取数据。即使读取某一段媒体暂时耗时,输出侧也不必立刻停下来等下一次读取;而两条输出链路最后仍要按时间信息对齐。莲叔把解封装、解码和转换合在一个 Decoder,是为了把教学示例控制在可讲清的规模,并非完整播放器只能这样拆类。

选择基础库时,莲叔比较了 FFmpeg 与 WebRTC 的侧重点。FFmpeg 提供广泛的媒体处理能力,WebRTC 更围绕实时通信及相关网络、音频处理需求展开;它们有交集,但不能只因为都能处理音视频就视作同一种工具。他因此在这个播放器示例中选择 FFmpeg。参见 FFmpeg 官方介绍,WebRTC 官方介绍

分享还区分了 FFmpeg 的命令行工具与开发库:ffprobe 用于查看媒体信息,ffplay 可以直接播放,ffmpeg 命令用于处理和转换;应用集成时则会用到 libavformat、libavcodec 等库。Tiny Player 主要借助前者处理封装,后者处理编解码,并使用 libswresample 转换音频格式;libswscale 等组件则承担图像转换等不同职责。

在比较 FFmpeg 与 WebRTC 时,他还补了一个转码例子:容器从 MKV 换成 MP4,和视频编码从 VP9 换成 H.264,是两个层面的转换。Tiny Player 要做的是读取本地媒体并输出音画,因此首先需要广泛的解封装和解码能力;实时通信中网络传输与实时音频处理的重要性,则是 WebRTC 的重点。选择库要回到当下播放器的任务。

用接口与线程模型明确需求

莲叔从使用方式倒推 Player 的设计。调用者能够设置媒体地址,执行 open、play、pause、stop 等操作,并获取一个平台视图放进自己的界面。示例 App 创建播放器、添加视图、设置本地 MP4,再打开并播放,这段调用流程就成为后续实现的目标。

内部有两类主动工作线程:读取线程负责解封装和解码,把结果放进音频帧、视频帧队列;视频渲染线程读取视频队列并输出画面。音频输出采用另一种节奏,iOS 的 Audio Unit 在需要数据时触发回调,播放器把相应样本填进去。它也涉及线程执行,只是回调的调度由音频系统驱动,不能理解为音频完全没有线程。

从 Packet 得到可播放的 Frame

Decoder 先定义自己的帧结构,记录媒体类型、数据、长度、位置和持续时间。视频帧还需要宽高与图像分量等信息,队列保存这些帧的引用。打开媒体时建立 FFmpeg 的相关上下文,之后不断读取解封装得到的 packet,依据所属流送给对应解码器。

这里的 packet 仍装着压缩数据,不能直接当作已经可显示的画面。演示使用 avcodec_send_packet 送入数据,再通过 avcodec_receive_frame 获取解码结果,把 FFmpeg 的帧转换为播放器自己的结构。视频这一路还会提取图像信息及用于渲染的数据,音频则在取得样本后继续做格式转换。

莲叔在演示中先从媒体文件持续读取 packet,根据 stream index 判断它属于音频还是视频,再交给相应的解码上下文。得到的 AVFrame 还不是 Tiny Player 自己的帧:他继续提取宽高、时长和位置等信息,整理图像分量,才将结果放进播放器队列。这个转换步骤让后面的渲染模块不必直接依赖 FFmpeg 的全部数据结构。

复现时需要特别注意,输入一个 packet 和输出一个 frame 并非固定一一对应。FFmpeg 官方说明,输出可能暂时没有,也可能有多个;还要处理返回状态,并在输入结束后取出解码器内部剩余数据。原片为讲清主线省略了一些分支,因此不能把展示的循环直接当作已经覆盖全部错误和结束状态的生产代码。FFmpeg 5.1 解码 API

音频转换则要匹配输出端需要的采样率、样本类型和声道布局。示例通过 swr_convert 等处理后再保存可播放的数据。libswresample 的能力包含重采样、样本格式及声道布局转换,这些参数需要成组核对,不能只保证某一个数值相同。参见 libswresample 文档

视频渲染怎样隔离平台细节

视频输出通过 PlayerView 一类抽象接口连接平台。在 iOS 实现中,Objective-C++ 文件既可以操作 C++ 对象,也可以调用平台的视图对象。公共播放器只使用准备、绑定和显示等接口,具体视图和图形环境的处理留在平台实现里。

平台桥接落在 Objective-C++ 的 .mm 文件里:C++ 的 PlayerView 实现持有平台视图,再把准备、绑定和显示等调用交给 iOS 对象。播放器的读取和同步逻辑因此只认识公共接口。莲叔没有把这种封装讲成性能捷径,它解决的是一份核心代码怎样清楚地调用不同系统能力。

原片采用 OpenGL 相关路径演示。准备阶段创建和关联渲染缓冲与帧缓冲,设置图形程序、顶点及纹理坐标,并放入 YUV 转 RGB 的 shader。每次渲染时,把示例采用的 Y、U、V 分量上传为纹理,绘制到目标缓冲,再通过平台层完成显示。这个过程把解码数据、GPU 处理与屏幕视图连起来,具体格式与图形 API 选择仍需结合目标平台。

音频回调里,不能丢掉半帧数据

Audio Unit 初始化时,输入格式要与前面转换后的 PCM 一致,包括样本表示及相关参数。原片举出的格式不匹配问题,说明不能依赖默认配置刚好符合解码结果。声道缓冲的数量、排列和填充,也必须与实际协商的格式对应;示例的简化填充不应当作所有立体声数据的通用写法。

回调需要多少数据,由当次请求决定,不一定刚好等于队列里一整帧。莲叔举例,一帧有 200 字节,这次只取了 80 字节,播放器必须保存当前帧和 80 字节的偏移;下次回调先读完剩余的 120 字节,才能进入后面的帧。队列负责跨帧缓存,当前帧与偏移负责一帧内部的连续读取。若每次回调都直接跳到新帧,尚未播放的样本就会丢失。

最后用同步把两条链路合起来

第一次把组件跑在一起,演示出现了视频已经结束、音频还在播放的现象。原因在于视频帧被过快消费,而音频输出有自身的采样与播放节奏。单纯把所有画面尽快送上屏,并不能得到正确的播放时间。

他用帧率解释最初的现象:同样三十张画面,如果按每秒三十帧显示,就是一秒;按每秒六十帧显示,只需半秒。演示的最初版本甚至没有控制显示帧率,视频线程会尽快渲染队列中的画面。声音却要按音频设备的节奏逐样本输出,所以画面先跑完并不奇怪。看到这个结果后,他才把音视频同步作为最后一个必须解决的问题引入。

莲叔介绍了以视频、音频或外部时钟作为同步参考的思路,这个示例选择音频时钟。视频领先时等待,落后较多时丢弃部分帧追赶,处在允许范围内则正常显示。原片用简单阈值演示,不代表所有播放器都应采用相同阈值或同一种主时钟。调整后,两条链路在演示中能够更接近地结束。

Tiny Player 的价值在于让关键处理环节运行起来。莲叔在结尾也说明,主要目的是展示音视频基础处理方式与重点;完整产品还会面对更多输入、设备和运行状态,不能把这次最小实现视作已经覆盖全部播放器需求。

成长对谈:兴趣、业务与技术深度怎样连接

从兴趣编程到创业,再到 UC

访谈开始时,莲叔补充,当时团队除了短视频,也承担信息流及直播等相关工作。他既与成员一起开发,也会在需求需要时补位。回顾学习经历,他从中学因兴趣接触编程说起,大学做过不少管理信息系统,也通过实验室项目接触多媒体;研究生及实习阶段更多接触服务端,离开学校创业后转入 iOS,并沿这个方向继续工作。

谈到对成长帮助最大的事情,他首先提到兴趣。早期学习时,他更关心能用工具创造什么,而不只是完成一道题。这让编程与想做的东西联系起来,形成持续尝试的动力。

其次是工作环境带来的变化。他回忆,创业时需要处理很多业务问题,资源有限,解决到能够支撑业务的程度往往比不断追求细节更紧迫;进入 UC 后,则有机会继续研究此前没有深入的部分。前一种经历扩展了解决问题的范围,后一种环境给了他深化技术的机会。原片用完成七成和后续三成作比喻,不是对创业项目质量的量化调查。

个人项目之外,也要在工作中寻找成长

第三点来自他对业余项目的观察。有人把兴趣完全寄托在 side project 上,觉得日常工作只是消耗;但业余能投入的时间有限,大量时间仍在公司项目中。他因此建议主动寻找工作与技术兴趣的连接,观察哪些业务问题值得深入,怎样在完成职责时也获得能力上的进展。

主持人进一步回应,这些问题有时也能发展为更系统的方案、工具或开源成果。两人的讨论强调从真实工作中发现机会,而非保证每一项工作都天然有趣。

怎样让学习持续发生

莲叔坦言,自己也会拖延,单靠下决心未必能坚持。他会把想学的事情转为明确任务,例如约定一次分享或一个交付时间,让进度有可以检查的结果。准备前会有压力,完成后通常能感受到整理过程带来的收获。

资料选择上,他提到会阅读 Medium 上自己关注方向的内容;面对较难、需要建立体系的知识,则更倾向于读书。零散文章可能各自讲得有道理,却不一定能组成适合初学者的完整路径。他在这里分享的是自己的学习偏好,不能仅凭平台或载体判断所有资料的质量。

主持人笑着追问,这次分享是不是也靠截止日期推动。莲叔承认,自己报名很积极,临近分享又会因准备工作感到压力;但真正讲完后,往往发现整理内容本身使自己学到了东西。他说的“用截止日期”是给容易拖延的学习任务设置一次需要拿出成果的机会。

技术专家的深度从哪里来

他提出先找到感兴趣的技术方向,或逐渐理解并喜欢正在做的工作。在此基础上,还要理解当前问题的全貌:上下游怎样协作,背后的原理是什么,业内同方向的人正在解决哪些难题。这并不等于必须掌握所有领域,而是为选择深入方向准备必要背景。

以 iOS 为例,先能把业务做好,再通过性能问题追查成因;随后了解同行遇到的问题及其解决方式,判断其中哪些也可能出现在自己的项目里。最终结合当前业务,选择最值得突破的一点。兴趣、领域理解与实际问题,在他的回答里共同决定投入方向。

怎样理解团队文化与招聘要求

关于 UC 的团队文化,莲叔回顾了自己经历的组织变化,以及平等、创新等共同表达。他同时对仅用文化口号解释公司成功保留意见,更关注一个组织长期形成的能力,例如其在工具和体验上的积累。这是他对所在环境的观察,不能替代对所有公司文化的结论。

现场谈到招聘时,他表示当时有名额,但招聘尚未恢复;这条信息只对应 2022 年的交流。对于候选人,他主要看计算机基础和项目经历:操作系统、网络以及数据结构等知识,能否在具体问题里体现理解,而不只是会答概念;例如有没有用空间换时间一类思路处理过实际困难。

他特别说明,数据结构不一定非要通过高难算法题检查。面试中聊一个候选人实际解决过的问题,追问何时用空间换时间、为什么选这个结构,也能看出基础知识是否进入了工程判断。项目经历则可以继续追问:体验做到了什么程度,架构是否只够当前需求,稳定性与音视频知识如何影响实现。

项目经历也不只用于确认做过什么。他想了解候选人会不会继续改善体验,是否能把方案做得更完整、更有体系,以及在稳定性或音视频等岗位相关方向上积累到了什么程度。项目背后的工作方法,比简单罗列项目名字更能帮助判断双方是否匹配。

选择下一家公司时,先看哪些问题

莲叔从行业和具体工作两个层面回答。一方面,了解行业是否仍有新增需求,还是需要在存量竞争中获得位置;另一方面,确认公司真正要做的技术工作,与自己的兴趣和能力是否匹配。有些人能在新领域逐渐培养兴趣,有些人更依赖已有兴趣,这也会影响选择。

原片用不同产业作了当时的例子,也用轻松的说法比较发展机会。文章保留这个检查问题的方式,不把某个行业写成只要加入就会获得回报的保证。

迷茫时,怎样形成自己的判断

创业结束后,他曾考虑是否继续做 iOS,担心方向的空间,也想过服务端。最终,一方面已有的 iOS 积累更深,另一方面自己对服务端的理解还有限,他选择继续走原来的方向。迷茫期间,他先努力把手头工作做好,在具体业务里重新发现可深入的问题,而不是只围绕抽象的职业标签打转。

主持人没有停在“坚持原方向”这个答案,又追问别人劝转行时该怎么办。莲叔区分了两种建议者:一位可能因为近期求职受挫而否定整个领域,另一位则可能经过多种项目与公司,仍认为方向有问题。听建议时不能只数赞成和反对的人数,而要理解他们各自经历了什么、推理链条是否适用于自己。

对于别人的建议,他提出一个进一步的问题:对方为什么会得出这个判断?它来自某段不顺利的经历,还是来自对领域较充分的了解?自己暂时没有答案时,可以研究建议的推理过程,再逐渐形成自己的判断。直接接受一个结论,与理解结论的依据,是不同的事情。

平台变化,会不会让积累归零

主持人把担忧进一步放到平台上:iOS 依赖苹果,如果平台格局变化,开发者会怎样?莲叔回应,长期解决移动端问题时,积累的不只是 Swift、Objective-C 或某个 SDK 的具体用法,还有性能优化、有限资源下的体验设计,以及处理业务问题的方法。

这些能力有机会迁移到其他平台,变化时仍需要学习新的接口与限制,却不能简单把以往所有经验都算作失效。他没有保证任何平台永远存在,而是提醒区分平台特有知识与能够带走的问题解决能力。

对移动开发与个人未来的安排

在当时的判断里,莲叔认为人们仍大量使用手机,移动开发并没有因出现新的热门方向而失去全部需求,只是寻找自身位置可能需要更多努力。这是 2022 年基于用户使用习惯的观察,不是当前就业形势报告。

个人规划则有两条线。面对职业阶段变化,他希望保持技术能力,同时从更完整的业务视角思考:如果由自己对这个业务负责,最需要解决什么?问题可能出在人员协作,也可能出在项目组织,于是相应补齐能力,让自己能为团队创造更多价值。他用 CEO 视角解释这种思考方式,重点是扩大对问题的责任范围,而不是要求每位开发者都去做 CEO。

另一条线仍然是兴趣。他希望在生活条件允许时持续写代码、研究新技术,做出让自己觉得有意思的东西。职业上的能力建设与个人探索,在他的设想里可以相互支持。对谈由此回到了最初的起点:愿意投入的兴趣,也需要通过真实问题和实际行动继续生长。

完整原片、分段信息可从第 8 期访谈页继续查看。

查看本期原视频与分段信息:我在 UC 做音视频 →

主题
音视频播放器移动开发T Chat