WebRTC 源码分析 (一) Android 相机采集


简介
在  WebRTC Android 中,已经兼容了 Camera 和 Camera2 原生 API 的相机采集,所以我们不必再单独实现一套采集功能。不过我们可以根据 RTC 的抽象 CameraCapturer 接口实现 CameraX (其实也是基于 Camera2的封装) 的相机采集,显然,这不是该篇的主题就不再多说了。该篇文章,主要为大家解析 WebRTC 的相机采集从  java 到 Jni 的一个调用过程。
先上一个整个调用的时序图,有条件的同学可以根据时序图来跟踪源码:
其实整个涉及到的文件还是比较多的
原图链接: http://devyk.top/2022/WebRTC相机采集流程.png

相机采集
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.ccvoid 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.javaprivate 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.ccstatic jlong JNI_VideoTrack_WrapSink(JNIEnv* jni,                                     const JavaParamRef<jobject>& sink) {  return jlongFromPointer(new VideoSinkWrapper(jni, sink));}
咦,我们发现上一小节的相机数据 native 处理好像也用到了 VideoSinkWrapper 类,我们继续看一下这个实例化对象做了什么
![image-20220904185544097](/Users/devyk/Library/Application Support/typora-user-images/image-20220904185544097.png)
通过上图标注 1 处,我们发现实例化该对象只是赋值了从 java 端传递过来的 ProxyVideoSink 。该 sink 就是接收采集到渲染的数据。
第二处就是执行在 ProxyVideoSink 中的 onFrame 函数,将 VideoFrame 给回调给 java 。所以这整个采集流程到渲染起始处都已经结合起来了,后面具体的渲染就是 SurfaceViewRenderer 了,通过 OpenGL ES 来渲染。就不详细来分析了。
实战 Demo
我把相机采集的代码从 WebRTC 中抽出来了,感兴趣的可以 clone 下来看看。
地址: https://github.com/yangkun19921001/WebRTCSample
总结
该篇文章详细的分析了 Camera1/2 的整个采集流程,有条件的建议 debug webrtc 源码来看。如果大家不会在 MACOS 下 debug webrtc 我后续可以出一篇如何在 MACOS 下搭建 webrtc 调试环境。
参考
《WebRTC Native 开发实战》
到顶部