(四) FFmpeg软硬解码和多线程解码(C++ NDK)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huanghuangjin/article/details/81876823

dts(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
pts(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

AVFrame的linesize成员:
这里写图片描述

代码

#include "common.hpp"

#ifdef __cplusplus // ffmpeg是基于c语言开发的库,所有头文件引入的时候需要 extern "C"
extern "C" {
#endif

    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavcodec/jni.h"

    JNIEXPORT jint JNI_OnLoad(JavaVM * vm, void * res) // jni 初始化时会调用此函数
    {
        // ffmpeg要使用硬解码,需要将java虚拟机环境传给ffmpeg,ffmpeg中通过vm来调用android中mediacodec硬解码的java接口 , 第二个函数为日志,不需要,传0
        av_jni_set_java_vm(vm, 0);
        return JNI_VERSION_1_4; // 选用jni 1.4 版本
    }

#ifdef __cplusplus
}
#endif

// 当前时间戳
static long long getNowMs()
{
    /*
        struct timeval tv1,tv2;
        unsigned long long timeconsumed = 0;
        // 获取当前时间:
        gettimeofday(&tv1,NULL);
        ...
        gettimeofday(&tv2,NULL);
        // 时间统计:
        timeconsumed = tv2.tv_sec-tv1.tv_sec +(tv2.tv_usev-tv1.tv_usec)/1000000; // 以秒为单位
     */
    struct timeval tv; // timeval结构体的两个成员为 秒 与 微秒
    gettimeofday(&tv, NULL); // 获取系统当前时间, 1970到系统时间的秒数? signed long值装不下
    int sec = tv.tv_sec%360000; // 100个小时
    long long time = sec*1000 + tv.tv_usec/1000; // 将 timeval 时间单位转换为 毫秒
    return time;
}

JNIEXPORT void JNICALL Java_hankin_hjmedia_mpeg_Mp5_11Activity_decode(JNIEnv *env, jobject instance, jstring url_, jobject handle)
{
    const char * path = env->GetStringUTFChars(url_, NULL); // jstring 2 char*
    if (path[0]=='\0')
    {
        LOGE("path is empty.");
        return;
    }

    av_register_all();
    int netInit = avformat_network_init();
    LOGD("netInit=%d", netInit);

    avcodec_register_all(); // 在4.0 已过时,Register all the codecs, parsers and bitstream filters which were enabled at configuration time. 注释此代码无影响

    AVFormatContext * ic = NULL;
    int openRet = avformat_open_input(&ic, path, NULL, NULL); // 打开媒体文件
    if (openRet!=0 || ic==NULL)
    {
        LOGE("avformat_open_input is failed : %s", av_err2str(openRet));
        return;
    }
    int findStream = avformat_find_stream_info(ic, NULL); // 查找媒体信息
    if (findStream!=0)
    {
        LOGE("avformat_find_stream_info is failed");
        return;
    }
    LOGD("duration=%lld, nb_streams=%d, bit_rate=%lld", ic->duration, ic->nb_streams, ic->bit_rate);
    int videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); // 查找视频流在媒体流中的下标,方式一
    int audioStream = -1;
    for (int i = 0; i < ic->nb_streams; ++i)
    {
        AVStream * as = ic->streams[i];
        if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) // 音频流
        {
            audioStream = i; // 查找音频流在媒体流中的下标,方式二,遍历
            break;
        }
    }
    if (videoStream==AVERROR_STREAM_NOT_FOUND || audioStream==-1)
    {
        LOGE("没有找到视频流或音频流 : videoStream=%d, audioStream=%d", videoStream, audioStream);
        return;
    }
    LOGD("videoStream=%d, audioStream=%d", videoStream, audioStream);
    AVPacket * pkt = av_packet_alloc(); // 创建AVPacket

    /////////////////// 视频解码器配置信息 ///////////////////   打开解码器最好与解封装代码分开
    AVCodec * videoCodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id); // 查找软解码器,AVCodec存放解码器的配置信息,并不是解码信息
    if (!videoCodec) LOGE("查找视频软解码器失败.");
    // 通过名字查找解码器,之前编译ffmpeg的时候将硬解码编译了进去,所以这里name可以传 "h264_mediacodec" 表示硬解,
    //      mediacodec是android arm芯片自带的硬解码固件,这里ffmpeg函数内部调用了android的java接口,因为android系统的mediacodec硬解只提供了java接口
    videoCodec = avcodec_find_decoder_by_name("h264_mediacodec"); // 硬解码器
    if (!videoCodec)
    {
        LOGE("查找视频硬解码器失败.");
        return; // 不要return,还需要在后面释放相应内存
    }
    // 创建视频解码器上下文
    AVCodecContext * videoContext = avcodec_alloc_context3(videoCodec); // 创建编解码器(ffmpeg中编码解码都是用的AVCodec),codec参数传NULL也可以,但是不会创建编、解码器
    if (!videoContext)
    {
        LOGE("创建视频解码器上下文失败.");
        return;
    }
    int cvRet = avcodec_parameters_to_context(videoContext, ic->streams[videoStream]->codecpar); // 将AVStream中的参数直接复制到AVCodec中
    if (cvRet!=0) LOGE("视频 avcodec_parameters_to_context 错误.");
    videoContext->thread_count = 8; // 设置软解码线程数量,硬解码时次参数无用
    /*
        int avcodec_open2(
            AVCodecContext *avctx, // 编解码器上下文
            const AVCodec *codec, // 如果在创建AVCodecContext的时候没有传codec,那么这里就要传,创建时传了,这里就可以传NULL,总之,这两个函数中只要传一个codec
            AVDictionary **options // 字典的数组,key-value , 参看 ffmpeg源码 ./libavformat/options_table.h
        );
     */
    int vret = avcodec_open2(videoContext, 0, 0); // 打开视频解码器
    if (vret!=0)
    {
        LOGE("打开视频解码器失败");
        return;
    }
    /////////////////// 音频解码器,同视频解码器一样 ///////////////////
    AVCodec * audioCodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id); // 音频没有硬解码器?
    if (!audioCodec)
    {
        LOGE("查找音频软解码器失败.");
        return;
    }
    AVCodecContext * audioContext = avcodec_alloc_context3(audioCodec);
    if (!audioContext)
    {
        LOGE("创建音频解码器上下文失败.");
        return;
    }
    int caRet = avcodec_parameters_to_context(audioContext, ic->streams[audioStream]->codecpar);
    if (caRet!=0) LOGE("音频 avcodec_parameters_to_context 错误.");
    audioContext->thread_count = 8;
    int aret = avcodec_open2(audioContext, 0, 0);
    if (aret!=0)
    {
        LOGE("打开音频解码器失败");
        return;
    }
    AVFrame * frame = av_frame_alloc(); // 创建AVFrame
    /////////////////// end ///////////////////

    long long start = getNowMs(); // 其实时间
    int frameCount = 0;
    while (true)
    {
        if (getNowMs() - start >= 3000) // 3秒内
        {
            // 软解码单线程时,手机每秒50帧左右(非neon、mediacodec的so库性能差百分之三四十),模拟器差不多快一倍。手机cpu占用率大概在16%
            // 8线程时性能几乎快一倍,但是cpu占用率有百分之七八十
            // 硬解码时,使用的是非cpu gpu的固件,解码率理论上是固定的
            LOGW("3秒内平均每秒解码视频帧数 : %d", frameCount/3);
            start = getNowMs();
            frameCount = 0;
        }

        int ret = av_read_frame(ic, pkt); // 读取视频流和音频流的每一帧数据,返回非0表示出错或者读到了文件末尾
        if (ret!=0)
        {
            LOGD("读到结尾处了");
            int pos = 20 * (double) ic->streams[videoStream]->time_base.num / (double) ic->streams[videoStream]->time_base.den;
            av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME); // seek到在pos位置往前找到关键帧的地方
            break;
        }
        LOGV("stream=%d, size=%d, pts=%lld, flag=%d", pkt->stream_index, pkt->size, pkt->pts, pkt->flags);

        // 开始视频、音频解码
        AVCodecContext * acc = videoContext;
        if (pkt->stream_index==audioStream) acc = audioContext;
        /*
            int avcodec_send_packet( // 早前ffmpeg解码音视频是分两个不同接口,现在是多线程解码,需要先将AVPacket发到解码队列中去(子线程中)
                AVCodecContext *avctx, // 解码上下文
                const AVPacket *avpkt // 调用此函数会浅拷贝一份AVPacket,avpkt内部data引用计数会加1,所以调用此函数后可以放心 av_packet_unref与释放avpkt
            ); // return 0 on success, otherwise negative error code:
         */
        int rr = avcodec_send_packet(acc, pkt); // 最后一帧的处理 pkt 要传 NULL ?
        if (pkt->stream_index==videoStream) LOGI("%s send rr=%d", "video", rr);
            else LOGD("%s send rr=%d", "audio", rr);
        /*
            AVFrame                                                     结构体,存放每帧解码后的数据,对应着AVPacket,不过比AVPacket的内存开销更大
                uint8_t *data[AV_NUM_DATA_POINTERS];                        指针数组,如果是视频表示每行的数据,如果是音频表示每个通道的数据。 这样存的原因是因为数据的存放方式(有交叉存放有平面存放)
                int linesize[AV_NUM_DATA_POINTERS];                         与data成对的信息,如果是视频表示一行数据的大小,如果是音频表示一个通道的数据的大小
                int width;                                                  视频的宽高,只有视频有
                int height;
                int nb_samples;                                             单通道的样本数量,只有音频有
                int64_t pts;                                                这一帧的pts
                int64_t pkt_pts;                                            对应AVPacket中的pts
                int64_t pkt_dts;                                            对应AVPacket中的dts
                uint64_t channel_layout;                                    有音频有,通道类型,Channel layout of the audio data.
                int channels;                                               声道数,只用于音频
                int sample_rate;                                            采样率,只用于音频
                int format;                                                 像素格式(枚举AVPixelFormat) 或 音频的样本格式(比如16bit、32bit等)(枚举AVSampleFormat)

                AVFrame *av_frame_alloc(void);                              创建AVFrame
                void av_frame_free(AVFrame **frame);                        释放AVFrame本身的内存
                AVFrame *av_frame_clone(const AVFrame *src);                同AVPacket的操作类似,浅拷贝,src内部data的引用计数加1
                int av_frame_ref(AVFrame *dst, const AVFrame *src);         同AVPacket的操作类似,src浅拷贝到dst,同时src内部data的引用计数加1
                void av_frame_unref(AVFrame *frame);                        同AVPacket的操作类似,将frame内部data的引用计数减1 ,为0时释放data内存

            int avcodec_receive_frame( // 从解码队列中已成功解码的数据的缓存中取出帧数据,一次函数调用有可能接受到多帧数据,也有可能接受不到
                AVCodecContext *avctx, // 解码上下文
                AVFrame *frame // 返回的接受到的每帧的数据
            ); // return 0 on success, otherwise negative error code:
         */
        while (true) // 循环接受解码队列中成功解码的帧数据,直到该时刻没有成功解码的帧数据为止
        {
            rr = avcodec_receive_frame(acc, frame);
            if (pkt->stream_index==videoStream) LOGI("%s receive rr=%d", "video", rr);
                else LOGD("%s receive rr=%d", "audio", rr);
            if (rr==0)
            {
                if (pkt->stream_index==videoStream) LOGI("%s frame->pts=%lld", "video", frame->pts);
                    else LOGD("%s frame->pts=%lld", "audio", frame->pts);
            }
            else break; // 没有接收到的时候,break,这个时候是不会为frame内部的data申请内存的
            av_frame_unref(frame); // 这句代码不加也不会内存溢出。。 avcodec_receive_frame 并不会为 frame 的data重复申请内存?
            if (pkt->stream_index==videoStream && rr==0) frameCount++; // 读取到的视频帧数 ++
        }

        av_packet_unref(pkt); // av_read_frame读取每帧数据的时候会为AVPacket的data指针申请内存,这里将其引用计数减1,以释放内存
    }
    avcodec_free_context(&videoContext); // 清理AVCodecContext内存空间,如果不及时释放会造成应用内存崩溃
    avcodec_free_context(&audioContext);

    av_packet_free(&pkt); // 释放AVPacket本身占用的内存
    avformat_close_input(&ic); // 关闭打开的媒体流,释放内存

    env->ReleaseStringUTFChars(url_, path); // java的String引用计数减1,释放path内存
}

猜你喜欢

转载自blog.csdn.net/huanghuangjin/article/details/81876823