7、本地预览:SurfaceViewRenderer使用、本地视频流绑定与预览、切换前后摄像头
好,我们正式开始写代码了。
这一章,我们要做的是——把摄像头画面显示到手机屏幕上。听起来简单,对吧?但这里面有几个坑,我当年第一次做的时候全踩了一遍。今天咱们一个一个说清楚。
7.1 为什么用 SurfaceViewRenderer?
Android 上显示视频,常见的有 SurfaceView、TextureView、还有我们 WebRTC 专用的 SurfaceViewRenderer。
你可能会问:为什么不用普通的 SurfaceView?
嗯,这里有个关键点。WebRTC 的视频帧是 YUV 格式的,而且是从 native 层直接送过来的。如果你用普通的 View,你得先把 YUV 转成 Bitmap,再渲染到 Canvas 上。这一来一回,性能损耗非常大。我早期在项目里试过,720p 的视频就卡得不行。
SurfaceViewRenderer 是 WebRTC 官方封装好的组件。它内部直接对接了 native 层的渲染管线,说白了就是——数据从摄像头出来,经过编码、传输、解码,最后直接画到 Surface 上,中间没有多余的拷贝。效率极高。
7.2 布局中添加 SurfaceViewRenderer
首先,在布局文件里加上它。我个人习惯把它放在一个相对布局中,方便后续添加控制按钮。
<!-- activity_main.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/local_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
注意,这里用的是全限定名 org.webrtc.SurfaceViewRenderer,因为它是 WebRTC 库里的类,不是 Android SDK 自带的。
7.3 初始化 SurfaceViewRenderer
在 Activity 或 Fragment 的 onCreate 中,我们需要做几件事:
- 创建 PeerConnectionFactory(后面会细讲)
- 初始化 SurfaceViewRenderer
- 设置渲染器的缩放类型
代码大概长这样:
// 在 onCreate 中
SurfaceViewRenderer localSurfaceView = findViewById(R.id.local_surface_view);
// 初始化 EGL 上下文
EglBase eglBase = EglBase.create();
// 初始化渲染器
localSurfaceView.init(eglBase.getEglBaseContext(), null);
// 设置缩放模式:保持比例,裁剪多余部分
localSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
// 设置是否镜像(前置摄像头需要)
localSurfaceView.setMirror(true);
这里我解释一下几个关键点:
- EglBase:WebRTC 的 EGL 上下文管理类。所有渲染器共享同一个 EGL 上下文,这样纹理才能在不同组件间传递。
- ScalingType:
SCALE_ASPECT_FILL会裁剪边缘以填满整个 View,适合全屏预览;SCALE_ASPECT_FIT会保持完整画面,但可能有黑边。 - setMirror(true):前置摄像头默认是镜像的,如果不设置,你会看到自己的脸是反的。嗯,这个我当年就忘了,调试时总觉得哪里不对。
7.4 获取本地视频流并绑定
渲染器准备好了,接下来要获取摄像头数据。WebRTC 里通过 VideoCapturer 来采集视频,然后通过 VideoSource 把数据传给渲染器。
流程是这样的:
// 1. 创建 VideoCapturer(这里用 Camera2 采集)
VideoCapturer videoCapturer = createCameraCapturer(Camera2Enumerator.getInstance(context));
// 2. 通过 PeerConnectionFactory 创建 VideoSource
VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
// 3. 创建 VideoTrack
VideoTrack localVideoTrack = peerConnectionFactory.createVideoTrack("local_track", videoSource);
// 4. 把 VideoTrack 绑定到 SurfaceViewRenderer
localVideoTrack.addSink(localSurfaceView);
// 5. 开始采集
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
videoCapturer.startCapture(1280, 720, 30);
这里有个容易混淆的地方:addSink 和 setSink 的区别。我刚开始也搞混过。
- addSink:可以添加多个渲染器。比如本地预览一个,远端也看一个。
- setSink:只能设置一个,新的会覆盖旧的。
在本地预览场景下,用 addSink 更灵活。万一你后面想同时显示到两个 View 上呢?
7.5 切换前后摄像头
这个功能几乎是视频会议 App 的标配。WebRTC 的 CameraVideoCapturer 接口提供了 switchCamera 方法。
实现起来很简单:
// 假设 videoCapturer 是 CameraVideoCapturer 类型
if (videoCapturer instanceof CameraVideoCapturer) {
CameraVideoCapturer cameraCapturer = (CameraVideoCapturer) videoCapturer;
cameraCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() {
@Override
public void onCameraSwitchDone(boolean isFrontCamera) {
// 切换成功,更新 UI 状态
localSurfaceView.setMirror(isFrontCamera);
}
@Override
public void onCameraSwitchError(String errorDescription) {
// 切换失败,比如另一个摄像头被占用
Log.e(TAG, "切换摄像头失败: " + errorDescription);
}
});
}
注意几个细节:
- 切换后要重新设置
setMirror,因为后置摄像头不需要镜像。 - 切换过程中画面会短暂黑屏,这是正常的。如果你觉得体验不好,可以在切换前冻结最后一帧。
- 有些设备只有一个摄像头,调用
switchCamera会直接回调错误。
videoSource.adaptOutputFormat(...) 来调整输出格式。
7.6 完整流程的 SVG 示意图
下面这张图展示了从摄像头采集到画面显示在屏幕上的完整链路:
7.7 释放资源
最后,别忘了在 Activity 销毁时释放资源。WebRTC 的组件如果不手动释放,会导致内存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
// 停止采集
if (videoCapturer != null) {
try {
videoCapturer.stopCapture();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 释放渲染器
if (localSurfaceView != null) {
localSurfaceView.release();
}
// 释放 EGL 上下文(如果是单例,注意不要重复释放)
// eglBase.release();
}
这里有个细节:stopCapture 可能会抛出 InterruptedException,需要捕获处理。另外,释放顺序也很重要——先停止采集,再释放渲染器,最后释放 EGL 上下文。顺序反了可能会崩溃。
WebRTCUtils.release(),这样不容易遗漏。我在项目中就吃过这个亏,漏掉了某个渲染器的 release,结果内存占用一直降不下来。
7.8 本章小结
好了,这一章的内容就这些。我们做了三件事:
- 在布局中添加 SurfaceViewRenderer 并初始化
- 创建 VideoCapturer 和 VideoTrack,绑定到渲染器
- 实现前后摄像头切换
这些是 WebRTC 视频通话的基础。下一章我们会在此基础上,加入远端视频流的显示。到时候你会发现,本地预览和远端预览其实用的是同一套机制——都是 VideoTrack + SurfaceViewRenderer 的组合。
嗯,今天就到这里。代码写完了记得跑一下,看看你的脸是不是镜像的——如果是,说明你忘了调 setMirror(true)。
公众号:蓝海资料掘金营,微信deep3321