在 Unity 中使用 Android 侧提供的视频渲染相关的能力,有两种方案可选:

第一种是将渲染播放页单独做一个页面,在 Unity事件交互的时候打开对应 Activity 页面,或者获取到 Unity 创建的 Acitivity 动态添加 View。

第二种是只借助 Android 的渲染能力,将数据渲染到 Unity 的控件上。

两种方案各有优劣,第一种大大地减少了播放器相关的开发工作量,整个页面逻辑可以实现复用,但是交互页面的话 iOS/Android 需要写两套。第二种实现成本相对较高,但是交互可以由 Unity 侧进行,只是播放器使用封装好的 plugin 进行,能达到交互相对较统一,本文也主要是讲述该方案的实现。

Android 平台基本播放逻辑

在正式开发改造之前,对 Android 侧的一个播放器渲染流程进行简单的介绍,以 MediaPlayer 为例,利用 MediaPlayer 进行视频解码渲染,并将视频最后输出到 SurfaceView 上,一次播放器视频渲染到View上的的主要代码流程为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void initPlayer() {
MediaPlayer mediaPlayer = new MediaPlayer();
SurfaceView surfaceView = new SurfaceView(activity);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(^ {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Surface surface = holder.getSurface();
mediaPlayer.setSurface(surface);
mediaPlayer.prepareAsync();
}
……
});

mediaPlayer.setDataSource(URI...);
mediaPlayer.setOnPreparedListener(mp -> mp.start());
}

对于渲染 mediaPlayer.setSurface(surface) 设为播放器解码数据的接受器,Surface 来自于 SurfaceView。

播放器是将数据图形绘制在 Surface 对象上,Surface中会关联一个 BufferQueue 用于提供图像数据缓存,SurfaceFlinger 会把 Surface 对应的图像层混合在一起,将其输出到 FrameBuffer 中(Framebuffer就是一块内存区域,它通常是显示驱动的内部缓冲区在内存中的映射),最后在屏幕上看到合成的图像。

整个流程引入外部大佬的一张图所示:
https://juejin.cn/post/6993123390231937031#heading-24

Unity 中的一些改造

上面的流程最终是通过播放器解码渲染到 SurfaceView 上,当然,你可以通过获取到 UnityPlayer 对应的 Acitivity 将这个 SurfaceView 动态添加到当前界面,实现“在 Unity 中利用 Android 能力进行视频渲染”。

所以需要对其进行改造,我们的目的是实现 Android 播放器数据渲染到 Untiy 的组件中。实现这一过程需要借助 FBO(Frame Buffer Object) 的能力。

(一)FBO

在 OpenGL 渲染管线中几何数据和纹理经过变换和一些测试处理,最后以二维像素的形式显示在屏幕上。OpenGL管线的最终渲染目的地被称作帧缓存(framebuffer),OpenGL渲染管线的最终位置是在帧缓冲区中,默认情况下 OpenGL 使用的是窗口系统提供的帧缓冲区。

但有些场景是不想要直接渲染到窗口上的(例如加视频特效),于是 OpenGL 提供了一种方式来创建额外的帧缓冲区对象(FBO)。使用帧缓冲区对象,OpenGL 可以将原先绘制到窗口提供的帧缓冲区重定向到 FBO 之中。FBO本身不是一块内存,没有空间,真正存储东西,可实际读写的是依附于FBO的东西:纹理(texture)和渲染缓存(renderbuffer),依附的方式,是一个二维数组来管理,结构如图所示:

(二)具体实现

使用 FBO 我们可以将渲染目标渲染到其他的空间,我们目的是将播放器解码后的数据渲染到 Unity 控件的纹理空间中。
渲染播放器将输出到 FBO 中,FBO 指向 Unity 控件数据的输入,从而实现:Android 的播放器输出数据显示到 Unity 的控件中。

(三)从渲染输出数据到外部纹理

由于 mediaPlayer.setSurface(surface) 对应的 Surface 来源于 SurafaceView,会直接渲染到屏幕上,这里我们需要使用 构造一个新的 SurfaceTexture 以将图像流式传输到给定的 OpenGL 纹理;

要获取到播放器渲染得数据,需要借助 SurfaceTexture ,SurfaceTexture 是Surface 和 OpenGL ES 纹理的结合,其对图像流的处理并不直接显示,而是从图像流中捕获帧作为 OpenGL 的外部纹理,图像流来自相机预览和视频解码。

SurfaceTexture 创建的 Surface 是数据的生产者,而 SurfaceTexture 是对应的消费者,Surface 接收媒体数据并将数据发送到 SurfaceTexture,当调用 updateTexImage 的时候,创建SurfaceTexture 的纹理对象相应的内容将更新为最新图像帧,也就是会将图像帧转换为 GL 纹理,并将该纹理绑定到 GL_TEXTURE_EXTERNAL_OES 纹理对象上。具体实现逻辑参考:Android Opengl OES 纹理渲染到 GL_TEXTURE_2D

1
2
3
4
SurfaceTexture surfaceTexture = new SurfaceTexture(videoTextureId);
player.setUpSurface(new Surface(surfaceTexture), width, height);
surfaceTexture.setDefaultBufferSize(width, height);
surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {……});;

其中 videoTextureId 来源于创建的 OES 纹理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static int createOESTextureID() {
int[] texture = new int[1];
// 创建纹理对象,一个容器对象,保存渲染所需要的纹理数据,例如:图像数据
//在OpenGL 中纹理对象是一个无符号整数,是一个纹理对象的句柄
GLES30.glGenTextures(texture.length, texture, 0);

// 绑定纹理ID到纹理单元的纹理目标上
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);

// 设置纹理参数
……

GLES30.glGenerateMipmap(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
return texture[0];
}

(四)FBO纹理数据到 Unity 的纹理数据

学习了解到Unity中可以使用 RawImage 或者 quad 等相关控件可以显示纹理,这里以 RawImage 为例。在 Unity 脚本编写初始化的逻辑,构造一个 Texture2D 对象,将句柄传递到 Android,并赋值给 RawImage,并将texture id 传递到 Android 平台,完成一次渲染的重定向。

1
2
3
4
5
6
 void InitPlayer()
{
Texture2D texture2D = new Texture2D(width, height, TextureFormat.RGB24, false, false);
androidObj.Call("init", (int)texture2D.GetNativeTexturePtr(), width, height);
RawImage.texture = texture2D;
}

创建FBO

1
2
3
4
5
public static int createFBO() {
int[] fbo = new int[1];
GLES30.glGenFramebuffers(fbo.length, fbo, 0);
return fbo[0];
}

SurfaceTexture 设置了 OnFrameAvailableListener 后,当有新的图形流数据生成之后,就可以通过 mSurfaceTexture.updateTexImage() 将当前图片流更新到纹理所关联的OpenGLES中纹理,并绘制 FBO.

1
2
3
4
5
6
7
8
9
10
11
12
13
publc void draw() {
//1. 绑定 FrameBuffer 到当前的绘制环境上, 后续 GL 绘制都会到这个 framebuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo[0]);

//2.把一个2D纹理作为帧缓冲区附着
//即所有渲染操作的结果将会被储存在 unityTextureId 对应的纹理图像中
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, unityTextureId, 0);

//绑定指定纹理到当前激活的纹理单元
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureId);

//…… 省略 Opengl 绘制的常规流程
}

这一步是最关键的,实现了将 FBO 的输出指向 Unity 里面创建的纹理,也就实现了 Android 渲染与 Unity 之间的数据打通。

这里的 unityTextureId 来源于在 Unity 中初始化的 (int)texture2D.GetNativeTexturePtr()值。

整体的流程为:

效果图:

图片名称

图中播放视频区域为 Unity 的 RawImage 控件,渲染的视频通过 Pag 等相关素材由渲染SDK合成。

如图所示,视频画面正常地进行渲染,图中有两个区域展示了视频画面,上面的使用的 Quad 组件,下面是用的 RawImage,流程都一直,只是在 Unity 使用 Texture2D 的时候通过 Quad.mainTexture = texture2D 赋值。

总结

本文主要讲了 Unity 利用 Android 提供的能力进行视频相关的特效渲染的方案,总体正常运行。还需要一些优化,例如对 Multithreaded Rendering配置还未支持,以及一些逻辑可能受限于游戏侧的配置,例如图形渲染的配置使用的 OpenGL3.0,如果使用 OpenGL2.0 或者 Vulkan,还需要单独调整相关逻辑。