Android利用硬解硬编和OpenGLES来高效的处理MP4视频

215 篇文章 4 订阅

https://blog.csdn.net/junzia/article/details/77924629

 

最近工作中遇到一个问题,就是要对视频增加视频特效,实现类似于抖音的效果,抖音的效果由其他同事实现,我的工作重心在视频的处理,特效的集成。按照之前的思路很快就实现了这个功能,但是实际应用到项目中时却遇到各种问题。于是就有了这篇博客。
遇到的问题

说是各种问题,特效方便的不管,我所遇到的视频处理的问题主要为以下两个方面:

    处理过程耗时较长。因为处理的时候是按照之前的思路,用MediaCodec解码,取出ByteBuffer,然后用OpenGLES处理,处理完毕后readPixels,得到图像数据,然后将图像数据推入MediaCodec编码。 在这里readPixels非常耗时。480*840的视频,一帧耗时基本是40ms+。
    手机兼容性很成问题。虽然不需要考虑低版本兼容,只需要考虑4.4+的手机。但是Android手机市场的情况,开发者朋友们应该也都知道,各家有各家的小动作,混乱不堪。解码出来的视频数据,并不是固定的格式,虽然大多数手机都支持YUV420P或者YUV420SP,但是也有些奇葩手机,只能解码出OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m这类的格式,总不能都去判断然后根据格式去转换吧。

之前看官方文档的时候,有看到MediaCodec解码视频支持直接解码到Surface上,编码也可以直接从Surface采集数据,这样的话,视频数据可以直接解码到Surface上,然后通过OpenGLES处理,再又通过Surface进行编码,就无需关注解码出来的数据的格式了,而且应用层也不必自己去将原始数据导入GPU以及将处理后的数据导出GPU了,这些工作可以都丢给Android SDK去做。理论上就能一举解决上面的两个问题。那么具体应该如何做呢?
处理流程

有了理论,剩下的就是实现了。不卖关子,根据以上的方案,直接列出处理的流程:

    利用MediaExtractor获取Mp4的音轨和视轨,获取音频视频的MediaFormat.
    根据音视频信息,创建视频解码器,视频编码器,音频暂时不处理就不创建编解码器了。其中视频解码器的Surface是通过先创建一个SurfaceTexture,然后将这个SurfaceTexture作为参数创建的,这样的话,视频流就可以通过这个SurfaceTexture提供给OpenGL环境作为输出。视频编码器的Surface可直接调用createInputSurface()方法创建,这个Surface后续传递给OpenGL环境作为输出
    创建MediaMuxer,用于后面合成处理后的视频和音频。
    创建OpenGL环境,用于处理视频图像,这个OpenGL环境由EGL创建,EGLSurface为WindowSurface,并以编码器创建的Surface作为参数。
    MediaExtractor读取原始Mp4中的视频流,交由解码器解码到Surface上。
    SurfaceTexture监听有视频帧时,通知OpenGL线程工作,处理视频图像,并渲染。
    OpenGL线程每次渲染完毕,通知编码线程进行编码,编码后的数据通过MediaMuxer混合。
    视频流处理完毕后,利用MediaExtractor读取音频流,并利用MediaMuxer混合到新的视频文件中。
    处理完毕后调用MediaMuxer的stop方法,处理后的视频就生成成功了。

具体实现

流程一捋,道理到家都懂,具体怎么实现呢。根据以上流程上代码了。
创建需要的编解码工具

这里是直接把1、2、3步的事情,在一个方法中完成了:

//todo 获取视频旋转信息,并做出相应处理
MediaMetadataRetriever mMetRet=new MediaMetadataRetriever();
mMetRet.setDataSource(mInputPath);
mExtractor=new MediaExtractor();
mExtractor.setDataSource(mInputPath);
int count=mExtractor.getTrackCount();
//解析Mp4
for (int i=0;i<count;i++){
    MediaFormat format=mExtractor.getTrackFormat(i);
    String mime=format.getString(MediaFormat.KEY_MIME);
    if(mime.startsWith("audio")){
        mAudioDecoderTrack=i;
    }else if(mime.startsWith("video")){

        mVideoDecoderTrack=i;
        mInputVideoWidth=format.getInteger(MediaFormat.KEY_WIDTH);
        mInputVideoHeight=format.getInteger(MediaFormat.KEY_HEIGHT);
        mVideoDecoder=MediaCodec.createDecoderByType(mime);
        mVideoTextureId=mEGLHelper.createTextureID();
        //注意这里,创建了一个SurfaceTexture
        mVideoSurfaceTexture=new SurfaceTexture(mVideoTextureId);
        mVideoSurfaceTexture.setOnFrameAvailableListener(mFrameAvaListener);
        //将SurfaceTexture作为参数创建一个Surface,用来接收解码视频流
        mVideoDecoder.configure(format,new Surface(mVideoSurfaceTexture),null,0);
        if(!isRenderToWindowSurface){
            if(mOutputVideoWidth==0||mOutputVideoHeight==0){
                mOutputVideoWidth=mInputVideoWidth;
                mOutputVideoHeight=mInputVideoHeight;
            }
            MediaFormat videoFormat=MediaFormat.createVideoFormat(mime,mOutputVideoWidth,mOutputVideoHeight);
            videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            videoFormat.setInteger(MediaFormat.KEY_BIT_RATE,mOutputVideoHeight*mOutputVideoWidth*5);
            videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 24);
            videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            mVideoEncoder=MediaCodec.createEncoderByType(mime);
            mVideoEncoder.configure(videoFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
            //注意这里,创建了一个Surface,这个Surface是编码器的输入,也是OpenGL环境的输出
            mOutputSurface=mVideoEncoder.createInputSurface();
            Bundle bundle=new Bundle();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                bundle.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE,mOutputVideoHeight*mOutputVideoWidth*5);
                mVideoEncoder.setParameters(bundle);
            }
        }
    }
}
//这里的if是测试时候,直接解码到屏幕上,外部设置了OutputSurface,用于测试,所以不必管
if(!isRenderToWindowSurface){
    //如果用户没有设置渲染到指定Surface,就需要导出视频,暂时不对音频做处理
    mMuxer=new MediaMuxer(mOutputPath,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    MediaFormat format=mExtractor.getTrackFormat(mAudioDecoderTrack);
    mAudioEncoderTrack=mMuxer.addTrack(format);
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

创建OpenGL环境

第4步,创建OpenGL环境,用来处理视频图像,先直接贴个工具类,用于创建OpenGL环境

public class EGLHelper {

    private EGLSurface mEGLSurface;
    private EGLContext mEGLContext;
    private EGLDisplay mEGLDisplay;
    private EGLConfig mEGLConfig;

    private EGLContext mShareEGLContext=EGL14.EGL_NO_CONTEXT;

    private boolean isDebug=true;

    private int mEglSurfaceType=EGL14.EGL_WINDOW_BIT;

    private Object mSurface;

    /**
     * @param type one of {@link EGL14#EGL_WINDOW_BIT}、{@link EGL14#EGL_PBUFFER_BIT}、{@link EGL14#EGL_PIXMAP_BIT}
     */
    public void setEGLSurfaceType(int type){
        this.mEglSurfaceType=type;
    }

    public void setSurface(Object surface){
        this.mSurface=surface;
    }

    /**
     * create the environment for OpenGLES
     * @param eglWidth width
     * @param eglHeight height
     */
    public boolean createGLES(int eglWidth, int eglHeight){
        int[] attributes = new int[] {
                EGL14.EGL_SURFACE_TYPE, mEglSurfaceType,      //渲染类型
                EGL14.EGL_RED_SIZE, 8,  //指定RGB中的R大小(bits)
                EGL14.EGL_GREEN_SIZE, 8, //指定G大小
                EGL14.EGL_BLUE_SIZE, 8,  //指定B大小
                EGL14.EGL_ALPHA_SIZE, 8, //指定Alpha大小,以上四项实际上指定了像素格式
                EGL14.EGL_DEPTH_SIZE, 16, //指定深度缓存(Z Buffer)大小
                EGL14.EGL_RENDERABLE_TYPE, 4, //指定渲染api类别, 如上一小节描述,这里或者是硬编码的4(EGL14.EGL_OPENGL_ES2_BIT)
                EGL14.EGL_NONE };  //总是以EGL14.EGL_NONE结尾

        int glAttrs[] = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,  //0x3098是EGL14.EGL_CONTEXT_CLIENT_VERSION,但是4.2以前没有EGL14
                EGL14.EGL_NONE
        };

        int bufferAttrs[]={
                EGL14.EGL_WIDTH,eglWidth,
                EGL14.EGL_HEIGHT,eglHeight,
                EGL14.EGL_NONE
        };

        //获取默认显示设备,一般为设备主屏幕
        mEGLDisplay= EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);

        //获取版本号,[0]为版本号,[1]为子版本号
        int[] versions=new int[2];
        EGL14.eglInitialize(mEGLDisplay,versions,0,versions,1);
        log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VENDOR));
        log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VERSION));
        log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_EXTENSIONS));

        //获取EGL可用配置
        EGLConfig[] configs = new EGLConfig[1];
        int[] configNum = new int[1];
        EGL14.eglChooseConfig(mEGLDisplay, attributes,0, configs,0, 1, configNum,0);
        if(configs[0]==null){
            log("eglChooseConfig Error:"+ EGL14.eglGetError());
            return false;
        }
        mEGLConfig = configs[0];

        //创建EGLContext
        mEGLContext= EGL14.eglCreateContext(mEGLDisplay,mEGLConfig,mShareEGLContext, glAttrs,0);
        if(mEGLContext==EGL14.EGL_NO_CONTEXT){
            return false;
        }
        //获取创建后台绘制的Surface
        switch (mEglSurfaceType){
            case EGL14.EGL_WINDOW_BIT:
                mEGLSurface=EGL14.eglCreateWindowSurface(mEGLDisplay,mEGLConfig,mSurface,new int[]{EGL14.EGL_NONE},0);
                break;
            case EGL14.EGL_PIXMAP_BIT:
                break;
            case EGL14.EGL_PBUFFER_BIT:
                mEGLSurface=EGL14.eglCreatePbufferSurface(mEGLDisplay,mEGLConfig,bufferAttrs,0);
                break;
        }
        if(mEGLSurface==EGL14.EGL_NO_SURFACE){
            log("eglCreateSurface Error:"+EGL14.eglGetError());

            return false;
        }

        if(!EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext)){
            log("eglMakeCurrent Error:"+EGL14.eglQueryString(mEGLDisplay,EGL14.eglGetError()));
            return false;
        }
        log("gl environment create success");
        return true;
    }

    public void setShareEGLContext(EGLContext context){
        this.mShareEGLContext=context;
    }

    public EGLContext getEGLContext(){
        return mEGLContext;
    }

    public boolean makeCurrent(){
        return EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext);
    }

    public boolean destroyGLES(){
        EGL14.eglMakeCurrent(mEGLDisplay,EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_CONTEXT);
        EGL14.eglDestroySurface(mEGLDisplay,mEGLSurface);
        EGL14.eglDestroyContext(mEGLDisplay,mEGLContext);
        EGL14.eglTerminate(mEGLDisplay);
        log("gl destroy gles");
        return true;
    }

    public void setPresentationTime(long time){
        EGLExt.eglPresentationTimeANDROID(mEGLDisplay,mEGLSurface,time);
    }

    public boolean swapBuffers(){
        return EGL14.eglSwapBuffers(mEGLDisplay,mEGLSurface);
    }


    //创建视频数据流的OES TEXTURE
    public int createTextureID() {
        int[] texture = new int[1];
        GLES20.glGenTextures(1, texture, 0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
        return texture[0];
    }

    private void log(String log){
        if(isDebug){
            Log.e("EGLHelper",log);
        }
    }

}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157

借助上面的工具类创建OpenGL环境。可以看到里面使用了信号量,是用于当有新的视频图像时由SurfaceTexture的监听器通知GL线程执行渲染,没有的话就等待新的视频图像解码完后再执行处理工作。

mSem=new Semaphore(0);
//设置输出的Surface
mEGLHelper.setSurface(mOutputSurface);
//根据设置的输出视频的宽高创建OpenGL环境
boolean ret=mEGLHelper.createGLES(mOutputVideoWidth,mOutputVideoHeight);
if(!ret)return;
mRenderer.onCreate(mOutputVideoWidth,mOutputVideoHeight);
while (mGLThreadFlag){
    try {
        mSem.acquire();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    mVideoSurfaceTexture.updateTexImage();
    //回调用户的处理函数
    mRenderer.onDraw();
    //设置时间点,用于输出视频图像的时间点,这里是填入输入视频的时间点
    mEGLHelper.setPresentationTime(mVideoDecoderBufferInfo.presentationTimeUs*1000);
    if(!isRenderToWindowSurface){
        //调用编码函数进行编码
        videoEncodeStep(false);
    }
    mEGLHelper.swapBuffers();
}
if(!isRenderToWindowSurface){
    //编码视频,传入true表示视频结束
    videoEncodeStep(true);
}
//销毁OpenGL环境
mEGLHelper.destroyGLES();
mRenderer.onDestroy();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

第6步就是用于通知这个GL线程执行渲染工作,只需要在监听器中,发出信号就可以了。

private SurfaceTexture.OnFrameAvailableListener mFrameAvaListener=new SurfaceTexture.OnFrameAvailableListener() {
        @Override
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            mSem.release();
        }
    };

    1
    2
    3
    4
    5
    6

视频流解码

第5步,需要将视频解码,解码的方法如下。在解码的线程中循环调用此方法,其返回值为true时结束循环,也就是视频帧解码完毕。

//视频解码到SurfaceTexture上,以供后续处理。返回值为是否是最后一帧视频
private boolean videoDecodeStep(){
    int mInputIndex=mVideoDecoder.dequeueInputBuffer(TIME_OUT);
    if(mInputIndex>=0){
        ByteBuffer buffer=getInputBuffer(mVideoDecoder,mInputIndex);
        buffer.clear();
        synchronized (Extractor_LOCK) {
            mExtractor.selectTrack(mVideoDecoderTrack);
            int ret = mExtractor.readSampleData(buffer, 0);
            if (ret != -1) {
                mVideoDecoder.queueInputBuffer(mInputIndex, 0, ret, mExtractor.getSampleTime(), mExtractor.getSampleFlags());
            }
            isVideoExtractorEnd = !mExtractor.advance();
        }
    }
    while (true){
        int mOutputIndex=mVideoDecoder.dequeueOutputBuffer(mVideoDecoderBufferInfo,TIME_OUT);
        if(mOutputIndex>=0){
            mVideoDecoder.releaseOutputBuffer(mOutputIndex,true);
        }else if(mOutputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
            MediaFormat format=mVideoDecoder.getOutputFormat();
        }else if(mOutputIndex==MediaCodec.INFO_TRY_AGAIN_LATER){
            break;
        }
    }
    return isVideoExtractorEnd;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

视频流编码并混合

在第四步的代码中,已经出现了视频流编码的方法了,也就是videoEncodeStep(boolean),其实现如下:

private boolean videoEncodeStep(boolean isEnd){
    if(isEnd){
        mVideoEncoder.signalEndOfInputStream();
    }
    while (true){
        int mOutputIndex=mVideoEncoder.dequeueOutputBuffer(mVideoEncoderBufferInfo,TIME_OUT);
        if(mOutputIndex>=0){
            ByteBuffer buffer=getOutputBuffer(mVideoEncoder,mOutputIndex);
            if(mVideoEncoderBufferInfo.size>0){
                mMuxer.writeSampleData(mVideoEncoderTrack,buffer,mVideoEncoderBufferInfo);
            }
            mVideoEncoder.releaseOutputBuffer(mOutputIndex,false);
        }else if(mOutputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
            MediaFormat format=mVideoEncoder.getOutputFormat();
            mVideoEncoderTrack=mMuxer.addTrack(format);
            mMuxer.start();
            synchronized (MUX_LOCK){
                MUX_LOCK.notifyAll();
            }
        }else if(mOutputIndex==MediaCodec.INFO_TRY_AGAIN_LATER){
            break;
        }
    }
    return false;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

音频流处理

因为现在暂时不需要对视音频处理,所以直接从原始MP4中读取音频流混合到新的Mp4中即可,与解码相同,这个方法也是在线程中循环调用,返回true时终止循环,最后调用MediaMuxer的stop方法,新的视频就生成好了。

private boolean audioDecodeStep(ByteBuffer buffer){
    buffer.clear();
    synchronized (Extractor_LOCK){
        mExtractor.selectTrack(mAudioDecoderTrack);
        int length=mExtractor.readSampleData(buffer,0);
        if(length!=-1){
            int flags=mExtractor.getSampleFlags();
            mAudioEncoderBufferInfo.size=length;
            mAudioEncoderBufferInfo.flags=flags;
            mAudioEncoderBufferInfo.presentationTimeUs=mExtractor.getSampleTime();
            mAudioEncoderBufferInfo.offset=0;
            mMuxer.writeSampleData(mAudioEncoderTrack,buffer,mAudioEncoderBufferInfo);
        }
        isAudioExtractorEnd=!mExtractor.advance();
    }
    return isAudioExtractorEnd;
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

为了不阻塞主线程,音视频的处理单独开一个线程处理为好。

mDecodeThread=new Thread(new Runnable() {
    @Override
    public void run() {
        //视频处理
        while (mCodecFlag&&!videoDecodeStep());
        mGLThreadFlag=false;
        try {
            mGLThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //将原视频中的音频复制到新视频中
        ByteBuffer buffer=ByteBuffer.allocate(1024*32);
        while (!audioDecodeStep(buffer));
        buffer.clear();

        mMuxer.stop();
        if(mCompleteListener!=null){
            mCompleteListener.onComplete(mOutputPath);
        }
    }
});

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

其他

源码在github上,有需要的朋友可自行下载,如有帮助欢迎fock和start。如果对于硬编硬解不太理解的,可以查阅官方文档,我的另外一篇博客也有编码的示例,可以参考——Android硬编码——音频编码、视频编码及音视频混合。对于OpenGLES不太熟悉的朋友,可以参考我前面的OpenGLES系列的博客。
---------------------  
作者:湖广午王  
来源:CSDN  
原文:https://blog.csdn.net/junzia/article/details/77924629  
版权声明:本文为博主原创文章,转载请附上博文链接!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值