[TOC]
开始前的BB
想来想去,感觉有些关于FFmpeg的细节和复杂的地方我jio的还是要单独给大家讲讲的,毕竟之前碰到的时候也是花了点功夫才理解,再次举几个栗子
FFmpeg
的Log机制FFmpeg
里的时间计算FFmpeg
的内存模型- 播放器框架流程(简版)
这章讲的也是在播放器开发中比较重要的知识,希望大家能仔细看,如果有不懂的,可以加我微信或者微信群进行交流,要开始了
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的时间 常用的时间分为三种:
- seconds 秒
- microsecond 微秒
- 自定义时间(音频的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的具体值不一样,常见的有
- AVFormatContext
- durtion 整个码流的时长,获取正常的时长的时候需要去除以AV_TIME_BASE,单位是秒
- AVStream
- time_base 单位是秒
- duration 表示当前数据流的时长,以time_base为单位
- AVPacket
- pts 以AVStream中的time_base为单位
- dts 以AVStream中的time_base为单位
- duration 以AVStream中的time_base为单位
- AVFrame
- pts 以AVStream中的time_base为单位
- pkt_dts 以AVStream中的time_base为单位
- duration 以AVStream中的time_base为单位
注意 在使用解码中AVFrame的pts的时候,可以做个时间矫正 frame->pts = frame->best_effort_timestamp
这个值一般情况下个pts是一样的,但是在某些情况下,比如丢帧,会进行一些纠正
FFmpeg的内存模型
内存模型的话我们这小节主要讲AVPacket
和AVFrame
这两个结构体,因为在播放器的开发中,我们操作的最多的就是这两个结构体,一旦处理不好,发生内存泄漏,显示不正常什么的。。很刺激的。。。
进入正题
首先,稍微来看一些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
进行拷贝的操作,那么这个时候就需要注意了
- 两个
AVPacket
引用的是统一数据的缓存空间,假如释放一个,另一个也会被释放 - 两个
AVPacket
的buf
引用不同的数据缓存空间,每个AVPacket
都有数据缓存的拷贝
分配的时候是不会分配buf的
我们来简单的测试一下 ,新建chapter_08/AVPacketMemoryModel
(为什么又采用C++的写法了?没办法,路子就是这么野) 头文件里
运行之后我们发现
至于他的原因大家可以去看一下AVPacket *av_packet_alloc(void)
方法,一看就知道为什么了
那么什么时候才回去把数据放进去呢?
没有错就是调用av_read_fram
的方法的时候,才会去对这个数据赋值
AVPacket
的数据共享模型,可以用下面这个图
对于多个AVPacket
共享同一个缓存空间,ffmpeg采用引用计数机制(reference-count)
- 初始化时引用计数为1
- 当有新的Packet引用共享的缓存控件时,将引用计数+1
- 当释放了引用共享控件的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 :"<复制代码
执行的结果是
AVFrame
和AVPacket
的操作差不多,这里就不费篇幅叙述了
** 未完持续 。。。**