码字不易,转载请注明出处喔
https://blog.csdn.net/newchenxf/article/details/119565585
1. 前言
Unity本身可以直接播放本地视频或网络视频,网上有很多例子,例如:
https://blog.csdn.net/cs874300/article/details/89294433
这可以完全在Unity封闭完成。
但是,有一种情况,是播放逻辑希望在Android的代码内完成(比如是直播的流,用的是android 的直播SDK),希望Unity只渲染Android播放的画面就可以。这种情况怎么做呢?本文一一说来。
首先,完成上面的工作,需要依赖FBO,所以先介绍FBO。
1.1 FBO简介
FBO,全名Frame Buffer Object,目前主要用于离屏渲染技术
。
在OpenGL渲染管线中几何数据和纹理经过变换和一些测试处理,最终会被展示到屏幕上。OpenGL渲染管线的最终位置是在帧缓冲区
中。默认情况下OpenGL使用的是窗口系统
提供的帧缓冲区
。
但总有场景是不想要直接渲染到窗口上的,于是OpenGL提供了一种方式来创建额外的帧缓冲区对象(FBO)。使用帧缓冲区对象,OpenGL可以将原先绘制到窗口提供的帧缓冲区重定向到FBO之中。
FBO本身不是一块内存,没有空间
,真正存储东西,可实际读写的是依附于FBO的东西:纹理(texture)和渲染缓存(renderbuffer)
。
依附的方式,是一个二维数组(或者说是一个映射表)来管理。
简单的说,FBO为了管理这2个东东,于是用一些标签来表示Texture Object或Renderbuffer Object,例如颜色缓冲区(GL_COLOR_ATTACHMENT0)、深度缓冲区(GL_DEPTH_ATTACHMENT)、模板缓冲区以及累积缓冲区。(这些都是int值)
以GL_COLOR_ATTCHMENT0
为例子,一个绑定代码如下:
//1. 绑定FrameBuffer到当前的绘制环境上, 后续GL绘制都会到这个framebuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);
//2. Framebuffer绑定纹理
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, unityTextureId, 0);
也就是,把一个纹理id叫unityTextureId
的纹理,绑定到FBO的GL_COLOR_ATTACHMENT0
标签上。
有了FBO,程序员就可以重定向渲染目标到其他的存储空间,比如将渲染目标重定向到纹理空间,实现渲染到纹理功能(Render to Texture)
,这也是本文的目标!
上面的绑定代码,就是你可以自己创建一个纹理,把ID给FBO绑定,后面渲染的结果,就到你要的纹理上了!完美,真香
2. 正式开发
分两部分,一个是Unity部分,一个是Android部分。
2.1 Unity的工作
先Canvas下建立一个RawImage对象,然后写个脚本,脚本绑定到Canvas下。
脚本定义一个变量为
public RawImage rawImage;
Inspector面板中,把创建的RawImage对象赋值给这个变量。
接下来,就是脚本的工作了:
public RawImage rawImage;
public GameObject playButton;
private AndroidJavaObject nativeObject;
private int width, height;
private Texture2D texture2D;
// Use this for initialization
void Start()
{ //nativeObject假装是VideoPlugin对象
nativeObject = new AndroidJavaObject("com.pvr.videoplugin.VideoPlugin");
width = 1600;
height = 900;
Debug.Log("VideoPlugin:" + width + ", " + height);
}
// Update is called once per frame
void Update()
{
if (texture2D != null && nativeObject.Call<bool>("isUpdateFrame"))
{
Debug.Log("VideoPlugin:Update");
nativeObject.Call("updateTexture");
GL.InvalidateState();
}
}
public void Click()
{
Debug.Log("VideoPlugin:Start");
if (playButton != null) {
playButton.SetActive(false);
}
if (texture2D == null)
{ //关键:创建纹理
texture2D = new Texture2D(width, height, TextureFormat.RGB24, false, false);
Debug.Log("VideoPlugin:create texture2D");
//关键:把纹理id传递给android 的VideoPlugin的start函数
nativeObject.Call("start", (int)texture2D.GetNativeTexturePtr(), width, height);
//关键:同时设置RawImage的纹理为刚建的texture2D
rawImage.texture = texture2D;
Debug.Log("finish set texture");
}
}
Click函数,由随便加的一个按扭来触发执行。
Click函数,先自己建立一个纹理,把纹理Id给到Android层。Android则把这块纹理作为FBO的问题。Android播放的视频,最终都渲染到这块纹理上。
接着,也把这个纹理赋值给Raw Image(作为输入),所以,Android的视频输出,最终会在Raw Image显示出来。
于是流程打通。
2.2 Android 插件的工作
首先是VideoPlugin的实现:
public class VideoPlugin implements OnFrameAvailableListener {
private SurfaceTexture mSurfaceTexture;
private FilterFBOTexture mFilterFBOTexture;
private MediaPlayer mMediaPlayer;
private boolean mIsUpdateFrame;
/**
* Unity调用,把Unity创建的textureId传递过来,绑定到FBO
*/
public void start(int unityTextureId, int width, int height) {
FBOUtils.log("start, unityTextureId " +unityTextureId +" width " +width +" height " +height);
//生成视频播放输出的纹理id
int videoTextureId = FBOUtils.createVideoTextureID();
//根据创建的纹理id生成一个SurfaceTexture, 视频播放输出到surface texture
mSurfaceTexture = new SurfaceTexture(videoTextureId);
mSurfaceTexture.setDefaultBufferSize(width, height);
mSurfaceTexture.setOnFrameAvailableListener(this);
//创建FBO相关资源
mFilterFBOTexture = new FilterFBOTexture(width, height, unityTextureId, videoTextureId);
initMediaPlayer();
}
private void initMediaPlayer() {
FBOUtils.log("initMediaPlayer");
mMediaPlayer = new MediaPlayer();
//设置mediaplayer的输出为自定义surface
mMediaPlayer.setSurface(new Surface(mSurfaceTexture));
try {
final File file = new File("/sdcard/test.mp4");
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setLooping(true);
mMediaPlayer.setDataSource(Uri.fromFile(file).toString());
mMediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
FBOUtils.log("MediaPlayer onPrepared");
mMediaPlayer.start();
}
});
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
FBOUtils.log("onFrameAvailable");
//视频播放开始有输出
mIsUpdateFrame = true;
}
/**
* Unity调用,更新视频纹理,然后绘制FBO
*/
public void updateTexture() {
FBOUtils.log("updateTexture");
mIsUpdateFrame = false;
mSurfaceTexture.updateTexImage();
mFilterFBOTexture.draw();
}
public boolean isUpdateFrame() {
return mIsUpdateFrame;
}
}
关键函数start,把Unity创建的textureId传递过来,绑定到FBO。
然后,MediaPlayer的视频输出,不是SurfaceView了,而是自己建立的SurfaceTextute。
接下来是FilterFBOTexture的实现:
public class FilterFBOTexture {
private final String vertexShaderCode =
"attribute vec4 av_Position; \n" +
"attribute vec2 af_Position; \n" +
"varying vec2 v_texPo; \n" +
"void main() { \n" +
" gl_Position = av_Position; \n" +
" v_texPo = af_Position; \n" +
"}";
private final String fragmentShaderCode =
"#extension GL_OES_EGL_image_external : require \n" +
"precision mediump float; \n" +
"varying vec2 v_texPo;\n" +
"uniform samplerExternalOES s_Texture;\n" +
"void main() { \n" +
" gl_FragColor = texture2D(s_Texture, v_texPo);\n" +
"}";
static float[] vertexData = {
-1f, 1f,
1f, 1f,
-1f, -1f,
1f, -1f,
};
static float[] textureData = {
0f, 0f,
1f, 0f,
0f, 1f,
1f, 1f,
};
private final FloatBuffer vertexBuffer;
private final FloatBuffer textureBuffer;
private final int av_Position;
private final int af_Position;
private final int s_Texture;
private final int program;
private final int fboId;
private final int width;
private final int height;
private final int videoTextureId;
private final int unityTextureId;
public FilterFBOTexture(int width, int height, int unityTextureId, int videoTextureId) {
this.width = width;
this.height = height;
this.unityTextureId = unityTextureId;
this.videoTextureId = videoTextureId;
fboId = FBOUtils.createFBO();
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
textureBuffer = ByteBuffer.allocateDirect(textureData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureData);
textureBuffer.position(0);
program = FBOUtils.buildProgram(vertexShaderCode, fragmentShaderCode);
av_Position = GLES20.glGetAttribLocation(program, "av_Position");
af_Position = GLES20.glGetAttribLocation(program, "af_Position");
s_Texture = GLES20.glGetUniformLocation(program, "s_Texture");
}
public void draw() {
FBOUtils.log("draw");
//视口
GLES20.glViewport(0, 0, width, height);
//清除颜色缓冲
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//绑定FrameBuffer到当前的绘制环境上, 后续GL绘制都会到这个framebuffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);
//把一个纹理附加到帧缓冲
//之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中
//即所有渲染操作的结果将会被储存在unityTextureId对应的纹理图像中
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, unityTextureId, 0);
//检查帧缓冲是否完整
if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) {
FBOUtils.log("FrameBuffer error");
return;
}
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
//激活着色器程序
GLES20.glUseProgram(program);
//告诉OpenGL该如何解析顶点数据(顶点坐标),并启用顶点属性
GLES20.glEnableVertexAttribArray(av_Position);
GLES20.glVertexAttribPointer(av_Position, 2, GLES20.GL_FLOAT, false, 2 * 4, vertexBuffer);
//告诉OpenGL该如何解析顶点数据(纹理坐标),并启用顶点属性
GLES20.glEnableVertexAttribArray(af_Position);
GLES20.glVertexAttribPointer(af_Position, 2, GLES20.GL_FLOAT, false, 2 * 4, textureBuffer);
//激活纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE7);
//绑定指定纹理到当前激活的纹理单元
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureId);
//为着色器中定义的采样器指定属于哪个纹理单元;不要忘记在设置uniform变量之前激活着色器程序
GLES20.glUniform1i(s_Texture, 7);
//绘制三角带
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(av_Position);
GLES20.glDisableVertexAttribArray(af_Position);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
//激活默认帧缓冲
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
2.3 整理流程
即,android播放视频的输出,到一块纹理上,这个纹理是FBO的输入。可以对这个纹理进行各种操作。
FBO的输出,又指向了Unity创建的另一个纹理。所以,上面的各种操作,直接体现在Unity创建的纹理。
而这个纹理,是RawImage的输入!
RawImage的输入有更新了,它就显示东西出来了!
2.4 其他说明
Android插件分为openGL ES 2.0和openGL ES 3.0。
2个模块都可以输出jar。
不管你选哪个,都需要Unity在打包时,自己定一下选用哪个版本。
即Unity -> File -> Build Settings -> Player Settings
3. 附录
完整的Unity项目
:
https://github.com/hywenbinger/EasyMovie-Unity
完整的Android插件代码
https://github.com/hywenbinger/EasyMovie-Android