Android使用MediaCodec和OpenGL对多段视频画面进行裁剪和拼接

太久没写博客了,由于工作,过年还有孩子出生搞得自己焦头烂额,现在有些时间了就搞点东西。发现浏览量突破10万了,也是挺高兴的,虽然很多东西写的不好,可也看到了自己的进步,也是前年到现在的累积。刚开始我只是学习视频解码,渲染和视频编码,慢慢的也开始搞音频了,本来没想过搞视频编辑这一块的,慢慢的做着做着就接触到了,也没想到会搞成一个系列,等完成了再好好整理一下,废话不多说开始说正题。

转gif由于文件大小问题降低了帧率所以看起来卡其实不卡

这篇文章只讲画面部分,音频部分在我以前的文章有android 使用MediaCodec和lamemp3对多段音频进行截取和拼接,以后再写一篇相结合的文章,真正实现一个有画面有声音的视频裁剪和合并。做音频拼接知道有些参数要一样才可以进行拼接,视频也一样,主要是帧速率和分辨率要一样,因为用到opengl,所以分辨率这一块可以通过计算解决,而帧速率可以进行丢帧处理,选最小的帧速率为参数,对大帧速率的视频进行丢帧。

首先裁剪视频,裁剪视频用的是我的这篇文章android 简单的视频编辑功能

主要用到这些参数,这是纯画面的,先不处理声音

已经加入了音频

    //视频路径
    private String videoFile;
    //裁剪的座标长宽
    private int cropLeft;
    private int cropTop;
    private int cropWidth;
    private int cropHeight;
    //开始结束时间
    private long startTime;
    private long endTime;
    //帧时间
    private long frameTime;

得到复数的视频开始统一参数

//这里遍历数据,选择最小的参数用来初始化编码器
        cropWidth = decoderList.get(0).getCropWidth();
        cropHeight = decoderList.get(0).getCropHeight();
        int frameRate = decoderList.get(0).getFrameRate();
        frameTime = decoderList.get(0).getFrameTime();
        //宽高选择方案
        //以最小宽高来显示还是根据宽高比计算最小显示分辨率
        //我这选用根据宽高比计算最小显示分辨率
        float sh = cropWidth*1.0f/cropHeight;
        if(decoderList.size() != 1) {
            for (VideoDecoder holder : decoderList) {
                float vh = holder.getCropWidth()*1.0f/holder.getCropHeight();
                if( sh < vh){
                    cropWidth = Math.min(cropWidth,holder.getCropWidth());
                    cropHeight = (int) (cropWidth*vh);
                    //宽高不能是奇数
                    if(cropHeight%2 != 0){
                        cropHeight = cropHeight - 1;
                    }
                }
                //cropWidth = Math.min(cropWidth,holder.getCropWidth());
                //cropHeight = Math.min(cropHeight,holder.getCropHeight());
                //选最小帧速率,对帧素率大的进行丢帧处理
                frameRate = Math.min(frameRate, holder.getFrameRate());
                //帧时间,帧速率越小,帧时间越大
                frameTime = Math.max(frameTime, holder.getFrameTime());
            }
        }
        //防止MediaFormat取不到帧速率
        if(frameRate < 1){
            frameRate = (int) (1000/(frameTime/1000));
        }
        //编码参数
        int BIT_RATE = cropWidth*cropHeight*2*8;
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(VIDEO, cropWidth, cropHeight);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

初始化编码器

        //创建编码器
        try {
            videoEncode = MediaCodec.createEncoderByType(VIDEO);
        } catch (IOException e) {
            e.printStackTrace();
        }
        videoEncode.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

将编码器生成的Surface传给opengl来获取opengl的显示画面

        //将编码器生成的Surface传给opengl来获取opengl的显示画面
        Surface surface = videoEncode.createInputSurface();
        videoEncode.start();
        //初始化opengl
        eglUtils = new EGLUtils();
        eglUtils.initEGL(surface);
        framebuffer = new GLFramebuffer();
        framebuffer.initFramebuffer(cropWidth,cropHeight);
        surfaceTexture = framebuffer.getSurfaceTexture();
        surfaceTexture.setDefaultBufferSize(cropWidth,cropHeight);

解码开始前要计算显示的画面

            //计算裁剪的显示画面
            float f = decoder.getCropLeft()*1.0f/decoder.getVideoWidth();
            float t = 1.0f - decoder.getCropTop()*1.0f/decoder.getVideoHeight();
            float r = (decoder.getCropLeft()+decoder.getCropWidth())*1.0f/decoder.getVideoWidth();
            float b = 1.0f - (decoder.getCropTop()+decoder.getCropHeight())*1.0f/decoder.getVideoHeight();
            float[] textureVertexData = {
                    r, b,
                    f, b,
                    r, t,
                    f, t
            };
            //将要显示的画面传给opengl
            framebuffer.setVertexDat(textureVertexData);
            framebuffer.setRect(decoder.getCropWidth(),decoder.getCropHeight());

创建解码器

        //创建解码器
        try {
            videoDecoder = MediaCodec.createDecoderByType(getMime());
            videoDecoder.configure(format, surface, null, 0);
            videoDecoder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }

根据时间进行解码

            int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
                if(outIndex >= 0){
                    //根据时间进行解码
                    if(info.presentationTimeUs >= videoHolder.getStartTime() &&
                            info.presentationTimeUs <= videoHolder.getEndTime()){
                        //解码一帧完成后回调
                        if(decoderListener != null){
                            decoderListener.onDecoder(info.presentationTimeUs,videoHolder);
                        }
                    }
                    videoDecoder.releaseOutputBuffer(outIndex, true);
                    //判断是否裁剪内部分解码完成
                    if(info.presentationTimeUs >= videoHolder.getEndTime()){
                        break;
                    }
                }

解码出的图像进行处理

        surfaceTexture.updateTexImage();
        surfaceTexture.getTransformMatrix(mSTMatrix);

        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(0f,0f,0f,0f);
        GLES20.glViewport(rect.left, rect.top, rect.right, rect.bottom);

        GLES20.glUseProgram(programId);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffers[0]);
        GLES20.glEnableVertexAttribArray(aPositionHandle);
        GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
                12, 0);

        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffers[1]);
        GLES20.glEnableVertexAttribArray(aTextureCoordHandle);
        GLES20.glVertexAttribPointer(aTextureCoordHandle, 2, GLES20.GL_FLOAT, false, 8, 0);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]);
        GLES20.glUniform1i(uTextureSamplerHandle,0);
        GLES20.glUniformMatrix4fv(uSTMMatrixHandle, 1, false, mSTMatrix, 0);

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

进行编码

ByteBuffer encodedData = videoEncode.getOutputBuffer(inputIndex);
                    if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                        info.size = 0;
                    }
                    if (info.size != 0) {
                        encodedData.position(info.offset);
                        encodedData.limit(info.offset + info.size);
                        info.presentationTimeUs = timeUs;
                        //填充数据
                        mediaMuxer.writeSampleData(videoTrackIndex, encodedData, info);
                        Log.d("==============","timeUs = "+timeUs);
                        timeUs = timeUs+frameTime;
                        //结合presentationTimeUs - videoHolder.getStartTime() < startFrameTime
                        //对帧速率大的进行丢帧
                        startFrameTime = startFrameTime + frameTime;
                    }
                    videoEncode.releaseOutputBuffer(inputIndex, false);

详细的可以看我的demo

VideoMerge

现在没加进度条,处理的进度就看log打印,等整理好以后加上音频和进度条,成为一个可以看的demo

2019/3/27更新

因为音频要转码成MP3,再转成视频所以效率不太好,可是也找不到什么好方法将采样率,比特率和声道变一样的方法

在整合音频时代码做了大改动,画面核心代码没变只是放到其他类和函数内了,所以就不改博客了,可以下载demo,里面我也做了注释

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章