2.1 分层架构与数据通路


文档摘要

2.1 分层架构与数据通路 有了第二章开篇的分层地图,本节把地图上一条最重要的路走一遍:一帧画面从应用调用发送接口,到被编码、打包、送进网络,途经的每一站。这条路走通了,后面的媒体章节其实都只是对其中某一段的放大。 接口与实现的分离纪律 引擎最外层的接口层是给嵌入方看的稳定门面:连接工厂、连接本体、收发器、轨道句柄,全都以纯虚接口形式发布。门面之下才是实现层——同名的实现类持有全部状态与逻辑。这么做的直接收益是 ABI 稳定:嵌入方只依赖头文件里的抽象形状,引擎内部怎么重构都不破坏宿主的编译。对读源码的人来说,这个纪律给了一个实用的检索技巧:看到接口类,找它的实现类;实现类里往往有一个对应的工厂或创建函数,顺着创建函数就能把对象的出生过程看全。

2.1 分层架构与数据通路

有了第二章开篇的分层地图,本节把地图上一条最重要的路走一遍:一帧画面从应用调用发送接口,到被编码、打包、送进网络,途经的每一站。这条路走通了,后面的媒体章节其实都只是对其中某一段的放大。

接口与实现的分离纪律

引擎最外层的接口层是给嵌入方看的稳定门面:连接工厂、连接本体、收发器、轨道句柄,全都以纯虚接口形式发布。门面之下才是实现层——同名的实现类持有全部状态与逻辑。这么做的直接收益是 ABI 稳定:嵌入方只依赖头文件里的抽象形状,引擎内部怎么重构都不破坏宿主的编译。对读源码的人来说,这个纪律给了一个实用的检索技巧:看到接口类,找它的实现类;实现类里往往有一个对应的工厂或创建函数,顺着创建函数就能把对象的出生过程看全。

接口层的另一个设计要点是观察者模式的全覆盖:连接状态变化、媒体到达、统计更新,全部通过观察者回调外送,引擎绝不主动持有宿主的业务对象。嵌入代码因此只需要回答"我关心哪些事件、回调里做什么",而事件从底层冒泡上来的路径,恰好就是本节要走的发送链的镜像。

发送侧调用链现场

下面用一次真实的源码走读演示"接口调用如何落进实现层"。以最常用的轨道发送为例,应用侧代码通常长这样:

rtc::scoped_refptr<webrtc::AudioTrackInterface> track = factory->CreateAudioTrack("audio0", source); rtc::scoped_refptr<webrtc::RtpSenderInterface> sender = pc->AddTrack(track, {"stream0"}).MoveValue();

这两行看似平淡,背后是一条相当纵深的链。AddTrack 首先在连接层确认协商状态允许追加媒体段落,随后创建或复用一个收发器,把方向改为发送;收发器内部再实例化发送句柄,把轨道挂上去。真正的媒体通路在呼叫层成形:呼叫对象收到"新增音频发送流"的指令,创建音频发送流实体;实体向引擎适配层申请编码通道,适配层把请求落到模块层的编码器实例上。此后每当采集端有新帧产生,它沿着"源到汇"的方向流经处理管线,编码完成后经打包器切成分片,进入发送队列,由平滑发送器按拥塞控制给出的节奏送进传输通道。

链上每一站都值得停一下,但本节先只提炼三个结构性观察。第一,协商与数据面是分离的:AddTrack 时媒体还没真正流动,要等协商完成后发送流才被激活,所以调试时"轨道加了但没流"要第一反应去看协商结果,而不是怀疑数据面。第二,呼叫层是音视频的汇合点:音频流与视频流是两个独立的发送实体,但共享同一个呼叫对象的带宽预算与传输通道,第七章的带宽分配就发生在这个汇合点上。第三,底座层的任务队列是链条的隐形粘合剂,每一站的移交都伴随一次跨线程投递,这正是下一节的主题。

案例:用断点把整条链走出来

背景。团队一位同事要给发送链路加自定义遥测,但拿不准挂载点该选在哪一层,凭感觉在模块层打了补丁,结果统计出来的帧数与浏览器面板显示的发送帧数对不上。

操作。我们改用实证的办法:在调试版上,沿调用链从接口层开始逐站下断点,每断一处记录线程名与关键参数。应用调用处断在信令线程;发送流创建处断在工作线程;帧到达编码器入口处断在编码器所属的任务队列;打包完成处断在负责发送节奏的任务队列。同时打开信息级日志,把每站断点的时刻与日志时间对齐。

结果。链路每经过一次线程投递,日志里的线程标签就切换一次,整条链在四个执行载体上接力完成。更关键的发现是:模块层打包器看到的帧包含重传与冗余封装的中间形态,而接口层统计的是业务语义的帧,二者本来就不是一个口径。

解读。同事最初的错位由此解释通了:他统计的是中间形态,面板展示的是终端口径,两边都对,只是层选错了。这个案例的教训值得写成规矩——加观测点之前,先回答"我要的口径定义在哪一层",然后到那一层去挂点。层级选对,遥测自然对得上。

变式。接收侧的链是这条链的镜像:网络线程收包、解包后投递工作线程解码、解码帧再送到渲染出口。用同样的断点方法走一遍接收链,你会发现方向相反但纪律相同,且抖动缓冲插在解包与解码之间——这正是第五章接收端处理的位置预告。

层边界的自查清单

嵌入开发里最伤元气的事故,几乎都源于越层调用——为了省事直接抓住下层的实现对象操作,短期是快,版本一升级就是连坐式崩溃。把分层的纪律落成一份自查清单,接入评审时逐条过:

检查项 越界的样子 正确姿势
获取对象 拿到接口指针后向下强转实现类 只用接口声明的方法
跨层调用 绕过连接层直接操纵发送流 经接口层发起请求
线程假设 在宿主线程直调引擎内部对象 投递到对象所属线程
数据持有 缓存引擎传出的帧引用长期使用 回调内用完即还
回调耗时 在引擎回调里做重活或等待 记录后投递回宿主队列

清单里"缓存帧引用"一条值得展开:引擎对媒体缓冲实行池化管理,回调里交到你手上的帧是借出的,用完归还进池子供下游复用;你若长期攥着不放,池子会持续扩容,表现为内存缓慢爬升且不回落。这类泄漏在压测里表现为"内存稳步上涨无平台期",是内存排查里最容易被误判成真正泄漏的一类。

另一条高频越界是"向下强转"。接口层的对象以抽象指针交付,有人为了调用某个实现类独有的方法而强转——编译能过,但引擎任何一次内部重构都可能让这个假设失效,而且失效形式是未定义行为而非干净报错。真需要实现类特有能力时,正路是向上提需求:接口层为这类能力留出扩展点,或者走本节案例演示过的旁路挂接。

要点回顾

本节的要点收拢成几句:接口层与实现层分离,找实现先找创建函数;发送链从连接层登记开始,经呼叫层成形,到模块层落地,线程随投递切换;协商面与数据面分离,"加了轨道没流量"先查协商;呼叫层是带宽分配的汇合点;观测点必须与口径定义的层级对齐。下一节我们把粘合这条链的线程与任务队列拆开细看。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U