(三) FFmpeg解封装(C++ NDK)

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

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

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

#ifdef __cplusplus
}
#endif

static bool isRead = true; // 用来退出循环解封装

static double r2d(AVRational r) // 时间基数转换为double
{
    return r.num==0||r.den==0 ? 0 : (double)r.num/(double)r.den;
}

JNIEXPORT jstring JNICALL Java_hankin_hjmedia_mpeg_Mp3_11Activity_ffmpegConfigure(JNIEnv *env, jobject instance)
{
    const char * config = avcodec_configuration(); // 获取编译ffmpeg时的./configure命令信息
    LOGD2("mydebug---", "编译ffmpeg时的./configure命令信息 : %s", config);
    return charTojstring_hjmedia(env, config);
}

JNIEXPORT jboolean JNICALL Java_hankin_hjmedia_mpeg_Mp3_11Activity_open(JNIEnv *env, jobject instance, jstring url_, jobject handle) // 如果在java层面url_传null,会报错
{
    jboolean jb;
    // 当从 JNI 函数 GetStringUTFChars 中返回得到字符串B时,如果B是原始字符串java.lang.String 的拷贝,则isCopy被赋值为 JNI_TRUE,(结果是拷贝)
    //      如果B和原始字符串指向的是JVM中的同一份数据,则 isCopy被赋值为 JNI_FALSE。
    const char *path = env->GetStringUTFChars(url_, &jb); // java中UTF字符集的字符转换为char*,不会有中文乱码
    LOGV("jb=%d, JNI_FALSE=%d", jb, JNI_FALSE); // jb=1, JNI_FALSE=0
    LOGI("paht=%s", path);
    if (path[0]=='\0') // 空字符传 ""
    {
        LOGE("path is empty.");
        return JNI_FALSE;
    }


    //初始化解封装
    av_register_all(); // 注册所有格式,解封装、压封装,也可单个注册某个格式             注释此代码对后面没影响
    //初始化网络
    avformat_network_init(); // 注册支持rtsp格式的数据(即基于rtp协议的),也支持http格式的数据             注释此代码对后面似乎也没影响
    //打开文件
    /*
         AVFormatContext // 结构体,封装的上下文,用于解封装、压封装
                AVIOContext *pb;            io流context,如果是自定义的格式,需要使用这个自己到内存中读
                char filename[1024];        文件地址
                unsigned int nb_streams;    存放AVStream的大小(即流的数量),一般就2个(flv文件获取不到),视频音频,可能还会有字幕
                AVStream **streams;         是一个数组,存的视频音频流,一般0是视频下标,1是音频下标,最好还是遍历查找确定一下
                int64_t duration;           整个媒体文件的总长度,以时间基数 AV_TIME_BASE(一百万分之一秒) 为单位,
                                                在AVStream中也有时间,这两个时间不一定相等。duration也不能保证一定获取到(比如flv文件获取不到)
                int64_t bit_rate;           比特率,1秒中内文件的大小
     */
    AVFormatContext * context = NULL;
    /*
         int avformat_open_input( // 打开某格式的音视频, 调用前确保已经 av_register_all 或 avformat_network_init 注册
            AVFormatContext **ps, // 格式化的上下文,**ps可以为NULL(ffmpeg会自动创建),但需要将指向NULL的指针的地址 ps 传进来,不能传NULL,会出错
            const char *url, // 音视频文件地址,支持本地、网络 http rtsp 等,会存到 AVFormatContext 结构体中,可以用作断开重连
            AVInputFormat *fmt, // 指定输入的封装格式,一般不用,传NULL就行,让ffmpeg自行探测,因为探测有消耗,如果需要频繁的打开的话还是传一下格式
            AVDictionary **options // 字典的数组,key-value , 打开本地文件传NULL就行,如果是rtsp的,需要传递 参看 ffmpeg源码 ./libavformat/options_table.h
        );
     */
    int ret = avformat_open_input(&context, path, 0, 0); // 返回 0 表示打开成功
    LOGD2("mydebug---", "context is NULL : %d", (context==NULL)); // path不对的时候,avformat_open_input就不会创建context了
    if(ret!=0 || context==NULL)
    {
        LOGE2("mydebug---", "avformat_open_input open failed : %s", av_err2str(ret)); // av_err2str(ret)输出打开失败的原因
        return JNI_FALSE;
    }
    // 1 duration=60000000, nb_streams=2, bit_rate=0
    LOGD("1 duration=%lld, nb_streams=%u, bit_rate=%lld", context->duration, context->nb_streams, context->bit_rate);
    // 如果打开音视频的时候,其头文件中没有包含格式和索引信息的时候,利用此函数到文件后面去查找。除了这个还有探测,直接探测网络上过来的流
    ret = avformat_find_stream_info(context, 0); // 返回 0 表示找到了流的信息
    if (ret!=0) LOGW2("mydebug---", "avformat_find_stream_info failed : %s", av_err2str(ret)); // 这个时候流信息读到了
    // 2 duration=60000000, nb_streams=2, bit_rate=3447910
    LOGW("2 duration=%lld, nb_streams=%u, bit_rate=%lld", context->duration, context->nb_streams, context->bit_rate);

    // 打印音视频信息
    double fps = 0;
    int videoStream = 0; // 视频流的下标
    int audioStream = 1; // 音频流的下标
    for (int i = 0; i < context->nb_streams; ++i)
    {
        /*
             AVStream                           结构体,解封装、解码
                AVCodecContext *codec;      已过时,以前是将解封装与解码放在一起的,新版本将它们隔离
                AVRational time_base;       时间基数,Rational表示有理数(即能用分子分母表示的数),可以理解为一个分数,它里面存的就是分子分母
                int64_t duration;           时长,不一定有  duration * ((double)time_base.num/(double)time_base.den) * 1000  此表达式得到的是毫秒
                int64_t nb_frames;
                AVRational avg_frame_rate;  平均帧率,主要针对视频,也是用的分数表示
                AVCodecParameters *codecpar; 结构体,音视频参数,用来替代上面过时的 AVCodecContext *codec 的
                    enum AVMediaType codec_type;    编码数据的类型,音频还是视频
                    enum AVCodecID   codec_id;      编码格式,在ffmpeg内部 H264 或者 其他编码 都对应着一种格式,参看枚举 AVCodecID
                    uint32_t codec_tag;             用4个字节来表示各种编码器,暂时用不到
                    int format;                     像素格式(枚举AVPixelFormat) 或 音频的样本格式(比如16bit、32bit等)(枚举AVSampleFormat)
                    int width;                      宽高,只有视频有
                    int height;
                    uint64_t channel_layout;        只有音频有,The channel layout bitmask. May be 0 if the channel layout is unknown or unspecified,
                                                                        otherwise the number of bits set must be equal to the channels field
                    int channels;                   声道数,只用于音频
                    int sample_rate;                采样率,只用于音频
                    int frame_size;                 一帧音频的大小,只用于音频
         */
        AVStream * as = context->streams[i]; // 获取文件中流信息
        if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) // 视频数据
        {
            videoStream = i; // 保存视频流的下标
            fps = r2d(as->avg_frame_rate); // 获取平均帧率
            // 视频数据 : fps=25.000000, width=1920, height=1080, codec_id=27, pixformat=0, videoStream=0
            LOGD("视频数据 : fps=%f, width=%d, height=%d, codec_id=%d, pixformat=%d, videoStream=%d",
                 fps, as->codecpar->width, as->codecpar->height, as->codecpar->codec_id, as->codecpar->format, videoStream);
        }
        else if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) // 音频数据
        {
            audioStream = i; // 保存音频流的下标
            // 音频数据 : sample_rate=44100, channels=2, sample_format=8, audioStream=1    8表示AV_SAMPLE_FMT_FLTP(float平面类型)
            LOGD("音频数据 : sample_rate=%d, channels=%d, sample_format=%d, audioStream=%d",
                 as->codecpar->sample_rate, as->codecpar->channels, as->codecpar->format, audioStream);
        }
    }
    // 除了上面的遍历的方式获取流信息,还可以用以下方式
    /*
         int av_find_best_stream( // 打开音视频文件后,需要对音视频进行单独的处理,此函数返回音频或视频流在上下文的streams中的索引
            AVFormatContext *ic, // 上下文
            enum AVMediaType type, // 要获取的音频还是视频信息
            int wanted_stream_nb, // 想要的流信息,暂传-1
            int related_stream, // 相关的流信息,ffmpeg中视频下还有节目的概念,咱传-1
            AVCodec **decoder_ret, // 解码器,传一个空指针进来,函数会自动分配内存,暂用不到,因为将解封装与解码分开了,咱传NULL
            int flags // 预留参数,ffmpeg现在还没用到,传0即可
        );
     */
    audioStream = av_find_best_stream(context, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    LOGI2("mydebug---", "audioStream=%d", audioStream); // audioStream=1

    //读取帧数据,并从视频20秒的位置到末尾循环读取,看内存情况         循环一定次数后logcat会read: unexpected EOF!,但是循环还是继续执行的
    /*
         AVPacket                           结构体
                AVBufferRef *buf;           指针,指向buffer空间,用于存储引用计数
                int64_t pts;                显示的时间,具体为 pts * (num/den) 时间单位也是微秒(百万分之一秒)
                int64_t dts;                解码的时间,具体为同上。 如果没有B帧,pts与dts是一致的
                uint8_t *data;              指向的是 AVBufferRef *buf 中再分配的空间
                int size;                   data的size

                // AVPacket 的一些使用
                AVPacket *av_packet_alloc(void);                            创建并初始化AVPacket,这个会申请分配堆上内存,用完后需要释放
                AVPacket *av_packet_clone(const AVPacket *src);             浅拷贝, AVPacket *src 的引用计数会加1
                int av_packet_ref(AVPacket *dst, const AVPacket *src);      将 src 加到 dst 中,src 的引用计数会加1 ,该函数其实就是调用了上面的两个函数
                void av_packet_unref(AVPacket *pkt);                        将 AVPacket *pkt 的引用计数减1,引用计数为0的时候,AVPacket内部的 uint8_t *data 就会释放内存
                void av_packet_free(AVPacket **pkt);                        除了要释放AVPacket内部指针的指向的内存,还需要释放AVPacket本身占用的内存,
                                                                                此函数释放AVPacket内存并减引用计数,同时*pkt指针会置空,防止野指针
                void av_init_packet(AVPacket *pkt);                         在栈上以默认值初始化AVPacket, this does not touch the data and size members
                int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size);    初始化 AVPacket 中 data 与 size 成员
                int av_copy_packet(AVPacket *dst, const AVPacket *src);     深拷贝,已过时,用 av_packet_clone 与引用计数代替
     */
    AVPacket * pkt = av_packet_alloc();
    while (isRead)
    {
        /*
             int av_read_frame( // 读解封装出来的数据包,其包含了 pts dts 音频或视频索引 是否是关键帧 ,不过AVPacket中去掉了上面说的H264的间隔符00000001或000001
                AVFormatContext *s, // 上下文
                AVPacket *pkt // 解封装后出来的一个一个的数据包,不能传NULL
             ); // 返回值 0 表示ok,小于0表示错误或读到了末尾
         */
        int result = av_read_frame(context, pkt); // 读取每帧的时候,pkt内部data指针会分配内存,如果不释放会造成内存泄漏,累计多了应用也会内存崩溃
        if (result!=0)
        {
            LOGI("读到结尾处了");
            /*
                 int av_seek_frame( // seek操作,可以单独的针对音频或视频独立操作seek, return >= 0 on success
                     AVFormatContext *s, // 封装格式的上下文
                     int stream_index, // 音频视频流的索引,-1 default 一般是视频
                     int64_t timestamp, // 时间戳,即要移动到哪个时间位置,单位为微秒,同样要以 AVStream.time_base 处理
                     int flags // 标识位,表示移动的方法,解决seek时非关键帧与时间戳的计算
                                     AVSEEK_FLAG_BACKWARD 1 ///< seek backward 往后找一帧(时间越早的称为后),
                                     AVSEEK_FLAG_BYTE     2 ///< seeking based on position in bytes 用的不多,按文件的大小位置添砖
                                     AVSEEK_FLAG_ANY      4 ///< seek to any frame, even non-keyframes 任意帧,就找seek位置最近的帧,不会去寻找关键帧
                                     AVSEEK_FLAG_FRAME    8 ///< seeking based on frame number 表示要找到关键帧,要与其他flags一起使用 |
                 );
             */
            double timebase = r2d(context->streams[videoStream]->time_base);
            int pos = 20 * timebase; // 视频总长60秒,计算第20秒的时间戳
            int sek = av_seek_frame(context, videoStream, pos, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME); // 跳转到第20秒
            LOGI("timebase=%lf, sek=%d", timebase, sek); // timebase=0.000078, sek=0
            continue;
        }
        // stream 标识此为音频或视频的AVPacket
        LOGV2("mydebug---", "stream=%d, size=%d, pts=%lld, flag=%d", pkt->stream_index, pkt->size, pkt->pts, pkt->flags);
        // 将 AVPacket *pkt 的引用计数减1,引用计数为0的时候,AVPacket内部的 uint8_t *data 就会释放内存
        av_packet_unref(pkt);
    }
    av_packet_free(&pkt); // 还需要释放AVPacket本身占用的内存,此函数释放AVPacket内存并减引用计数,同时pkt指针会置空,防止野指针

    // 专门的用来关闭输入的上下文的函数,关闭输出的有其他方式。函数内部已经将指针置零(包含AVFormatContext占用的空间与其内部成员指针指向的空间)了,防止野指针
    avformat_close_input(&context);
    LOGI("close and end");


    env->ReleaseStringUTFChars(url_, path); // 在jvm中release对象url_ ,这里是完全释放内存,还是只是对象的引用计数减1 ?   同时释放c的字符串的内存?
    return JNI_TRUE; // 在c中可以不return,但是c++中必须要return,否则运行报错
}

JNIEXPORT void JNICALL Java_hankin_hjmedia_mpeg_Mp3_11Activity_stop(JNIEnv *env, jobject instance)
{
    isRead = false;
}

猜你喜欢

转载自blog.csdn.net/huanghuangjin/article/details/81840807
ndk