【AndroidRTC-11】如何理解webrtc的Source、TrackSink
Android-RTC系列软重启,改变以往细读源代码的方式 改为 带上实际问题分析代码。增加实用性,方便形成肌肉记忆。同时不分种类、不分难易程度,在线征集问题切入点。
问题1:如何理解VideoSource、VideoTrack&VideoSink三者的关系?它们只能是1v1v1的关系吗?
问题2:有前三者,还有一个MediaStream,这又有什么作用?
答1:在WebRTC中,VideoSource、VideoSink和VideoTrack三者构成了视频数据的生产、传输和消费链路,其关系可概括如下:
组件 | 角色 | 功能 |
---|---|---|
VideoSource | 数据生产者 | 生成原始视频帧(如摄像头、屏幕捕获、文件解码器)。 |
VideoTrack | 数据通道与管理者 | 将VideoSource的数据封装为媒体轨道,管理数据的传输和分发。 |
VideoSink | 数据消费者 | 接收并处理视频帧(如渲染到屏幕、编码发送、保存到文件等)。 |
三者构成 “1:N:M” 的拓扑结构:
1个VideoTrack 必须关联 1个VideoSource,强制一对一,VideoTrack与VideoSource必须一一绑定,无法跨源管理。
1个VideoTrack 可以分发到 多个VideoSink,灵活一对多,VideoTrack可分发到多个VideoSink,支持复杂业务场景。
1个VideoSource 可以被 多个VideoTrack 共享,多源共存方案,通过创建多个 VideoTrack 实现多源混合,并通过 MediaStream 统一管理。
代码逻辑示例:
**(1) 创建VideoTrack并绑定Source**
// 创建摄像头VideoSource
rtc::scoped_refptr<VideoCaptureModule> capture_module = ...;
std::unique_ptr<VideoSource> video_source(new VideoSource(capture_module));
// 创建VideoTrack并绑定Source
rtc::scoped_refptr<VideoTrackInterface> video_track =
peer_connection_factory->CreateVideoTrack("camera_track", video_source.get());
**(2) 添加VideoSink渲染画面**
class VideoRenderer : public rtc::VideoSinkInterface<VideoFrame> {
public:
void OnFrame(const VideoFrame& frame) override {
// 渲染帧到屏幕
RenderFrameToScreen(frame);
}
};
// 将渲染器注册为VideoSink
VideoRenderer renderer;
video_track->AddOrUpdateSink(&renderer, rtc::VideoSinkWants());
// 移除VideoSink
video_track->RemoveSink(&renderer);
以上基础知识来自 腾讯元宝版的DeepSeek
至于第二个问题,不太好说明白。但基于从问题出发,我们还是看看MediaStream有什么内容。
/** Java wrapper for a C++ MediaStreamInterface. */
public class MediaStream {
private static final String TAG = "MediaStream";
public final List<AudioTrack> audioTracks = new ArrayList<>();
public final List<VideoTrack> videoTracks = new ArrayList<>();
public final List<VideoTrack> preservedVideoTracks = new ArrayList<>();
private long nativeStream;
@CalledByNative
public MediaStream(long nativeStream) {
this.nativeStream = nativeStream;
}
}
代码很简单,就是三个Track列表,看到一句关键的注释 Java wrapper for a C++ MediaStreamInterface. 我们不妨再去看看MediaStreamInterface。
// C++ version of https://www.w3.org/TR/mediacapture-streams/#mediastream.
//
// A major difference is that remote audio/video tracks (received by a
// PeerConnection/RtpReceiver) are not synchronized simply by adding them to
// the same stream; a session description with the correct "a=msid" attributes
// must be pushed down.
//
// Thus, this interface acts as simply a container for tracks.
class MediaStreamInterface : public webrtc::RefCountInterface,
public NotifierInterface {
... ...
}
注释翻译:https://www.w3.org/TR/mediacapture-streams/#mediastream.的C++版本实现。一个主要区别是,远程音频/视频轨道(由PeerConnection的RtpReceiver接收)不是简单地通过将它们添加到同一流中来同步的;必须向下推送具有正确“a=msid”属性的会话描述。因此,此接口仅充当轨道的容器。
大致意思应该是,MediaStreamInterface这个类只是一个简单的包装器,把同一(msid)会话的音频视频轨包装在一起。
我们再深挖一下MediaStream的引用地方,也就是pc/peer_connection.cc
看到这就很关键的 RTC_CHECK( ! IsUnifiedPlan()),原来这个MediaStream是旧标准的接口,这下就好理解了。
再提 PlanB and UnifiedPlan
在前一篇文章中,我们简单提过《PlanB and UnifiedPlan》 其核心差异体现在媒体流(Track)的表示方式、m-line(媒体行)数量、SSRC关联逻辑等方面。
PlanB 和 UnifiedPlan 其实就是 WebRTC 在多路媒体源(multi media source)场景下的两种不同的 SDP 协商方式。如果引入 Stream 和 Track 的概念,那么一个 Stream 可能包含 AudioTrack 和 VideoTrack,当有多路 Stream 时,就会有更多的 Track,如果每一个 Track 唯一对应一个自己的 M 描述,那么这就是 UnifiedPlan,如果每一个 M line 描述了多个 Track(track id),那么这就是 Plan B。
Plan B的SDP片段:同一行 m-line下两个SSRC流都用着VP8编码参数。
m=video 9 UDP/TLS/RTP/SAVPF 96 97
a=ssrc:1234 cname:stream1
a=ssrc:5678 cname:stream2
a=sendrecv
a=rtpmap:96 VP8/90000
a=fmtp:96 max-fs=12288;max-fr=60
Unified Plan的SDP片段:每个m-line独立配置编码格式,通过mid标识不同Track。
m=video 9 UDP/TLS/RTP/SAVPF 96
a=sendonly
a=mid:video1
a=rtpmap:96 VP8/90000
m=video 9 UDP/TLS/RTP/SAVPF 97
a=recvonly
a=mid:video2
a=rtpmap:97 H264/90000
Note: 当只有一路音频流和一路视频流时,Plan B 和 UnifiedPlan 的格式是相互兼容的。
Note: 如何快速判断is_unified_plan_?直接看m=video/m=audio的行数吧。
借用大佬的两张图直观分析。

