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

[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++的写法了?没办法,路子就是这么野) 头文件里

然后实现方法

运行之后我们发现

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

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

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

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

对于多个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<<"当前引用值 :"<if (packet->buf) {
        int ret = av_buffer_get_ref_count(packet->buf);
        std::cout<<"当前引用值 1 :"<复制代码

执行的结果是

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

** 未完持续 。。。**

转载于:https://juejin.im/post/5cad78186fb9a06891739472

你可能感兴趣的:(八、关于FFmpeg需要絮叨的一些事)