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

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的操作差不多,这里就不费篇幅叙述了

** 未完持续 。。。**

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

推荐阅读更多精彩内容

  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,684评论 0 3
  • 因为FFmpeg更新的比较快,API也会跟着有所变动,所以声明一下,本文使用的FFmpeg版本为V3.3.5。 1...
    片片碎阅读 38,270评论 14 24
  • ### YUV颜色空间 视频是由一帧一帧的数据连接而成,而一帧视频数据其实就是一张图片。 yuv是一种图片储存格式...
    天使君阅读 3,273评论 0 4
  • 本来是想写一篇time_base的详细文章,后来发现很多大神写过,那这里我就总结一下各种time_base,方便大...
    耕地阅读 13,663评论 2 15
  • 曾记否,20岁的那年,梳着高高的马尾,怀揣对大学的所有憧憬,走进了我的大学校园。 ...
    强者联盟阅读 169评论 0 0