文章目录
- SurfaceViewRenderer
- [SurfaceEglRenderer / EglRenderer](#SurfaceEglRenderer / EglRenderer)
- [OpenGL\OpenGL ES\EGL](#OpenGL\OpenGL ES\EGL)
- renderFrameOnRenderThread
- VideoFrameDrawer
- 总结
在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的使用全都解耦了,每个类都分工明确,没有一丝冗余的功能,这些思路非常值得我们学习。