Android使用OpenGL和MediaCodec渲染视频

发布于:2025-04-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

一,借助MediaCodec封装解码工具类VideoCodec

二,使用OpenGl绘制视频封装SoulFilter


一,借助MediaCodec封装解码工具类VideoCodec

/**
 * 解码工具类
 * 解码完成后的数据 通过 ISurface 回调出去
 */
public class VideoCodec {
    private ISurface mISurface;
    private String mPath;
    private MediaExtractor mMediaExtractor;
    private int mWidth;
    private int mHeight;
    private int mFps;
    private MediaCodec mMediaCodec;
    private boolean isCodeing;
    private byte[] outData;
    private CodecTask mCodecTask;

    /**
     * 要在prepare之前调用
     * @param surface
     */
    public void setDisplay(ISurface surface){
        mISurface = surface;
    }


    /**
     * 设置要解码的视频地址
     * @param path
     */
    public void setDataSource(String path){
        mPath = path;
    }


    /**
     *
     * 准备方法
     */
    public void prepare(){
        //MediaMuxer:复用器 封装器
        //解复用(解封装)
        mMediaExtractor = new MediaExtractor();
        try {
            //把视频给到 解复用器
            mMediaExtractor.setDataSource(mPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        int videoIndex = -1;
        MediaFormat videoMediaFormat = null;
        // mp4 1路音频 1路视频
        int trackCount = mMediaExtractor.getTrackCount();
        for (int i = 0; i < trackCount; i++) {
            //获得这路流的格式
            MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(i);
            //选择视频 获得格式
            // video/  audio/
            String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
            if(mime.startsWith("video/")){
                videoIndex = i;
                videoMediaFormat = mediaFormat;
                break;
            }
        }
        //默认是-1
        if (null != videoMediaFormat){
            //解码 videoIndex 这一路流
            mWidth = videoMediaFormat.getInteger(MediaFormat.KEY_WIDTH);
            mHeight = videoMediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
            mFps = 20;
            if (videoMediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
                mFps = videoMediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
            }
            // 个别手机  小米(x型号) 解码出来不是yuv420p
            //所以设置 解码数据格式 指定为yuv420
            videoMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo
            .CodecCapabilities.COLOR_FormatYUV420Planar);

            try {
                //创建一个解码器
                mMediaCodec = MediaCodec.createDecoderByType(videoMediaFormat.getString(MediaFormat.KEY_MIME));
                mMediaCodec.configure(videoMediaFormat,null,null,0);
            } catch (IOException e) {
                e.printStackTrace();
            }
            //选择流 后续读取这个流
            mMediaExtractor.selectTrack(videoIndex);
        }
        if (null != mISurface){
            mISurface.setVideoParamerters(mWidth,mHeight,mFps);
        }
    }


    /**
     * 开始解码
     */
    public void start(){
        isCodeing = true;
        //接收 解码后的数据 yuv数据大小是 w*h*3/2
        outData = new byte[mWidth * mHeight * 3 / 2];
        mCodecTask = new CodecTask();
        mCodecTask.start();
    }

    /**
     * 停止
     */
    public void stop(){
        isCodeing = false;
        if (null != mCodecTask && mCodecTask.isAlive()){
            try {
                mCodecTask.join(3_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //3s后线程还没结束
            if (mCodecTask.isAlive()){
                //中断掉
                mCodecTask.interrupt();
            }
            mCodecTask = null;
        }

    }


    /**
     * 解码线程
     */
    private class CodecTask extends Thread{

        @Override
        public void run() {
            if (null == mMediaCodec) {
                return;
            }
              // 开启
            mMediaCodec.start();
            boolean isEOF = false;
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            //是否中断线程
            while(!isInterrupted()){
                if (!isCodeing){
                    break;
                }
                // 如果 eof是true 就表示读完了,就不执行putBuffer2Codec方法了
                //并不代表解码完了
                if (!isEOF) {
                    isEOF = putBuffer2Codec();
                }
                //...
                //从输出缓冲区获取数据  解码之后的数据
                int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 100);
                //获取到有效的输出缓冲区 意味着能够获取到解码后的数据了
                if (status >= 0){
                    ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(status);
                    if (bufferInfo.size == outData.length){
                        //取出数据 存到outData yuv420
                        outputBuffer.get(outData);
                        if (null != mISurface){
                            mISurface.offer(outData);
                        }
                    }
                    //交付掉这个输出缓冲区 释放
                    mMediaCodec.releaseOutputBuffer(status,false);
                }
                //干完活了 ,全部解码完成了
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
                    //解码完了
                    break;
                }
            }

            mMediaCodec.stop();
            mMediaCodec.release();
            mMediaCodec = null;
            mMediaExtractor.release();
            mMediaExtractor = null;
        }

        /**
         *
         * @return true:没有更多数据了
         *           false:还有
         */
        private boolean putBuffer2Codec(){
            // -1 就一直等待
            int status = mMediaCodec.dequeueInputBuffer(100);
            //有效的输入缓冲区 index
            if (status >=0 ){
                //把待解码数据加入MediaCodec
                ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(status);
                //清理脏数据
                inputBuffer.clear();
                // ByteBuffer当成byte数组 ,读数据存入 ByteBuffer 存到byte数组的第0个开始存
                int size = mMediaExtractor.readSampleData(inputBuffer, 0);
                //没读到数据 已经没有数据可读了
                if (size < 0){
                    //给个标记 表示没有更多数据可以从输出缓冲区获取了
                    mMediaCodec.queueInputBuffer(status,0,0,0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    return true;
                }else{
                    //把噻了数据的输入缓冲区噻回去
                    mMediaCodec.queueInputBuffer(status,0,size,
                            mMediaExtractor.getSampleTime(), 0);
                    //丢掉已经加入解码的数据 (不丢就会读重复的数据)
                    mMediaExtractor.advance();
                }
            }
            return false;
        }
    }

}

二,使用OpenGl绘制视频封装SoulFilter

/**
 * 灵魂出窍滤镜
 */
public class SoulFilter extends AbstractFilter {

    private  int[] mTextures;
    //肉体
    private  GLImage bodyImage;
    //灵魂
    private  GLImage soulImage;

    private  int mAlpha;
    private  int mSamplerV;
    private  int mSamplerU;
    private  int mSamplerY;
    private int mFps;
    private float[] matrix = new float[16];
    private int interval;

    public SoulFilter(Context context) {
        super(context, R.raw.soul_vertex, R.raw.soul_frag);
        bodyImage = new GLImage();
        soulImage = new GLImage();
        mSamplerY = GLES20.glGetUniformLocation(mGLProgramId, "sampler_y");
        mSamplerU = GLES20.glGetUniformLocation(mGLProgramId,"sampler_u");
        mSamplerV = GLES20.glGetUniformLocation(mGLProgramId,"sampler_v");
        mAlpha = GLES20.glGetUniformLocation(mGLProgramId, "alpha");
        //3个纹理 yuv
        mTextures = new int[3];
        OpenGLUtils.glGenTextures(mTextures);
    }

   public void onReady2(int width,int height,int fps){
        super.onReady(width,height);
        mFps = fps;
       bodyImage.initSize(width,height);
       soulImage.initSize(width,height);
   }

    public void onDrawFrame(byte[] yuv) {
        //把yuv分离出来 保存在 image中的 y、u、v三个变量中
        bodyImage.initData(yuv);
        //分离出的数据有效
        if (!bodyImage.hasImage()){
            return;
        }
        //启用着色器程序
        GLES20.glUseProgram(mGLProgramId);
        //初始化矩阵 不进行任何缩放平移
        Matrix.setIdentityM(matrix,0);
        //给肉体的 无变化矩阵
        GLES20.glUniformMatrix4fv(vMatrix,1,false,matrix,0);
        //透明度 肉体不透明
        GLES20.glUniform1f(mAlpha,1);
        //传值
        //画画
        onDrawBody(bodyImage);
        //混合灵魂
        onDrawSoul(yuv);
    }

    private void onDrawBody(GLImage image){
        //传递坐标
        mGLVertexBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGLVertexBuffer);
        GLES20.glEnableVertexAttribArray(vPosition);

        mGLTextureBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);


        //传递yuv数据
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]);
        //把y数据与 0纹理绑定
        //  GL_LUMINANCE: yuv 给这个
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_LUMINANCE,
                mOutputWidth,mOutputHeight,0,GLES20.GL_LUMINANCE,
                GLES20.GL_UNSIGNED_BYTE,image.getY());
        GLES20.glUniform1i(mSamplerY, 0);

    //u数据
        GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[1]);
        //把y数据与 0纹理绑定
        //  GL_LUMINANCE: yuv 给这个
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_LUMINANCE,
                mOutputWidth/2,mOutputHeight/2,0,GLES20.GL_LUMINANCE,
                GLES20.GL_UNSIGNED_BYTE,image.getU());
        GLES20.glUniform1i(mSamplerU, 1);


        GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[2]);
        //把y数据与 0纹理绑定
        //  GL_LUMINANCE: yuv 给这个
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_LUMINANCE,
                mOutputWidth/2,mOutputHeight/2,0,GLES20.GL_LUMINANCE,
                GLES20.GL_UNSIGNED_BYTE,image.getV());
        GLES20.glUniform1i(mSamplerV, 2);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);


        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }

    private void onDrawSoul(byte[] yuv){
        interval++;
        //没保存一个灵魂 或者使用次数已经达到上限了
        // 灵魂只能使用x次 使用完了之后就要更新灵魂
        if (!soulImage.hasImage() || interval > mFps){
            //次数重置为1
            interval = 1;
            //记录新灵魂
            soulImage.initData(yuv);
        }
        if (!soulImage.hasImage()){
            return;
        }

        //画灵魂
        GLES20.glEnable(GLES20.GL_BLEND);
        //1:源 灵魂  GL_ONE:画灵魂自己
        //2: 肉体  也是肉体自己
        //两个都是用自己原本的颜色去混合
//        GLES20.glBlendFunc(GLES20.GL_ONE,GLES20.GL_ONE);
        //让灵魂整体颜色变淡
        // GL_SRC_ALPHA: 取源(灵魂)的alpha 作为因子
        // 假设alpha是0.2 rgb都是1 -> 混合就是用 rgb都是 0.2*1 整体变淡
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA,GLES20.GL_ONE);

        //初始化矩阵 不进行任何缩放平移
        Matrix.setIdentityM(matrix,0);
    //设置缩放大小 本次放大为 1+当前灵魂次数占总次数*2的比例
        //不一次放太大 为了达到较好的表现效果 fps*2
        //所以这里值为 1+1/60 ---> 1+20/40 1.025... ---> 1.5
        float scale = 1.0f + interval / (mFps * 2.f);
        Matrix.scaleM(matrix,0,scale,scale,0);
        //给肉体的 无变化矩阵
        GLES20.glUniformMatrix4fv(vMatrix,1,false,matrix,0);


        //传递透明度 透明度值为0-1 渐渐降低 0.1+x/100 x为fps-[0~fps]
        //这里值为0.29 ---> 0.1
        GLES20.glUniform1f(mAlpha, 0.1f + (mFps - interval) / 100.f);

        //画灵魂
        onDrawBody(soulImage);
    }
}