Unity 渲染 Android播放的视频画面

Posted 长江很多号

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 渲染 Android播放的视频画面相关的知识,希望对你有一定的参考价值。

码字不易,转载请注明出处喔
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为例子,一个绑定代码如下:

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);

        //激活帧缓冲
        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的输入有更新了,它就显示东西出来了!

3. 附录

完整的Unity项目

https://github.com/hywenbinger/EasyMovie-Unity

完整的Android插件代码

https://github.com/hywenbinger/EasyMovie-Android

以上是关于Unity 渲染 Android播放的视频画面的主要内容,如果未能解决你的问题,请参考以下文章

Android视频融合特效播放与渲染

Unity实现Android端视频播放

Unity 视频播放

unity rtsp 视频渲染

unity rtsp 视频渲染

ffmpeg 播放音视频,time_base解决音频同步问题,SDL渲染画面