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

文章目录

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

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

java 复制代码
/**
 * 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,它的使用方式如下:

java 复制代码
// activity.xml
<org.webrtc.SurfaceViewRenderer
    android:id="@+id/surfaceView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    
// MainActivity.kt
VideoTrack.addSink(binding.surfaceView)

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

java 复制代码
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的生命周期进行了处理。

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

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

SurfaceEglRenderer / EglRenderer

首先我们看SurfaceEglRenderer的定义

java 复制代码
public class SurfaceEglRenderer extends EglRenderer implements SurfaceHolder.Callback {

public class EglRenderer implements VideoSink {

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

java 复制代码
//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的内容我们会在后面介绍。我们再来看对视频帧的处理

java 复制代码
// 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源码

java 复制代码
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 <= 0) {
      // FPS reduction is disabled.
      shouldRenderFrame = true;
    } else {
      final long currentTimeNs = System.nanoTime();
      if (currentTimeNs < nextFrameTimeNs) {
        logD("Skipping frame rendering - fps reduction is active.");
        shouldRenderFrame = false;
      } else {
        nextFrameTimeNs += minRenderPeriodNs;
        // The time for the next frame should always be in the future.
        nextFrameTimeNs = Math.max(nextFrameTimeNs, currentTimeNs);
        shouldRenderFrame = true;
      }
    }
  }

  final long startTimeNs = System.nanoTime();

  // 计算原始帧宽高比
  final float frameAspectRatio = frame.getRotatedWidth() / (float) frame.getRotatedHeight();
  // 如果用户自定义了宽高比,就用用户自定义的
  final float drawnAspectRatio;
  synchronized (layoutLock) {
    drawnAspectRatio = layoutAspectRatio != 0f ? layoutAspectRatio : frameAspectRatio;
  }
  
  // 根据原始宽高比和用户自定义宽高比,计算缩放率
  final float scaleX;
  final float scaleY;

  if (frameAspectRatio > drawnAspectRatio) {
    scaleX = drawnAspectRatio / frameAspectRatio;
    scaleY = 1f;
  } else {
    scaleX = 1f;
    scaleY = frameAspectRatio / drawnAspectRatio;
  }
  // 准备好变换矩阵,主要操作为镜像、缩放
  drawMatrix.reset();
  drawMatrix.preTranslate(0.5f, 0.5f);
  drawMatrix.preScale(mirrorHorizontally ? -1f : 1f, mirrorVertically ? -1f : 1f);
  drawMatrix.preScale(scaleX, scaleY);
  drawMatrix.preTranslate(-0.5f, -0.5f);

  try {
    if (shouldRenderFrame) {
      //OpenGL 清理画布
      GLES20.glClearColor(0 /* red */, 0 /* green */, 0 /* blue */, 0 /* alpha */);
      GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

      int drawWidth = eglBase.surfaceWidth();
      int drawHeight = eglBase.surfaceHeight();
      if (eglBase.surfaceWidth() <= 1080){
        drawWidth = 2304;
        drawHeight = 1294;
      }
      // 绘制
      frameDrawer.drawFrame(frame, drawer, drawMatrix, 0, 0,
              drawWidth, drawHeight);

      // swap显示到屏幕
      final long swapBuffersStartTimeNs = System.nanoTime();
      if (usePresentationTimeStamp) {
        eglBase.swapBuffers(frame.getTimestampNs());
      } else {
        eglBase.swapBuffers();
      }

      final long currentTimeNs = System.nanoTime();
      synchronized (statisticsLock) {
        ++framesRendered;
        renderTimeNs += (currentTimeNs - startTimeNs);
        renderSwapBufferTimeNs += (currentTimeNs - swapBuffersStartTimeNs);
      }
    }

    notifyCallbacks(frame, shouldRenderFrame);
  } catch (GlUtil.GlOutOfMemoryException e) {
    logE("Error while drawing frame", e);
    final ErrorCallback errorCallback = this.errorCallback;
    if (errorCallback != null) {
      errorCallback.onGlOutOfMemory();
    }
    // Attempt to free up some resources.
    drawer.release();
    frameDrawer.release();
    bitmapTextureFramebuffer.release();
    // Continue here on purpose and retry again for next frame. In worst case, this is a continous
    // problem and no more frames will be drawn.
  } finally {
    frame.release();
  }
}

可以看到renderFrameOnRenderThread主要做了绘制前的准备工作,包括清理画布、生成变化矩阵,最后由frameDrawer.drawFrame进行绘制,并通过eglBase.swapBuffers将GPU缓存交换到surface上进行显示

VideoFrameDrawer

在VideoFrameDrawer的drawFrame方法中,再次进行矩阵变换后,最终交由GlGenericDrawer.drawOes进行最后的处理。从这部分的源码可以看到非常多的OpenGL相关操作的内容。OpenGL的详细使用内容,我也不太熟悉,下面就简单看一些关键部分

java 复制代码
@Override
public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight,
    int viewportX, int viewportY, int viewportWidth, int viewportHeight) {
  //准备工作,初始化GlShader
  prepareShader(
      ShaderType.OES, texMatrix, frameWidth, frameHeight, viewportWidth, viewportHeight);
  // 激活纹理区域
  GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
  // 绑定纹理,GL_TEXTURE_EXTERNAL_OES
  // 专门用于渲染来自摄像头、视频解码器或其他硬件加速源的图像数据
  GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId);
  // 定义渲染区域(视口)的位置和大小
  GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight);
  // 绘制一个四边形(2个三角形组成),将纹理渲染到视口中。
  GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
  // 解绑纹理
  GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}

总结

以上就是WebRTC收到视频帧以后的整个绘制流程,其中关于OpenGL、EGL相关的初始化,都在Surface创建的时候进行初始化的,有兴趣的也可以从源码中很清晰的看到。从源码中,我们可以看到Surface、EGL、OpenGL的使用全都解耦了,每个类都分工明确,没有一丝冗余的功能,这些思路非常值得我们学习。

相关推荐
〆、风神29 分钟前
EasyExcel 数据字典转换器实战:注解驱动设计
android·java·注解
stevenzqzq1 小时前
Android studio xml布局预览中 Automotive和Autotive Distant Display的区别
android·xml·android studio
QING6182 小时前
Kotlin commonPrefixWith用法及代码示例
android·kotlin·源码阅读
QING6182 小时前
Kotlin groupByTo用法及代码示例
android·kotlin·源码阅读
兰琛7 小时前
Compose组件转换XML布局
android·xml·kotlin
水w8 小时前
【Android Studio】解决报错问题Algorithm HmacPBESHA256 not available
android·开发语言·android studio
隐-梵10 小时前
Android studio进阶教程之(二)--如何导入高德地图
android·ide·android studio
Kika写代码11 小时前
【Android】界面布局-线性布局LinearLayout-例子
android·gitee
wangz7611 小时前
kotlin,jetpack compose,使用DataStore保存数据,让程序下次启动时自动获取
android·kotlin·datastore·jetpack compose