简介
在 WebRTC Android 中,已经兼容了 Camera 和 Camera2 原生 API 的相机采集,所以我们不必再单独实现一套采集功能。不过我们可以根据 RTC 的抽象 CameraCapturer 接口实现 CameraX (其实也是基于 Camera2的封装) 的相机采集,显然,这不是该篇的主题就不再多说了。该篇文章,主要为大家解析 WebRTC 的相机采集从 java 到 Jni 的一个调用过程。
先上一个整个调用的时序图,有条件的同学可以根据时序图来跟踪源码:
其实整个涉及到的文件还是比较多的
相机采集
WebRTC Android 中定义了一个抽象的视频采集接口为 VideoCapturer , 内部定义了 初始化(SurfaceTexture 渲染帮助类 、相机采集 VideoFrame 回调 ) 、**开始/结束/释放 ** 等采集生命周期 API, 最终实现有 Camera1Capturer 和 Camera2Capturer 2 个子类。
为了有一个更清晰的一个结构,下面可以看一下 VideoCapturer 类图:
下面我们分别来介绍 webrtc 是如何来对 Camera1/2 进行创建并使用的,首先我们先看下上面的类图, 最顶层是 VideoCapturer 抽象接口
// Base interface for all VideoCapturers to implement.
public interface VideoCapturer {
void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext,
CapturerObserver capturerObserver);
void startCapture(int width, int height, int framerate);
void stopCapture() throws InterruptedException;
void changeCaptureFormat(int width, int height, int framerate);
void dispose();
boolean isScreencast();
}
它的直接实现为 CameraVideoCapturer 然后它具体的实现是一个抽象 CameraCapturer , 该抽象类封装了 Camera1/2 公共部分,当调用 startCapture 接口,内部实现了 createCameraSession 抽象函数,以供 Camera1/2 各自进行创建 Session,可以看下下面代码的调用:
abstract class CameraCapturer implements CameraVideoCapturer {
...
@Override
public void startCapture(int width, int height, int framerate) {
...
createSessionInternal(0);
}
private void createSessionInternal(int delayMs) {
cameraThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
createCameraSession(createSessionCallback, cameraSessionEventsHandler, applicationContext,
surfaceHelper, cameraName, width, height, framerate);
}
}, delayMs);
}
//Camera 1 and Camera1 实现
abstract protected void createCameraSession(CameraSession.CreateSessionCallback createSessionCallback, CameraSession.Events events,
Context applicationContext, SurfaceTextureHelper surfaceTextureHelper, String cameraName,
int width, int height, int framerate);
}
可以看到最终抽象的实现就是最上面类图中的 Camera1Capturer 和 Camera2Capturer ,我们还是分别进行分析。
Camera 1
如果最上层使用 Camera1 进行 startCapture, 最终将执行到如下代码:
public Camera1Capturer(
@Override
protected void createCameraSession(CameraSession.CreateSessionCallback createSessionCallback,
CameraSession.Events events, Context applicationContext,
SurfaceTextureHelper surfaceTextureHelper, String cameraName, int width, int height,
int framerate) {
Camera1Session.create(createSessionCallback, events, captureToTexture, applicationContext,surfaceTextureHelper, Camera1Enumerator.getCameraIndex(cameraName), width, height,framerate);
}
}
可以看到函数体中具体是 Camera1Session#create 的调用,我们跟进去看一下
class Camera1Session implements CameraSession {
...
public static void create(final CreateSessionCallback callback, final Events events,
final boolean captureToTexture, final Context applicationContext,
final SurfaceTextureHelper surfaceTextureHelper, final int cameraId, final int width,
final int height, final int framerate) {
final long constructionTimeNs = System.nanoTime();
...
final android.hardware.Camera camera;
camera = android.hardware.Camera.open(cameraId);
camera.setPreviewTexture(surfaceTextureHelper.getSurfaceTexture());
final android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
final CaptureFormat captureFormat;
final android.hardware.Camera.Parameters parameters = camera.getParameters();
captureFormat = findClosestCaptureFormat(parameters, width, height, framerate);
final Size pictureSize = findClosestPictureSize(parameters, width, height);
updateCameraParameters(camera, parameters, captureFormat, pictureSize, captureToTexture);
if (!captureToTexture) {
final int frameSize = captureFormat.frameSize();
for (int i = 0; i < NUMBER_OF_CAPTURE_BUFFERS; ++i) {
final ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize);
camera.addCallbackBuffer(buffer.array());
}
}
// Calculate orientation manually and send it as CVO insted.
camera.setDisplayOrientation(0 /* degrees */);
//内部会设置采集的 NV21 or OES 回调
callback.onDone(new Camera1Session(events, captureToTexture, applicationContext,
surfaceTextureHelper, cameraId, camera, info, captureFormat, constructionTimeNs));
}
}
可以看到这一步就是通过 Camera.open 打开摄像头,并进行帧率、采集格式、预览等一些基础设置.
private Camera1Session(...) {
...
surfaceTextureHelper.setTextureSize(captureFormat.width, captureFormat.height);
startCapturing();
}
当实例化 Camera1Session 时,会首先设置 surfaceTexture 的缓冲大小,也就是分辨率,最后是调用 this.startCapturing()
在 startCapturing 实现体中,调用了 camera 的预览函数,我们一起来看下
private void startCapturing() {
Logging.d(TAG, "Start capturing");
checkIsOnCameraThread();
state = SessionState.RUNNING;
...
if (captureToTexture) {
//采集输出 OES 纹理
listenForTextureFrames();
} else {
//采集输出 YUV420sp(NV21)buf
listenForBytebufferFrames();
}
try {
camera.startPreview();
} catch (RuntimeException e) {
stopInternal();
events.onCameraError(this, e.getMessage());
}
}
这里我们统一只介绍采集 OES 纹理,在 listenForTextureFrames 函数中实现了采集的 frame 回调
private void listenForTextureFrames() {
surfaceTextureHelper.startListening((VideoFrame frame) -> {
checkIsOnCameraThread();
...
final VideoFrame modifiedFrame = new VideoFrame(
CameraSession.createTextureBufferWithModifiedTransformMatrix(
(TextureBufferImpl) frame.getBuffer(),
/* mirror= */ info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT,
/* rotation= */ 0),
/* rotation= */ getFrameOrientation(), frame.getTimestampNs());
events.onFrameCaptured(Camera1Session.this, modifiedFrame);
modifiedFrame.release();
});
}
当接收到采集后的数据,会调用 events.onFrameCaptured 函数,后面的处理就会在 native 中了,会单独进行分析。
Camera2
Camera2 API 是 Android API-22 加入的,目的是为了支持更加复杂的相机使用场景,因此它的 API 使用也相较于 Camera1 复杂一些。
如果最上层使用 Camera2 进行 startCapture, 那么它的代码调用如下:
@TargetApi(21)
public class Camera2Capturer extends CameraCapturer {
...
@Override
protected void createCameraSession(CameraSession.CreateSessionCallback createSessionCallback,
CameraSession.Events events, Context applicationContext,
SurfaceTextureHelper surfaceTextureHelper, String cameraName, int width, int height,
int framerate) {
Camera2Session.create(createSessionCallback, events, applicationContext, cameraManager,
surfaceTextureHelper, cameraName, width, height, framerate);
}
}
这里与 Camera1 调用类似,具体 Camera2 使用也是在 CameraSeesion 中,代码如下:
class Camera2Session implements CameraSession {
public static void create(CreateSessionCallback callback, Events events,
Context applicationContext, CameraManager cameraManager,
SurfaceTextureHelper surfaceTextureHelper, String cameraId, int width, int height,int framerate) {
new Camera2Session(callback, events, applicationContext, cameraManager, surfaceTextureHelper,cameraId, width, height, framerate);
}
private Camera2Session(...)
{
start();
}
private void start() {
checkIsOnCameraThread();
Logging.d(TAG, "start");
findCaptureFormat();
openCamera();
}
}
上面 start 主要是找到相机的采集格式,比如找到采集的 fps 范围,采集的分辨率等,还有采集的颜色格式(统一是 NV21),最后是通过 openCamera 打开摄像头
@SuppressLint("MissingPermission")
private void openCamera() {
checkIsOnCameraThread();
Logging.d(TAG, "Opening camera " + cameraId);
events.onCameraOpening();
try {
cameraManager.openCamera(cameraId, new CameraStateCallback(), cameraThreadHandler);
} catch (CameraAccessException e) {
reportError("Failed to open camera: " + e);
return;
}
}
通过 mCameraManager#openCamera 传入摄像头的 id 和 Camera 状态回调。调用之后,成功与失败就会执行到该接口中
public static abstract class StateCallback {
public abstract void onOpened(@NonNull CameraDevice camera);
public void onClosed(@NonNull CameraDevice camera);
public abstract void onDisconnected(@NonNull CameraDevice camera);
public abstract void onError(@NonNull CameraDevice camera,
@ErrorCode int error);
}
当打开成功就会执行到 #onOpened 函数中,我们看下具体打开成功后的操作
@Override
public void onOpened(CameraDevice camera) {
checkIsOnCameraThread();
Logging.d(TAG, "Camera opened.");
cameraDevice = camera;
surfaceTextureHelper.setTextureSize(captureFormat.width, captureFormat.height);
surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
try {
camera.createCaptureSession(
Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler);
} catch (CameraAccessException e) {
reportError("Failed to create capture session. " + e);
return;
}
}
可以看到,在该函数中主要设置启动预览,用的是 SurfaceTexture 进行接收数据,当调用 camera.createCaptureSession 之后会将创建的状态回调给 CameraCaptureSession.StateCallback 的子类,也就是第二个参数 new CaptureSessionCallback(), 当创建成功之后,会执行 onConfigured 回调,最后会在该回调中进行设置帧率,和关联 surface 和设置采集的回调(OES)。有一点需要注意一下,采集的宽高不是直接设置给 CameraDevice 的,而是设置给 SurfaceTexture 的。
public void onConfigured(CameraCaptureSession session) {
....
surfaceTextureHelper.startListening((VideoFrame frame) -> {
...
//将采集到的相机 OES 纹理回调出去
events.onFrameCaptured(Camera2Session.this, modifiedFrame);
...
}
....
}
这里之后,Camera1 和 Camera2 的处理流程是一样的了,下面会统一介绍。
到这里主要采集的工作已经完成了,我们总结下 Camera1和Camera2 的流程吧:
Camera1:
- 通过 Camera.open 实例化
- 通过 camera.setPreviewTexture 设置预览 SurfaceTexture , 主要是用来接收帧数据.
- 通过 camera.setParameters 设置相机预览的参数,比如帧率、分辨率等
- 通过 camera.setDisplayOrientation 设置预览的方向
- 通过 camera.startPreview/stopPreview/release 设置预览的生命周期
Camera2:
- 通过 getSystemService(Context.CAMERA_SERVICE) 创建相机管理类
- 通过 mCameraManager.openCamera 创建 CameraDevice ,并设置创建的状态回调
- 通过 CameraDevice.StateCallback#onOpened 接收创建成功的回调,通过 camera.createCaptureSession 开启预览的 session,并设置 create 的回调。
- 通过 CameraCaptureSession.StateCallback#onConfigured 来接收上一步设置的session 状态回调,最后是通过 session.setRepeatingRequest 来设置采集的数据格式
- 通过 cameraCaptureSession.stop 和 cameraDevice.close 来停止 Camera2 的预览
相机数据 native 处理
上一小节我们分析到了 events.onFrameCaptured(Camera2Session.this, modifiedFrame); 该回调会执行到
CameraCapturer#onFrameCaptured 函数中,最后调用 capturerObserver.onFrameCaptured(frame); 会执行到 VideoSource#onFrameCaptured 函数,如下代码所示:
//CameraCapturer.java
@Override
public void onFrameCaptured(CameraSession session, VideoFrame frame) {
checkIsOnCameraThread();
synchronized (stateLock) {
...
capturerObserver.onFrameCaptured(frame);
}
}
//VideoSource.java
@Override
public void onFrameCaptured(VideoFrame frame) {
final VideoProcessor.FrameAdaptationParameters parameters =
nativeAndroidVideoTrackSource.adaptFrame(frame);
synchronized (videoProcessorLock) {
if (videoProcessor != null) {
videoProcessor.onFrameCaptured(frame, parameters);
return;
}
}
VideoFrame adaptedFrame = VideoProcessor.applyFrameAdaptationParameters(frame, parameters);
if (adaptedFrame != null) {
nativeAndroidVideoTrackSource.onFrameCaptured(adaptedFrame);
adaptedFrame.release();
}
}
//NativeAndroidVideoTrackSource.java
private static native void nativeOnFrameCaptured(
long nativeAndroidVideoTrackSource, int rotation, long timestampNs, VideoFrame.Buffer buffer);
到这里,就会将采集到的数据 VideoFrame 包装类,传递到 NativeAndroidVideoTrackSource_jni.h 代码中, 接着会把 nativeAndroidVideoTrackSource 指针地址转为 Native 端的 AndroidVideoTrackSource class,再调用内部的 OnFrameCaptured 函数,详细代码如下:
JNI_GENERATOR_EXPORT void Java_org_webrtc_NativeAndroidVideoTrackSource_nativeOnFrameCaptured(
JNIEnv* env,
jclass jcaller,
jlong nativeAndroidVideoTrackSource,
jint rotation,
jlong timestampNs,
jobject buffer) {
AndroidVideoTrackSource* native =
reinterpret_cast<AndroidVideoTrackSource*>(nativeAndroidVideoTrackSource);
CHECK_NATIVE_PTR(env, jcaller, native, "OnFrameCaptured");
return native->OnFrameCaptured(env, rotation, timestampNs,
base::android::JavaParamRef<jobject>(env, buffer));
}
当执行到内部的 OnFrameCaptured 函数,代码如下:
void AndroidVideoTrackSource::OnFrameCaptured(
JNIEnv* env,
jint j_rotation,
jlong j_timestamp_ns,
const JavaRef<jobject>& j_video_frame_buffer) {
rtc::scoped_refptr<VideoFrameBuffer> buffer =
AndroidVideoBuffer::Create(env, j_video_frame_buffer);
//转为 c++ 枚举
const VideoRotation rotation = jintToVideoRotation(j_rotation);
// AdaptedVideoTrackSource handles applying rotation for I420 frames.
//这里主要是处理 I420 数据的旋转
if (apply_rotation() && rotation != kVideoRotation_0)
buffer = buffer->ToI420();
//调用当前的 OnFrame 函数
OnFrame(VideoFrame::Builder()
.set_video_frame_buffer(buffer)
.set_rotation(rotation)
.set_timestamp_us(j_timestamp_ns / rtc::kNumNanosecsPerMicrosec)
.build());
}
调用 OnFrame 后的代码
void AdaptedVideoTrackSource::OnFrame(const webrtc::VideoFrame& frame) {
rtc::scoped_refptr<webrtc::VideoFrameBuffer> buffer(
frame.video_frame_buffer());
if (apply_rotation() && frame.rotation() != webrtc::kVideoRotation_0 &&
buffer->type() == webrtc::VideoFrameBuffer::Type::kI420) {
/* Apply pending rotation. */
webrtc::VideoFrame rotated_frame(frame);
//内部调用 libyuv 进行处理旋转
rotated_frame.set_video_frame_buffer(
webrtc::I420Buffer::Rotate(*buffer->GetI420(), frame.rotation()));
rotated_frame.set_rotation(webrtc::kVideoRotation_0);
broadcaster_.OnFrame(rotated_frame);
} else {
broadcaster_.OnFrame(frame);
}
}
因为我们这里是 OES 纹理,并且也不会处理旋转,所以直接走 else ,VideoBroadcaster#OnFrame 函数,在这里会遍历 std::vector ,然后调用对应 sink 的 OnFrame 进行处理
void VideoBroadcaster::OnFrame(const webrtc::VideoFrame& frame) {
rtc::CritScope cs(&sinks_and_wants_lock_);
bool current_frame_was_discarded = false;
for (auto& sink_pair : sink_pairs()) {
...
if (sink_pair.wants.black_frames) {
webrtc::VideoFrame black_frame =
webrtc::VideoFrame::Builder()
.set_video_frame_buffer(
GetBlackFrameBuffer(frame.width(), frame.height()))
.set_rotation(frame.rotation())
.set_timestamp_us(frame.timestamp_us())
.set_id(frame.id())
.build();
sink_pair.sink->OnFrame(black_frame);
} else if (!previous_frame_sent_to_all_sinks_ && frame.has_update_rect()) {
webrtc::VideoFrame copy = frame;
copy.clear_update_rect();
sink_pair.sink->OnFrame(copy);
} else {
sink_pair.sink->OnFrame(frame);
}
}
}
通过 debug 我们可以知道,内部有 2 个 sink,一个是 VideoSinkWrapper 另一个是 VideoStreamEncoder
该篇不会涉及编码相关介绍,所以先忽略这个指针吧
我们跟进 VideoSinkWrapper#OnFrame 去看看
#video_sink.cc
void VideoSinkWrapper::OnFrame(const VideoFrame& frame) {
JNIEnv* jni = AttachCurrentThreadIfNeeded();
//1. 主要将 Native 中的 VideoFrame 实例化为 Java 端的 VideoFrame
ScopedJavaLocalRef<jobject> j_frame = NativeToJavaVideoFrame(jni, frame);
//2.
Java_VideoSink_onFrame(jni, j_sink_, j_frame);
//3.
ReleaseJavaVideoFrame(jni, j_frame);
}
我们直接看第二步把,这一步其实就是把 VideoFrame 通过 JNIEnv#CallVoidMethod 回调给 Java 端,代码如下
static void Java_VideoSink_onFrame(JNIEnv* env, const base::android::JavaRef<jobject>& obj, const
base::android::JavaRef<jobject>& frame) {
//拿到 "org/webrtc/VideoSink" Java的 claszz
jclass clazz = org_webrtc_VideoSink_clazz(env);
CHECK_CLAZZ(env, obj.obj(),
org_webrtc_VideoSink_clazz(env));
jni_generator::JniJavaCallContextChecked call_context;
call_context.Init<
base::android::MethodID::TYPE_INSTANCE>(
env,
clazz,
"onFrame",
"(Lorg/webrtc/VideoFrame;)V",
&g_org_webrtc_VideoSink_onFrame);
env->CallVoidMethod(obj.obj(),
call_context.base.method_id, frame.obj());
}
看到 CallVoidMethod 是不是感觉特亲切了,它的第一个参数就是回调给 java 具体的对象,第二个参数就是回调给对象中的具体函数,第三个传参的数据类型。
void CallVoidMethod(jobject obj, jmethodID methodID, ...)
第一个参数 obj 我们预览的时候再进行说明,第二个参数可以通过上面的代码得知,它是 onFrame 函数,参数签名为 "(Lorg/webrtc/VideoFrame;)V" ,后面传递的参数就是 VideoFrame。
到这里 jni 处理采集到的相机数据就处理完了,其实采集 java -> jni -> java 中间部分就通过 libyuv处理了下 I420 的旋转,如果不处理旋转的话,就直接再回调给 java 即可。
相机数据预览
上一小节我们留了一个问题,就是 env->CallVoidMethod(obj.obj(), call_context.base.method_id, frame.obj()); 的第一个参数这个 obj 具体是哪个类的,我们先来到 CallActivity.java 的 onConnectedToRoomInternal 函数这个函数的回调就是代表 WebSocket 连接成功并且进入到了视频通话房间中。我们具体看下该函数的实现:
//CallActivity.java
private final ProxyVideoSink remoteProxyRenderer = new ProxyVideoSink();
private final ProxyVideoSink localProxyVideoSink = new ProxyVideoSink();
private final List<VideoSink> remoteSinks = new ArrayList<>();
remoteSinks.add(remoteProxyRenderer);
private static class ProxyVideoSink implements VideoSink {
private VideoSink target;
@Override
synchronized public void onFrame(VideoFrame frame) {
if (target == null) {
Logging.d(TAG, "Dropping frame in proxy because target is null.");
return;
}
//本地纹理
target.onFrame(frame);
}
synchronized public void setTarget(VideoSink target) {
this.target = target;
}
}
private void onConnectedToRoomInternal(final SignalingParameters params) {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
signalingParameters = params;
logAndToast("Creating peer connection, delay=" + delta + "ms");
VideoCapturer videoCapturer = null;
if (peerConnectionParameters.videoCallEnabled) {
//第一步
videoCapturer = createVideoCapturer();
}
//第二步
peerConnectionClient.createPeerConnection(
localProxyVideoSink, remoteSinks, videoCapturer, signalingParameters);
...
}
P2P 建联媒体协商,这里就不再说明了,因为不是该篇的内容。我们先看第一步,可以发现 createVideoCapturer 返回值就是 VideoCapturer 相机采集的基类,跟第一小节的 VideoCapturer 给对应上了,接着往下看,这里会调用 PeerConnectionClient#createPeerConnection 函数,会把本地的 VideoSink和远端的 VideoSink 给传递过去,具体代码如下:
//PeerConnectionClient.java
public void createPeerConnection(final VideoSink localRender, final List<VideoSink> remoteSinks,
final VideoCapturer videoCapturer, final SignalingParameters signalingParameters) {
...
this.localRender = localRender;
this.remoteSinks = remoteSinks;
this.videoCapturer = videoCapturer;
executor.execute(() -> {
try {
...
createPeerConnectionInternal();
...
} catch (Exception e) {
reportError("Failed to create peer connection: " + e.getMessage());
throw e;
}
});
...
}
我们只分析关键位置,继续执行到 createPeerConnectionInternal 函数内部中
//PeerConnectionClient.java
private void createPeerConnectionInternal() {
...
if (isVideoCallEnabled()) {
peerConnection.addTrack(createVideoTrack(videoCapturer), mediaStreamLabels);
...
}
...
}
已经快接近真像了,再坚持一下. 我们先看一下 createVideoTrack 具体实现,代码如下:
//PeerConnectionClient.java
@Nullable
private VideoTrack createVideoTrack(VideoCapturer capturer) {
//第一步
surfaceTextureHelper =
SurfaceTextureHelper.create("CaptureThread", rootEglBase.getEglBaseContext());
videoSource = factory.createVideoSource(capturer.isScreencast());
//第二步
capturer.initialize(surfaceTextureHelper, appContext, videoSource.getCapturerObserver());
capturer.startCapture(videoWidth, videoHeight, videoFps);
localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
localVideoTrack.setEnabled(renderVideo);
//第三步
localVideoTrack.addSink(localRender);
return localVideoTrack;
}
这个函数代码比较核心,我们分为 3 个步骤进行分析
先来看第一步,首先是通过 SurfaceTextureHelper.create 创建一个 SurfaceTextureHelper 。在第一小节我们已经发现它的影子了,当时没有介绍它。就是等这一刻来介绍。
当调用 create 的时候,内部会进行实例化该对象,最终会执行到构造函数,我们看下具体实现,代码如下:
private SurfaceTextureHelper(Context sharedContext, Handler handler, boolean alignTimestamps,
YuvConverter yuvConverter, FrameRefMonitor frameRefMonitor) {
//判断线程
if (handler.getLooper().getThread() != Thread.currentThread()) {
...
}
...
//在 CaptureThread 线程中创建 EGL 环境
eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
try {
eglBase.createDummyPbufferSurface();
eglBase.makeCurrent();
} catch (RuntimeException e) {
eglBase.release();
handler.getLooper().quit();
throw e;
}
//OES 纹理id 生成的核心代码
oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
//创建 SurfaceTexture 对象,传入纹理 id
surfaceTexture = new SurfaceTexture(oesTextureId);
//设置渲染的回调,调用 updateTexImage 执行
setOnFrameAvailableListener(surfaceTexture, (SurfaceTexture st) -> {
hasPendingTexture = true;
//相机机数据可用时就会进行回调,然后就将采集到数据回调给 设置 SurfaceTextureHelper#startListening 处
tryDeliverTextureFrame();
}, handler);
}
@TargetApi(21)
private static void setOnFrameAvailableListener(SurfaceTexture surfaceTexture,
SurfaceTexture.OnFrameAvailableListener listener, Handler handler) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
surfaceTexture.setOnFrameAvailableListener(listener, handler);
} else {
surfaceTexture.setOnFrameAvailableListener(listener);
}
}
总结一下上面的代码作用,首先是创建 EGL 环境,然后通过 GL ES 创建一个 OES ID 纹理,将这个纹理传递给创建好的 SurfaceTexture 对象,最后是设置采集到的帧回调,如果有数据就会回调给如下代码:
surfaceTextureHelper.startListening((VideoFrame frame) -> {
...
events.onFrameCaptured(Camera1Session.this, modifiedFrame);
...
}
该处代码也与上一小节给对应上了。
我们继续看 createVideoTrack 代码的第二部分:
首先调用 VideoCapturer#initialize 初始化函数,其次是调用 VideoCapturer#startCapture 开始采集的函数,该处也与上一小节给对应上了。
我们来看 createVideoTrack 代码的第三部分:
我们传入了 本地预览的 VideoSink
localVideoTrack.addSink(localRender);
继续跟一下 addSink,
//VideoTrack.java
public void addSink(VideoSink sink) {
...
if (!sinks.containsKey(sink)) {
//1.
final long nativeSink = nativeWrapSink(sink);
sinks.put(sink, nativeSink);
//2.
nativeAddSink(getNativeMediaStreamTrack(), nativeSink);
}
}
第一步是一个 native 函数,它的实现在 VideoTrack_jni.h 处,我们跟一下代码
JNI_GENERATOR_EXPORT jlong Java_org_webrtc_VideoTrack_nativeWrapSink(
JNIEnv* env,
jclass jcaller,
jobject sink) {
return JNI_VideoTrack_WrapSink(env, base::android::JavaParamRef<jobject>(env, sink));
}
继续跟
//video_track.cc
static jlong JNI_VideoTrack_WrapSink(JNIEnv* jni,
const JavaParamRef<jobject>& sink) {
return jlongFromPointer(new VideoSinkWrapper(jni, sink));
}
咦,我们发现上一小节的相机数据 native 处理好像也用到了 VideoSinkWrapper 类,我们继续看一下这个实例化对象做了什么

通过上图标注 1 处,我们发现实例化该对象只是赋值了从 java 端传递过来的 ProxyVideoSink 。该 sink 就是接收采集到渲染的数据。
第二处就是执行在 ProxyVideoSink 中的 onFrame 函数,将 VideoFrame 给回调给 java 。所以这整个采集流程到渲染起始处都已经结合起来了,后面具体的渲染就是 SurfaceViewRenderer 了,通过 OpenGL ES 来渲染。就不详细来分析了。
实战 Demo
我把相机采集的代码从 WebRTC 中抽出来了,感兴趣的可以 clone 下来看看。
总结
该篇文章详细的分析了 Camera1/2 的整个采集流程,有条件的建议 debug webrtc 源码来看。如果大家不会在 MACOS 下 debug webrtc 我后续可以出一篇如何在 MACOS 下搭建 webrtc 调试环境。
参考
- 《WebRTC Native 开发实战》