WebRTC源码解析:Android如何渲染画面

06-02 1112阅读

文章目录

  • SurfaceViewRenderer
  • SurfaceEglRenderer / EglRenderer
  • OpenGL\OpenGL ES\EGL
  • renderFrameOnRenderThread
  • VideoFrameDrawer
  • 总结

    在WebRTC链接建立成功后,如果想要将对方推送的视频流展示出来,需要调用 VideoTrack.addSink(SurfaceViewRenderer)实现。接下来我们将介绍在调用addSink后涉及到的类,以及WebRTC是如何将视频数据渲染到View。以下是整个调用流程图

    WebRTC源码解析:Android如何渲染画面

    首先我们来看看VideoSink接口的定义:

    /**
     * Java version of rtc::VideoSinkInterface.
     */
    public interface VideoSink {
      /**
       * Implementations should call frame.retain() if they need to hold a reference to the frame after
       * this function returns. Each call to retain() should be followed by a call to frame.release()
       * when the reference is no longer needed.
       */
      @CalledByNative void onFrame(VideoFrame frame);
    }
    

    从接口定义就可以猜到,WebRTC在Native部分将视频解码后会通过onFrame回调到Java层,这里我们不研究WebRTC是如何解码的,我们直接从拿到解码后的数据开始分析。

    SurfaceViewRenderer

    SurfaceViewRenderer是WebRTC提供的用于预览视频流的SurfaceView,它的使用方式如下:

    // activity.xml
    
        
    // MainActivity.kt
    VideoTrack.addSink(binding.surfaceView)
    

    根据SurfaceViewRenderer的定义,它继承VideoSink,所以第一步我们要看SurfaceViewRenderer是如何处理onFrame回调的。

    public class SurfaceViewRenderer extends SurfaceView implements SurfaceHolder.Callback, VideoSink, RendererCommon.RendererEvents {
        private final SurfaceEglRenderer eglRenderer;
        
        @Override
        public void onFrame(VideoFrame frame) {
            eglRenderer.onFrame(frame);
        }
    }
    

    当SurfaceViewRenderer收到onFrame回调时,会直接交给SurfaceEglRenderer处理。

    为什么还需要SurfaceViewRenderer呢,既然这里什么操作都没有直接给SurfaceEglRenderer处理,直接使用SurfaceEglRenderer不就好了?

    答案在SurfaceViewRenderer的定义中,SurfaceViewRenderer是继承SurfaceView并实现了SurfaceHolder.Callback接口的,说明它既能作为View直接渲染,同时还对Surface的生命周期进行了处理。

    @Override
    public void surfaceCreated(final SurfaceHolder holder) {
      ThreadUtils.checkIsOnMainThread();
      surfaceWidth = surfaceHeight = 0;
      updateSurfaceSize();
    }
    

    从这部分代码可以看出来,SurfaceViewRenderer的主要功能是对Surface宽高等属性进行设置,实际的渲染都交给了SurfaceEglRenderer,那么我们继续看SurfaceEglRenderer做了什么。

    SurfaceEglRenderer / EglRenderer

    首先我们看SurfaceEglRenderer的定义

    public class SurfaceEglRenderer extends EglRenderer implements SurfaceHolder.Callback {
    public class EglRenderer implements VideoSink {
    

    SurfaceEglRenderer 直接继承EglRenderer并实现SurfaceHolder.Callback接口,这里我们把EglRenderer的定义也列出来,它直接实现VideoSink。所以从这个定义我们就可以分析出,EglRenderer是最纯粹的处理视频数据的地方,而SurfaceEglRenderer增加了对Surface生命周期管理的包装。我们看看源码是否正确。

    //SurfaceEglRenderer.java
    // 1. surface创建时
    @Override
    public void surfaceCreated(final SurfaceHolder holder) {
      ThreadUtils.checkIsOnMainThread();
      createEglSurface(holder.getSurface());
    }
    // 以下代码来自:EglRenderer.java
    public void createEglSurface(Surface surface) {
      createEglSurfaceInternal(surface);
    }
    private final EglSurfaceCreation eglSurfaceCreationRunnable = new EglSurfaceCreation();
    // 2. 异步处理surface
    private void createEglSurfaceInternal(Object surface) {
      eglSurfaceCreationRunnable.setSurface(surface);
      postToRenderThread(eglSurfaceCreationRunnable);
    }
    private class EglSurfaceCreation implements Runnable {
      private Object surface;
      // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
      @SuppressWarnings("NoSynchronizedMethodCheck")
      public synchronized void setSurface(Object surface) {
        this.surface = surface;
      }
      // 3. 核心处理surface的地方
      @Override
      @SuppressWarnings("NoSynchronizedMethodCheck")
      public synchronized void run() {
        if (surface != null && eglBase != null && !eglBase.hasSurface()) {
          if (surface instanceof Surface) {
            eglBase.createSurface((Surface) surface);
          } else if (surface instanceof SurfaceTexture) {
            eglBase.createSurface((SurfaceTexture) surface);
          } else {
            throw new IllegalStateException("Invalid surface: " + surface);
          }
          eglBase.makeCurrent();
          // Necessary for YUV frames with odd width.
          GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
        }
      }
    }
    

    SurfaceEglRenderer对Surface的处理方式是异步的将其设置给eglBase,EGL的内容我们会在后面介绍。我们再来看对视频帧的处理

    // SurfaceEglRenderer.java
    // VideoSink interface.
    @Override
    public void onFrame(VideoFrame frame) {
      // 1. 对onFrame进行简单的事件上报,包括(初次收到frame,frame分辨率变化)
      // 随后交由EglRenderer处理
      updateFrameDimensionsAndReportEvents(frame);
      super.onFrame(frame);
    }
    // EglRenderer.java
    @Override
    public void onFrame(VideoFrame frame) {
      synchronized (statisticsLock) {
        ++framesReceived;
      }
      final boolean dropOldFrame;
      synchronized (handlerLock) {
        if (renderThreadHandler == null) {
          logD("Dropping frame - Not initialized or already released.");
          return;
        }
        synchronized (frameLock) {
          dropOldFrame = (pendingFrame != null);
          if (dropOldFrame) {
            pendingFrame.release();
          }
          // pendingFrame 存放当前正在处理的frame
          pendingFrame = frame;
          // 引用计数+1,以防被回收
          pendingFrame.retain();
          // 异步处理 frame
          // 疑问点1 :
          renderThreadHandler.post(this::renderFrameOnRenderThread);
        }
      }
      if (dropOldFrame) {
        synchronized (statisticsLock) {
          ++framesDropped;
        }
      }
    }
    

    可以看到SurfaceEglRenderer并不参与视频渲染的工作,而是做简单的事件上报,例如首次收到视频帧,视频帧分辨率变化,这些事件回调定义在RendererCommon.RendererEvents。真正处理frame的是EglRenderer,它会使用pendingFrame保存最新收到的frame,随后交给hander异步处理。

    这里有一个疑问(以上代码注释中疑问点1):如果处理frame处理速度远低于frame到来的速度,那么会不断的调用handler.post并在handler积累runnable,这样不会有性能问题吗?

    通过查询我得到的答案是不会,原因有几点,首先是pendingFrame的使用,当新的frame来临时,会先判断pendingFrame是否不为空,也就是是否有未处理的帧,如果有未处理的帧,会直接通过pendingFrame.release()抛弃掉,并将最新的frame赋值给pendingFrame。这样无论frame来的多快,永远只处理最新的frame。其次是renderFrameOnRenderThread方法中,当pendingFrame为空时会立马return,即使handler中post了过多的runnable,也会是空执行。

    接下来就该看renderFrameOnRenderThread它是如何处理视频帧了,在此之前我觉得应该补充一下前文提到的EGL了。

    OpenGL\OpenGL ES\EGL

    随着计算机图形学的发展,越来越多的 2D 和 3D 图形的绘制需求产生,然而直接操作 GPU 进行绘制对于开发者来说过于复杂。因此,出现了一系列绘制 API,其中就包括 OpenGL。OpenGL 是一个跨平台、开源的 2D 和 3D 图形绘制 API。而 OpenGL ES 是 OpenGL 的精简版,它主要应用在一些系统资源受限的嵌入式设备,例如手机、游戏主机等。

    由于 OpenGL 仅是一个图形渲染绘制接口,它并不包括将 GPU 渲染结果显示到显示器的过程。这种设计的原因是,首先 OpenGL 作为跨平台接口,不同操作系统有不同的窗口管理机制;其次,这样可以解耦窗口管理和图形渲染。所以,想让 OpenGL 能够在不同系统间运行,还需要依赖例如 WGL(Windows)、GLX(Linux)、CGL(macOS)、EGL(Android)等用于衔接 OpenGL 和原生窗口的 API。

    简单来说,OpenGL 和 OpenGL ES 负责绘制图形,而 EGL 等则负责将图形显示到屏幕上。

    以 Android 系统为例,完整的工作流程如下:

    1.创建原生窗口(Surface)

    2.初始化 EGL,并将其和 Surface 绑定

    3.通过 OpenGL 发送渲染指令

    4.GPU 接收后进行渲染绘制等操作,绘制结果保存在 GPU 缓冲区

    5.通过 EGL 将 GPU 缓冲区数据交给窗口系统

    6.窗口系统进行刷新

    renderFrameOnRenderThread

    了解完OpenGL相关内容后,我们继续回到WebRTC源码

    private void renderFrameOnRenderThread() {
      //从pendingFrame中读取最新视频帧
      final VideoFrame frame;
      synchronized (frameLock) {
        if (pendingFrame == null) {
          return;
        }
        frame = pendingFrame;
        pendingFrame = null;
      }
      // 异常判断
      if (eglBase == null || !eglBase.hasSurface()) {
        frame.release();
        return;
      }
      // 如果设置了fps,做帧率控制,
      final boolean shouldRenderFrame;
      synchronized (fpsReductionLock) {
        if (minRenderPeriodNs == Long.MAX_VALUE) {
          // Rendering is paused.
          shouldRenderFrame = false;
        } else if (minRenderPeriodNs 
          // FPS reduction is disabled.
          shouldRenderFrame = true;
        } else {
          final long currentTimeNs = System.nanoTime();
          if (currentTimeNs 
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码