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

发布于:2025-04-04 ⋅ 阅读:(30) ⋅ 点赞:(0)


在WebRTC链接建立成功后,如果想要将对方推送的视频流展示出来,需要调用 VideoTrack.addSink(SurfaceViewRenderer)实现。接下来我们将介绍在调用addSink后涉及到的类,以及WebRTC是如何将视频数据渲染到View。以下是整个调用流程图
在这里插入图片描述
首先我们来看看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
<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回调的。

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 <= 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的详细使用内容,我也不太熟悉,下面就简单看一些关键部分

@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的使用全都解耦了,每个类都分工明确,没有一丝冗余的功能,这些思路非常值得我们学习。