赞
踩
码字不易,转载请注明出处喔
https://blog.csdn.net/newchenxf/article/details/119565585
Unity本身可以直接播放本地视频或网络视频,网上有很多例子,例如:
https://blog.csdn.net/cs874300/article/details/89294433
这可以完全在Unity封闭完成。
但是,有一种情况,是播放逻辑希望在Android的代码内完成(比如是直播的流,用的是android 的直播SDK),希望Unity只渲染Android播放的画面就可以。这种情况怎么做呢?本文一一说来。
首先,完成上面的工作,需要依赖FBO,所以先介绍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绑定,后面渲染的结果,就到你要的纹理上了!完美,真香
分两部分,一个是Unity部分,一个是Android部分。
先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显示出来。
于是流程打通。
首先是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); }
即,android播放视频的输出,到一块纹理上,这个纹理是FBO的输入。可以对这个纹理进行各种操作。
FBO的输出,又指向了Unity创建的另一个纹理。所以,上面的各种操作,直接体现在Unity创建的纹理。
而这个纹理,是RawImage的输入!
RawImage的输入有更新了,它就显示东西出来了!
Android插件分为openGL ES 2.0和openGL ES 3.0。
2个模块都可以输出jar。
不管你选哪个,都需要Unity在打包时,自己定一下选用哪个版本。
即Unity -> File -> Build Settings -> Player Settings
完整的Unity项目
:
https://github.com/hywenbinger/EasyMovie-Unity
完整的Android插件代码
https://github.com/hywenbinger/EasyMovie-Android
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。