视频的推流大致可以分为采集、渲染、编码和发送四个过程。尽管本书侧重于网络相关的内容,也就是最后的发送过程,但我们依然有必要对整个推流过程有所了解。这也有助于我们给 WebRTC 添加滤镜等功能。

采集

采集的实现强烈依赖系统提供的相机 API,但渲染、编码和发送过程与则系统无关。由于笔者是 Android 开发,因此这部分内容将围绕 Android 展开(iOS 应该大同小异)。建议读者结合 mthli/YaaRTC 的源码阅读。

当我们需要开启摄像头时,可以调用 WebRTC 提供的 VideoCapturer,这个类(的具体实现)封装并统一了 Android Camera 与 Camera2 两套 API 的行为,其初始化方法的定义如下:

1
2
3
4
5
6
7
public interface VideoCapturer {
void initialize(
SurfaceTextureHelper surfaceTextureHelper,
Context applicationContext,
CapturerObserver capturerObserver);
// other definitions...
}

SurfaceTextureHelper 是封装了 SurfaceTexture 的工具类,SurfaceTexture 充当了相机数据的消费者。在 Camera2 API 中我们需要调用 CameraDevice.createCaptureSession(...) 开启摄像头,此时便需要传入与 SurfaceTexture 关联的 Surface 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private class CameraStateCallback extends CameraDevice.StateCallback {
// other definitions...

@Override
public void onOpened(CameraDevice camera) {
// method body...

surfaceTextureHelper.setTextureSize(captureFormat.width, captureFormat.height);
// highlight-next-line
surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
try {
camera.createCaptureSession(
Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler);
} catch (CameraAccessException e) {
// method body...
}
}

// other definitions...
}

摄像头会将自己捕捉到的数据输出到 Surface,我们可以简单将 Surface 理解为相机数据的生产者。这里附上一张来自 Google Grafika(一个 Android 多媒体的实验项目)的相机数据的传输路径图,可以很好地帮助我们理解 Surface 与 SurfaceTexture 的关系:

回顾上文,VideoCapturer 初始化时还需要传入 CapturerObserver,其定义如下:

1
2
3
4
5
public interface CapturerObserver {
// other definitions...
/** Delivers a captured frame. */
void onFrameCaptured(VideoFrame frame);
}

当 SurfaceTexture 的缓冲区 BufferQueue 中有可用的帧数据时,便会回调 SurfaceTexture.OnFrameAvailableListener ,并最终回调 CapturerObserver.onFrameCaptured(frame) 将帧数据通过 NativeAndroidVideoTrackSource 传递给 WebRTC Native 层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class VideoSource extends MediaSource {
// other definitions...

private final CapturerObserver capturerObserver = new CapturerObserver() {
// other definitions...

@Override
public void onFrameCaptured(VideoFrame frame) {
// body method...

VideoFrame adaptedFrame = VideoProcessor.applyFrameAdaptationParameters(frame, parameters);
if (adaptedFrame != null) {
// highlight-next-line
nativeAndroidVideoTrackSource.onFrameCaptured(adaptedFrame);
adaptedFrame.release();
}
}
};

// other definitions...
}

持续追踪 NativeAndroidVideoTrackSource 在 Native 层的调用栈,结果如下:

1
2
3
webrtc::jni::AndroidVideoTrackSource::onFrameCaptured
→ rtc::AdaptedVideoTrackSource::OnFrame
→ rtc::VideoBroadcaster::OnFrame

摄像头采集的视频帧数据会被传递到 VideoBroadcaster 这个类进行处理,而采集过程也到此为止。从 VideoBroadcaster 的名称就不难发现,帧数据接下来会被以广播的形式发送给各个订阅者,也就是说后续的渲染、编码(和发送)过程是并行处理的。

渲染

这里的「渲染」指的是预览画面。我们需要调用 VideoTrack.addSink(sink) 添加预览画面,sink 是实现了 VideoSink 这个接口的类,比如 SurfaceViewRenderer。

1
2
3
4
// Java version of rtc::VideoSinkInterface.
public interface VideoSink {
@CalledByNative void onFrame(VideoFrame frame);
}

当我们调用 VideoTrack.addSink(sink) 时,实际上是添加了一个 VideoBroadcaster 的订阅者。当有可用的帧数据时,VideoBroadcaster 便会回调 VideoSink.onFrame(frame) 。对于 SurfaceViewRenderer 来说,便是将这些帧数据渲染到了 EGL。

编码和发送

前面我们说到,只要实现了 VideoSink 并添加到 VideoBroadcaster 即可收到帧数据,编码和发送的过程也是类似的。这里我们直接给出调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
webrtc::VideoStreamEncoder::OnFrame
→ webrtc::VideoStreamEncoder::MaybeEncodeVideoFrame
→ webrtc::VideoStreamEncoder::EncodeVideoFrame
→ webrtc::LibvpxVp8Encoder::Encode #1
→ webrtc::LibvpxVp8Encoder::GetEncodedPartitions
→ webrtc::VideoStreamEncoder::OnEncodedImage
→ webrtc::internal::VideoSendStreamImpl::OnEncodedImage
→ webrtc::RtpVideoSender::OnEncodedImage
→ webrtc::RTPSenderVideo::SendEncodedImage
→ webrtc::RTPSenderVideo::SendVideo
→ webrtc::RTPSenderVideo::LogAndSendToNetwork
→ webrtc::RTPSender::EnqueuePackets
→ webrtc::PacedSender::EnqueuePackets
→ webrtc::PacingController::EnqueuePacket
→ webrtc::PacingController::EnqueuePacketInternal
→ webrtc::PacedSender::Process #2
→ webrtc::PacingController::ProcessPackets
→ webrtc::PacedSender::SendRtpPacket
→ webrtc::ModuleRtpRtcpImpl2::TrySendPacket #3
→ webrtc::RtpSenderEgress::SendPacket
→ webrtc::RtpSenderEgress::SendPacketToNetwork
→ cricket::WebRtcVideoChannel::SendRtp
→ cricket::MediaChannel::SendPacket
→ cricket::MediaChannel::DoSendPacket
→ cricket::VideoChannel::SendPacket
→ webrtc::DtlsSrtpTransport::SendRtpPacket #4

这里分别对调用栈中标记的序号做说明:

  1. 这里的编码器是 LibvpxVp8Encoder,但换成其他继承自 webrtc::VideoEncoder 的子类都是可以的,比如 VP9Encoder 或者 H264Encoder。
  2. RtpPacket 入队之后,将由 webrtc::ProcessThreadImpl::Process 进行处理,严格意义上已经不算是调用栈了,但读者也可以将其理解为 RtpPacket 的处理流程。
  3. webrtc::ModuleRtpRtcpImpl2 是 WebRTC M85 (branch-heads/4183) 版本的新实现。
  4. 从这里开始进入 PeerConnection 发送数据包的流程。

添加滤镜

由于需要实时预览滤镜效果,所以必须在渲染开始之前添加滤镜。好在 WebRTC 已经提供了 VideoProcessor 这个接口类,可以对采集到的帧数据进行预处理,调用 VideoSource.setVideoProcessor(processor) 即可设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class VideoSource extends MediaSource {
// other definitions...

private final CapturerObserver capturerObserver = new CapturerObserver() {
// other definitions...

@Override
public void onFrameCaptured(VideoFrame frame) {
final VideoProcessor.FrameAdaptationParameters parameters =
nativeAndroidVideoTrackSource.adaptFrame(frame);
synchronized (videoProcessorLock) {
if (videoProcessor != null) {
// highlight-next-line
videoProcessor.onFrameCaptured(frame, parameters);
return;
}
}

// body method...
}
};

// other definitions...
}

最后给出一个 VideoProcessor 的简单实现,给各位读者参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public final class Example implements VideoProcessor {

@Nullable private VideoSink mVideoSink;

@Override
public void onCapturerStarted(boolean success) {
// DO SOMETHING IF YOU WANT.
}

@Override
public void onCapturerStopped() {
// DO SOMETHING IF YOU WANT.
}

@Override
public void setSink(@Nullable VideoSink sink) {
// 需要持有 WebRTC 传入的 VideoSink 对象
mVideoSink = sink;
}

@Override
public void onFrameCaptured(@NonNull VideoFrame frame) {
VideoFrame newFrame = yourVideoFilter(frame);

// 会调用 NativeAndroidVideoTrackSource 将新的帧数据传递给 Native 层
if (mVideoSink != null) mVideoSink.onFrame(frame);
}
}