32.FFmpeg+OpenGLES+OpenSLES播放器实现(六.FFmpeg音视频解码)

项目源码
FFmpeg开发文档
解码分为软解码和硬解码,那么什么是软解码和硬解码,二者有什么区别?简单来说,在于是否使用CPU进行解码,最初视频解码都是通过CPU进行的,那时候视频分辨率较低,CPU完全可以胜任解码的工作,但是随着高清视频的出现,使用CPU进行解码的压力越来越大

软解码

使用CPU进行解码,所以就很容易造成CPU负载过大。纯粹依靠CPU来解码,是在显卡本身不支持或者部分不支持硬件解码的前提下,将解压高清编码的任务交给CPU,这是基于硬件配置本身达不到硬解压要求的前提下的无奈之举。我们在解码过程中,应该在硬件支持的前提下优先使用硬解码。
对于一个超级电视而言,观看高清电影无疑是用户最大的诉求,而硬解码的优势就在于可以流畅的支持1080p甚至4K清晰度的电影播放,而不需要占用CPU,CPU就可以如释重负,轻松上阵,承担更多的其他任务。如果通过软解码的方式播放高清电影,CPU的负担较重,往往会出现卡顿、不流畅的现象。

硬解码

硬件解码就是通过显卡的视频加速功能对高清视频进行解码,使用非CPU进行,如GPU/VPU(GPU:图形处理器:Graphics Processing Unit,指计算机的显卡,VPU:Visual Processing Unit,视觉处理单元,由ATI提出的、用于区别于传统GPU的概念,实际二者均为显示处理核心,本质上并无任何区别)、专用的DSP、FPGA、ASIC芯片等,所以几乎不会占用CPU,部分产品在GPU硬件平台移植了优秀的软编码算法(如X264),解码质量基本等同于软编码。硬解码可以将CPU从繁重的视频解码中解放出来,使播放设备具备流畅播放高清视频的能力。显卡的GPU/VPU要比CPU更适合这类大数据量的、低难度的重复工作。

那么ffmpeg是如何进行软解码和硬解码的,分别来看

解码步骤

使用ffmpeg解码分为如下几步:
1.找到解码器
avcodec_find_decoder(软) avcodec_find_decoder_by_name(硬)
2.获取解码器上下文
avcodec_alloc_context3
3.填充解码器参数到上下文中
avcodec_parameters_to_context
4.打开解码器
avcodec_open2
5.将解封装得到的AVPacket发送到解码器中进行解码
avcodec_send_packet
6.从解码器中接收已经解码完成的数据存入AVFrame
avcodec_receive_frame
7.释放frame空间
av_frame_unref

具体到代码中看一看音视频的软解码和硬解码

视频软解码环境
    /***************************************video解码器*********************************************/
    //找到视频解码器(软解码)
    AVCodec *videoAVCodec = avcodec_find_decoder(avFormatContext->streams[videoIndex]->codecpar->codec_id);
    avcodec_open2 video failed!
    if (videoAVCodec == NULL){
        LOGE("avcodec_find_decoder failed !");
        return;
    }
    //初始化视频解码器上下文对象
    AVCodecContext *videoCodecContext = avcodec_alloc_context3(videoAVCodec);
    //根据所提供的编解码器的值填充编解码器上下文参数
    avcodec_parameters_to_context(videoCodecContext,avFormatContext->streams[videoIndex]->codecpar);
    //设置视频解码器解码的线程数,解码时将会以你设定的线程进行解码
    videoCodecContext->thread_count = 8;
    //打开解码器
    result = avcodec_open2(videoCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 video failed! %s",av_err2str(result));
        return;
    }
    /***********************************************************************************************/
视频硬解码环境
    //硬解码,硬解码需要Jni_OnLoad中做设置否则ffmpeg_player_error: avcodec_open2 video failed!
    AVCodec *videoAVCodec = avcodec_find_decoder_by_name("h264_mediacodec");
    if (videoAVCodec == NULL){
        LOGE("avcodec_find_decoder failed !");
        return;
    }
    //初始化视频解码器上下文对象
    AVCodecContext *videoCodecContext = avcodec_alloc_context3(videoAVCodec);
    //根据所提供的编解码器的值填充编解码器上下文参数
    avcodec_parameters_to_context(videoCodecContext,avFormatContext->streams[videoIndex]->codecpar);
    //设置视频解码器解码的线程数,解码时将会以你设定的线程进行解码
    videoCodecContext->thread_count = 8;
    //打开解码器
    result = avcodec_open2(videoCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 video failed! %s",av_err2str(result));
        return;
    }

可以看到软硬解码的区别也只是在于获取解码器的那一步操作

音频软解码环境
    /***************************************audio解码器*********************************************/

    //找到音频解码器(软解码)
    AVCodec *audioAVCodec = avcodec_find_decoder(avFormatContext->streams[audioIndex]->codecpar->codec_id);
    //初始化音频解码器上下文对象
    AVCodecContext *audioCodecContext = avcodec_alloc_context3(audioAVCodec);
    //根据所提供的编解码器的值填充编解码器上下文参数
    avcodec_parameters_to_context(audioCodecContext,avFormatContext->streams[audioIndex]->codecpar);
    //设置音频解码器解码的线程数,解码时将会以你设定的线程进行解码
    audioCodecContext->thread_count = 1;
    //打开音频解码器
    result = avcodec_open2(audioCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 audio failed!");
        return;
    }
    /***********************************************************************************************/
音频硬解码环境
/***************************************audio解码器*********************************************/

    //找到音频解码器(软解码)
    //硬解码
    AVCodec *audioAVCodec = avcodec_find_decoder_by_name("h264_mediacodec");
    //初始化音频解码器上下文对象
    AVCodecContext *audioCodecContext = avcodec_alloc_context3(audioAVCodec);
    //根据所提供的编解码器的值填充编解码器上下文参数
    avcodec_parameters_to_context(audioCodecContext,avFormatContext->streams[audioIndex]->codecpar);
    //设置音频解码器解码的线程数,解码时将会以你设定的线程进行解码
    audioCodecContext->thread_count = 1;
    //打开音频解码器
    result = avcodec_open2(audioCodecContext,NULL,NULL);
    if (result != 0){
        LOGE("avcodec_open2 audio failed!");
        return;
    }
    /***********************************************************************************************/

同样也是区别在于解码器的获取

获取到解码器之后解码的过程代码如下:

    for (;;) {

        //********************测试每秒解码帧数代码*******************
        if(GetNowMs() - start >=3000){
            LOGI("now decode fps is %d",frameCount/3);
            start = GetNowMs();
            frameCount = 0;
        }
        //********************测试每秒解码帧数代码*******************

        //Return the next frame of a stream.
        int read_result = av_read_frame(avFormatContext,avPacket);
        if(read_result != 0){
            //读取到结尾处,从20秒位置继续开始播放
            LOGI("读取到结尾处 %s",av_err2str(read_result));
            //跳转到指定的position播放,最后一个参数表示
            //int pos = 200000 * r2d(avFormatContext->streams[videoIndex]->time_base);
            //av_seek_frame(avFormatContext,videoIndex,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
            //LOGI("avFormatContext->streams[videoIndex]->time_base= %d",avFormatContext->streams[videoIndex]->time_base);
            //continue;
            break;
        }
        LOGW("stream = %d size =%d pts=%lld flag=%d pos = %d",
             avPacket->stream_index,avPacket->size,avPacket->pts,avPacket->flags,avPacket->pos
        );

        //解码测试
        if(avPacket->stream_index != videoIndex){
            continue;
        }

        AVCodecContext *codecContext = videoCodecContext;
        if (avPacket->stream_index == audioIndex){
            codecContext = audioCodecContext;
        }

        //将packet发送到解码器中进行解码
        read_result = avcodec_send_packet(videoCodecContext,avPacket);
        if (read_result != 0){
            LOGE("avcodec_send_packet failed!");
            continue;
        }

        for(;;){
            //从解码器中返回的已经解码的数据
            read_result = avcodec_receive_frame(codecContext,avFrame);
            if(read_result != 0){
                LOGE("avcodec_receive_frame failed!");
                break;
            }

            //********************测试每秒解码帧数代码*******************
            //说明解码的是视频,
            if(codecContext == videoCodecContext) {
                frameCount++;
            }
            //********************测试每秒解码帧数代码*******************

            LOGW("avcodec_receive_frame %lld",avFrame->pts);
        }

        //packet使用完成之后执行,否则内存会急剧增长
        //不再引用这个packet指向的空间,并且将packet置为default状态
        av_packet_unref(avPacket);
    }
软硬解码已经多线程解码效率测试结果

neon 单线程下解码视频结果:
每秒帧数27~68
CPU占用16%左右
内存占用67M左右

neon 八线程下解码视频结果
每秒解码帧数100~170帧
CPU占用70%左右
内存占用90M左右

neon h264_mediacodec硬解码 设置单线程
每秒解码帧数46~58,硬解码是一个固定值,这是由于计算误差产生的
CPU占用3%左右
内存占用23M左右

neon h264_mediacodec硬解码 设置8线程
每秒解码帧数46~85,线程数对帧率无影响,因为硬解码帧率是固定的
CPU和内存的占用可忽略不计

总结一下

1.软解码相较于硬解码,对内存和CPU的开销都明显很大
2.硬解码的解码帧率是固定的,但是几乎不占用CPU

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容