美文网首页
八、关于FFmpeg需要絮叨的一些事

八、关于FFmpeg需要絮叨的一些事

作者: Mirs | 来源:发表于2019-04-17 11:11 被阅读0次

    [TOC]

    开始前的BB

    想来想去,感觉有些关于FFmpeg的细节和复杂的地方我jio的还是要单独给大家讲讲的,毕竟之前碰到的时候也是花了点功夫才理解,再次举几个栗子

    1. FFmpeg的Log机制
    2. FFmpeg里的时间计算
    3. FFmpeg的内存模型
    4. 播放器框架流程(简版)

    这章讲的也是在播放器开发中比较重要的知识,希望大家能仔细看,如果有不懂的,可以加我微信或者微信群进行交流,要开始了

    开始

    FFmpeg的Log机制

    FFmpeg中日志的话主要的方法就是av_log(),在libavutil\log.h里,我们来看一下他的方法

    /**
    * @param avcl  包含一个AVClass的结构体
    * @param level 错误等级
    * @param fmt   抛出带有format信息的日志信息
    * @param ...   fmt中所需要的数据
    **/
    void av_log(void *avcl, int level, const char *fmt, ...) 
    
    

    Log的等级有以下几个

    //不打印输出
    #define AV_LOG_QUIET    -8
    
    //崩溃性的错误
    #define AV_LOG_PANIC     0
    
    //出现无法恢复的问题,比如找不到相应格式的header,或者是传入了非法的参数
    #define AV_LOG_FATAL     8
    
    //出现了问题,无法恢复数据
    #define AV_LOG_ERROR    16
    
    //有点问题,但是可能不影响
    #define AV_LOG_WARNING  24
    
    //输出标准的信息
    #define AV_LOG_INFO     32
    
    //输出更详细的信息
    #define AV_LOG_VERBOSE  40
    
    //输出libav*里面的debug信息 对开发者有用
    #define AV_LOG_DEBUG    48
    
    //非常冗长的调试,对libav*开发非常有用。
    #define AV_LOG_TRACE    56
    
    

    对于Log的输出,我们可以用

    int av_log_get_level(void);
    
    void av_log_set_level(int level);
    

    进行set和get操作来控制和获取日志的等级

    输出log的方法,就是利用void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));这个方法,设置一个函数指针,进行回调。

    FFmpeg中的时间计算

    我们在ffmpeg中时间主要是计算的PTS的时间 常用的时间分为三种:

    1. seconds 秒
    2. microsecond 微秒
    3. 自定义时间(音频的pts中可能会用到)

    这三种时间单位都有不同的用处,但是都可以进行互相换算。

    怎么确定ffmpeg中使用的是以什么时间为基准的呢?

    ffmpeg内部有个定义的宏#define AV_TIME_BASE 1000000,这个宏定义了它的时间基 (1s = AV_TIME_BASE),像这里就是用的微秒(us)做为时间基

    还有另一个AV_TIME_BASE_Q,这个宏的定义完整的是这样的

    #define AV_TIME_BASE_Q (AVRational){1,AV_TIME_BASE}
    

    这个宏是AV_TIME_BASE的分数表示 也就是 1/AV_TIME_BASE

    他们之间的转换关系是

    timestamp(时间戳) = AV_TIME_BASE * time(秒)
    
    time(秒) = AV_TIME_BASE_Q * timestamp(时间戳)
    

    在这里,细心的同学会发现 AVRational 这个结构体在很多地方都用到了,这个结构体的全貌是这样的

    /**
     * Rational number (pair of numerator and denominator).
     */
    typedef struct AVRational{
        int num; ///< Numerator
        int den; ///< Denominator
    } AVRational
    

    它就是简单的记录了一下分子和分母,我们在ffmpeg中可以直接用方法

    /**
     * Convert an AVRational to a `double`.
     * @param a AVRational to convert
     * @return `a` in floating-point form
     * @see av_d2q()
     */
    static inline double av_q2d(AVRational a){
        return a.num / (double) a.den;
    }
    

    直接进行计算,就像这样

    timestamp(秒 = pts * av_q2d(stream->time_base);
    

    就能计算出现在这帧的真实pts是多少秒

    在FFmpeg中存在的多个时间基(time_base) (没有错,就是这么坑),它们对应着不同的阶段,每个time_base的具体值不一样,常见的有

    1. AVFormatContext
      1. durtion 整个码流的时长,获取正常的时长的时候需要去除以AV_TIME_BASE,单位是秒
    2. AVStream
      1. time_base 单位是秒
      2. duration 表示当前数据流的时长,以time_base为单位
    3. AVPacket
      1. pts 以AVStream中的time_base为单位
      2. dts 以AVStream中的time_base为单位
      3. duration 以AVStream中的time_base为单位
    4. AVFrame
      1. pts 以AVStream中的time_base为单位
      2. pkt_dts 以AVStream中的time_base为单位
      3. duration 以AVStream中的time_base为单位

    注意 在使用解码中AVFrame的pts的时候,可以做个时间矫正
    frame->pts = frame->best_effort_timestamp
    这个值一般情况下个pts是一样的,但是在某些情况下,比如丢帧,会进行一些纠正

    FFmpeg的内存模型

    内存模型的话我们这小节主要讲AVPacketAVFrame这两个结构体,因为在播放器的开发中,我们操作的最多的就是这两个结构体,一旦处理不好,发生内存泄漏,显示不正常什么的。。很刺激的。。。

    进入正题

    首先,稍微来看一些AVPacket这个结构体

    typedef struct AVPacket {
        /**
         * 带有引用计数buffer,可能是空的
         */
        AVBufferRef *buf;
        
        /**
         * 当前Packet的pts
         */
        int64_t pts;
        
        /**
         * 当前Packet的dts
         */
        int64_t dts;
        uint8_t *data;
        int   size;
        
        /**
        * 当前Packet的流下标
        **/
        int   stream_index;
        /**
         * A combination of AV_PKT_FLAG values
         */
        int   flags;
        /**
         * Additional packet data that can be provided by the container.
         * Packet can contain several types of side information.
         */
        AVPacketSideData *side_data;
        int side_data_elems;
    
        /**
         * 当前的packet的持续时间
         */
        int64_t duration;
    
        int64_t pos;                            ///< 流中的字节位置,如果未知,则为-1
    
    #if FF_API_CONVERGENCE_DURATION
        /**
         * @deprecated Same as the duration field, but as int64_t. This was required
         * for Matroska subtitles, whose duration values could overflow when the
         * duration field was still an int.
         */
        attribute_deprecated
        int64_t convergence_duration;
    #endif
    } AVPacket;
    

    AVPacet 是一个解封装之后的数据(H264,AAC),假如我们要对这个AVPacket进行拷贝的操作,那么这个时候就需要注意了

    1. 两个AVPacket引用的是统一数据的缓存空间,假如释放一个,另一个也会被释放
    2. 两个AVPacketbuf引用不同的数据缓存空间,每个AVPacket都有数据缓存的拷贝

    分配的时候是不会分配buf的

    我们来简单的测试一下 ,新建chapter_08/AVPacketMemoryModel
    (为什么又采用C++的写法了?没办法,路子就是这么野)
    头文件里

    image

    然后实现方法

    image

    运行之后我们发现

    image

    至于他的原因大家可以去看一下AVPacket *av_packet_alloc(void)方法,一看就知道为什么了

    那么什么时候才回去把数据放进去呢?

    没有错就是调用av_read_fram的方法的时候,才会去对这个数据赋值

    AVPacket数据共享模型,可以用下面这个图

    image

    对于多个AVPacket共享同一个缓存空间,ffmpeg采用引用计数机制(reference-count)

    1. 初始化时引用计数为1
    2. 当有新的Packet引用共享的缓存控件时,将引用计数+1
    3. 当释放了引用共享控件的Packet,就将引用计数-1,引用计数为0时,就释放掉缓存空间

    (AVFrame表示我也是这么做的)

    闭上眼,仔细感受 有没有点智能指针的味道

    基于上面的引用机制,我们在调用AVPacket相关的方法时,就需要注意引用问题了,下面是有关的方法,以及引用的情况

    AVPacket *av_packet_alloc(void);                        //分配AVPacket
    void av_packet_free(AVPacket **pkt);                    //释放AVPacket
    void av_init_packet(AVPacket *pkt);                     //初始化AVPacket
    int av_new_packet(AVPacket *pkt, int size);             //给AVPacket的buf分配内存,引用计数初始化为1
    int av_packet_ref(AVPacket *dst, const AVPacket *src);  //增加引用计数
    void av_packet_unref(AVPacket *pkt);                    //减少引用计数
    void av_packet_move_ref(AVPacket *dst, AVPacket *src);  //转移引用计数
    AVPacket *av_packet_clone(const AVPacket *src); //等于av_packet_alloc()+av_packet_ref()
    

    那么 我们要怎么知道他的引用数呢?有个av_buffer_get_ref_count,可以获取Buffer的引用数

    在原来的程序基础上我们进行修改

    void AVPacketMemoryModel::testAVPacketAlloc() {
    
        AVPacket *packet = av_packet_alloc();
        std::string log = (packet->buf == nullptr) ? "null" : "not null";
        std::cout << log << std::endl;
    
        av_new_packet(packet, 20 * 1024 * 1024);
        memccpy(packet->data, this, 1, 20 * 1024 * 1024);
    
        if (packet->buf) {
            int ret = av_buffer_get_ref_count(packet->buf);
            std::cout<<"当前引用值 :"<<ret<<std::endl;
        }
    
        AVPacket* packet1 = av_packet_alloc();
        av_packet_ref(packet,packet);
    
        if (packet->buf) {
            int ret = av_buffer_get_ref_count(packet->buf);
            std::cout<<"当前引用值 1 :"<<ret<<std::endl;
        }
    
    }
    

    执行的结果是

    image

    AVFrameAVPacket的操作差不多,这里就不费篇幅叙述了

    ** 未完持续 。。。**

    相关文章

      网友评论

          本文标题:八、关于FFmpeg需要絮叨的一些事

          本文链接:https://www.haomeiwen.com/subject/rgpgwqtx.html