我的ffmpeg开源项目地址Viktor_ffmpeg
该项目主要以学习ffmpeg为主,代码中将ffplay摘抄出来(主要去除了sdl,使用c++的std),用自己的方式实现(面向对象)。
从该项目中你可以学到
1.如何使用ffmpeg解码视频并播放
2.如何多个视频串行播放
该项目是以剪映的功能为目标
ffplay源码中多处用到serial概念,用于区分是否连续的数据。
serial主要存在于如下几个ffplay的结构体中
typedef struct MyAVPacketList {
AVPacket pkt;
struct MyAVPacketList *next;
int serial;
} MyAVPacketList;
typedef struct PacketQueue {
MyAVPacketList *first_pkt, *last_pkt;
.....
int serial;
.....
} PacketQueue;
typedef struct Decoder {
.....
int pkt_serial;
.....
} Decoder;
typedef struct Clock {
.....
int serial; /* clock is based on a packet with this serial */
.....
int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
typedef struct Frame {
.....
int serial;
double pts; /* presentation timestamp for the frame 帧的表示时间戳,即相对总时长的偏移位置*/
double duration; /* estimated duration of the frame 帧的估计持续时间*/
int64_t pos; /* byte position of the frame in the input file 帧的字节位置*/
.....
} Frame;
typedef struct VideoState {
.....
int audio_clock_serial;
.....
}
首先分析PacketQueue和MyAVPacketList中的serial
所有结构体中的serial的赋值、更改都是由这两个结构体中的serial更改开始或者引起的。
PacketQueue.serial == MyAVPacketList.serial
在 packet_queue_put_private方法中,保证二者相等,并且二者的值的改变永远在该方法中。
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList *pkt1;
.....
//如果放入的是flush_pkt,需要增加 队列的序列号,以区分不连续的两段数据
if (pkt == &flush_pkt)
q->serial++;
pkt1->serial = q->serial;//用队列的序列号 标记节点
.....
return 0;
}
什么情况下PacketQueue.serial 和MyAVPacketList.serial的值会更改??
static void packet_queue_start(PacketQueue *q)
{
.....
/**
这里放入了一个flush_pkt
fush_pkt定义是static AVPacket flush_pkt;
是一个特殊的packet,主要用来作为非连续的两端数据的“分界”标记
*/
packet_queue_put_private(q, &flush_pkt);
.....
}
static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
int ret;
.....
ret = packet_queue_put_private(q, pkt);
.....
if (pkt != &flush_pkt && ret < 0)
av_packet_unref(pkt);
return ret;
}
这2个方法packet_queue_start、packet_queue_put分别在如下情况调用
packet_queue_start:
在stream_component_open中的switch case语句中会依次打开video,auido,subtitle开始解码:decoder_start---->packet_queue_start
即开始解码时,会往各自的PacketQueue中放入一个flush_pkt
packet_queue_put:
packet_queue_put方法被调用的地方很多,主要看read_thread方法中后面的for语句中seek相关操作。在seek成功后,会先调用packet_queue_flush清除PacketQueue的缓存,然后调用packet_queue_put往PacketQueue中放入一个flush_pkt,标记发生一次seek
if (is->seek_req) {
.....
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR,
"%s: error while seeking\n", is->ic->url);
} else {
/*
1.清除PacketQueue的缓存,并放入一个flush_pkt。放入的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据
2.同步外部时钟
*/
if (is->audio_stream >= 0) {
packet_queue_flush(&is->audioq);
packet_queue_put(&is->audioq, &flush_pkt);
}
if (is->subtitle_stream >= 0) {
packet_queue_flush(&is->subtitleq);
packet_queue_put(&is->subtitleq, &flush_pkt);
}
if (is->video_stream >= 0) {
packet_queue_flush(&is->videoq);
packet_queue_put(&is->videoq, &flush_pkt);
}
.....
}
.....
}
分析到这里可以看出:
serial值的改变都是因为放入一个flush_pkt导致!
放入flush_pkt时机:
1.一个是在开始解码时,会往各自的PacketQueue中放入一个flush_pkt,导致serial初次更改,此时serial=1;
2.其他的改变都是在seek成功后清除了PacketQueue数据后放入一个flush_pkt,并且都是累加,例如运行起来后,seek了一次,这时serial=2
Decoder.pkt_serial
Decoder.pkt_serial的赋值在packet_queue_get方法中
decoder_decode_frame--->do while(1)---->if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial)
在video,audio,subtitle3个解码线程各自解码时,即decoder_decode_frame方法中的do while(1)语句中,从各自的PacketQueue中获取AVPacket时给Decoder.pkt_serial赋值的,
do {
if (d->queue->nb_packets == 0)
.....
if (d->packet_pending) {
} else {
if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
return -1;
}
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(&pkt);
} while (1);
----------------------------
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
MyAVPacketList *pkt1;
.....
for (;;) {
.....
if (serial)//如果需要输出serial,把serial输出
*serial = pkt1->serial;
av_free(pkt1);
.....
} else if (!block) {
.....
} else {
.....
}
}
.....
return ret;
}
那么Decoder的pkt_serial的作用,或者pkt_serial都用在什么地方呢?
1.在decoder_decode_frame方法中比较是否一个连续的流"d->queue->serial == d->pkt_serial"
2.在decoder_decode_frame方法中取一个packet的do while中packet_queue_get方法中赋值,然后比较是否同一个流之后跳出do while
3.在get_video_frame方法中,丢帧处理时做if条件判断使用is->viddec.pkt_serial == is->vidclk.serial
4.在audio_thread中给Frame.serial赋值 af->serial = is->auddec.pkt_serial
5.在vidoe_thread的queue_picture方法中给Frame.serial赋值 vp->serial = serial
6.在subtitle_thread中给Frame.serial赋值 sp->serial = is->subdec.pkt_serial
7.在decoder_decode_frame方法中给Decoder.finished赋值
关于Decoder.finished赋值主要也在decoder_decode_frame方法中,
一次是解码完成时:d->finished = d->pkt_serial
一次是:d->finished = 0(过滤掉flush_pkt这种无效数据)
Decoder.finished主要作用在read_thread方法中判断是否播放完成,决定是要循环播放还是退出,
例如is->auddec.finished == is->audioq.serial
关于Decoder.finished在read_thread方法判断是否播放完成,可以如下追踪finished的来源decoder_decode_frame-->packet_queue_get(PacketQueue.serial-->MyAVPacketList.serial-->Decoder.pkt_serial-->Decoder.finished) -->read_thread-->is->auddec.finished == is->audioq.serial && is->viddec.finished == is->videoq.serial)
Frame.serial
来自于:
1.视频-->video_thread解码---->queue_picture入队----->is->viddec.pkt_serial----->vp->serial = serial
2.音频-->audio_thread解码---->frame入队列---->af->serial = is->auddec.pkt_serial'
3.文字-->subtitle_thread解码---->frame入队列---->sp->serial = is->subdec.pkt_serial
即Frame的serial来自音频,视频,文字解码时赋值,并且都是来自各自的解码Decoder.pkt_serial:
PacketQueue.serial-->MyAVPacketList.serial-->Decoder.pkt_serial-->Frame.serial
作用:
1.frame_queue_last_pos 条件判断是否同流
2.video_refresh 中:
----a.vp->serial != is->videoq.serial条件判断,标记一个节点已经被读过
----b.lastvp->serial != vp->serial 条件判断,更新VideoState.frame_timer
----c.update_video_pts 用于给视频时钟和外部时钟更新serial(见Clock.serial分析)
----d.vp_duration 条件判断是否同流
----e.字幕展示逻辑中,条件判断是否同流
3.sdl_audio_callback-->audio_decode_frame-->do while中做条件判断4.sdl_audio_callback-->audio_decode_frame---->is->audio_clock_serial = af->serial给audio_clock_serial赋值
其实最主要作用就是更新Clock.serial
Clock.serial和Clock.queue_serial
在set_clock_at中赋值:set_clock-->set_clock_at
set_clock方法主要被调用关键点:
1.read_thread中seek成功后给extclk.serial=0(即seek成功后将同步外部时钟extclk.serial清0)
2.set_clock_speed-->set_clock方法改变速度时,给外部时钟extclk.serial赋值,即自己给自己赋值
3.stream_toggle_pause-->set_clock方法暂停播放时,给vidclk,extclk各自的serial赋值,即自己给自己赋值
4.update_video_pts-->set_clock和sync_clock_to_slave(video_refresh视频)
sdl_audio_callback->set_clock和sync_clock_to_slave(sdl_audio_callback音频)sync_clock_to_slave-->set_clock即音频,视频各自的时钟同步到extclk
作用:
1.get_clock中用于判断Clock中的serial是否同一个流(*c->queue_serial != c->serial)
2.在get_video_frame方法中,丢帧处理时做if条件判断使用is->viddec.pkt_serial == is->vidclk.serial
Clock.queue_serial
(在init_clock中赋值,来自于PacketQueue.serial的地址,即指针,所以在packet_queue_put_private中给
PacketQueue.serial赋值,即给Clock.queue_serial赋值
作用:get_clock中用于判断Clock中的serial是否同一个流(*c->queue_serial != c->serial)
VideoState.audio_clock_serial
audio_clock_serial感觉比较鸡肋,这个值就是给音频重采样完成后同步audclk中的serial中使用的(即调用set_clock_at方法)。
sdl_audio_callback-->audio_decode_frame
1.在audio_decode_frame中使用Frame结构体中的serial给audio_clock_serial赋值(is->audio_clock_serial = af->serial);
2.回到sdl_audio_callback后去同步audclk时再用audio_clock_serial给audclk.serial赋值。
视频同步时钟直接调用update_video_pts,使用的serial就是来自Frame.serial
而音频同步却绕了一步,将Frame.serial赋值给audio_clock_serial,再在sdl_audio_callback中使用audio_clock_serial去同步时钟。
这里考虑的原因可能是sdl_audio_callback方法是opensles中调用的,在sdl_audio_callback方法中获取不到Frame,所以采用这种方式。
整理!!
先让我们整理下整个流程,ffplay的main函数是入口函数!
main-->stream_open
stream_open会启动一个read_thread线程,
在read_thread中干了2件大事
1.启动3个流(video,audio,subtitle)stream_component_open。
2.for循环去读取AVPacket,将AVPacket放入3个流各自的PacketQueue中。
接着3个流各自的stream_component_open中又启动了线程:
video_thread,audio_thread,subtitle_thread
到这里为止,共启动了4个线程
1.read_thread(主要读取AVPacket,存入3个流的PacketQueue中)
2.video_thread--->get_video_frame--->decoder_decode_frame
3.audio_thread--->decoder_decode_frame
4.subtitle_thread--->decoder_decode_frame
video_thread,audio_thread,subtitle_thread 3个线程都调用了decoder_decode_frame方法去解码,
就是将read_thread中for循环读取的AVPacket取出去解码,然后将解码后的Frame放入3个流各自的FrameQueue中
关于展示画面和播放声音
main-->event_loop-->refresh_loop_wait_event--->video_refresh
在video_refresh中不停的去视频对应的FrameQueue中获取AVFrame去显示
至于声音播放是在音频的stream_component_open--->audio_open---->打开了opensles去播放音频,opensles会不停的调用sdl_audio_callback
sdl_audio_callback--->audio_decode_frame(重采样)
整个流程主要是这些,回到主题,分析serial!
在3个流的stream_component_open中,都有
decoder_init--->d->queue = queue和d->pkt_serial = -1
decoder_start--->packet_queue_start:会将一个flush_pkt放入到视频PacketQueue中,且这时PacketQueue.serial == MyAVPacketList.serial == 1
(注意这里我们只分析视频流,另外2个流几乎相同,暂不分析)
此时PacketQueue.serial == MyAVPacketList.serial == 1,Decoder.pkt_serial = -1
当read_thread正常启动,正常读取一个AVPacket,假设为avPacket1存入PacketQueue中(记住此时PacketQueue.first_pkt==flush_pkt,PacketQueue.last_pkt==avPacket1)
目前为止
PacketQueue.serial == MyAVPacketList.serial == 1,Decoder.pkt_serial = -1
PacketQueue中的first_pkt==flush_pkt和last_pkt==avPacket1两个MyAVPacketList的serial都是1
同时video_thread运行
video_thread中的for循环启动--->get_video_frame--->decoder_decode_frame
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
AVPacket pkt;
.....
if (d->queue->serial == d->pkt_serial) {
do {
if (d->queue->abort_request)
return -1;
switch (d->avctx->codec_type) {
case AVMEDIA_TYPE_VIDEO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
if (decoder_reorder_pts == -1) {
frame->pts = frame->best_effort_timestamp;
} else if (!decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
....
break;
}
if (ret == AVERROR_EOF) {
d->finished = d->pkt_serial;
avcodec_flush_buffers(d->avctx);
return 0;
}
if (ret >= 0)
return 1;
} while (ret != AVERROR(EAGAIN));
}
do {
if (d->queue->nb_packets == 0)
SDL_CondSignal(d->empty_queue_cond);
if (d->packet_pending) {//如果有待重发的pkt,则先取待重发的pkt
av_packet_move_ref(&pkt, &d->pkt);
d->packet_pending = 0;
} else {
if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
return -1;
}
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(&pkt);
} while (1);
if (pkt.data == flush_pkt.data) {
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
} else {
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
.....
} else {
if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
d->packet_pending = 1;
av_packet_move_ref(&d->pkt, &pkt);
}
}
av_packet_unref(&pkt);
}
}
}
decoder_decode_frame方法:
第一次进入该方法,for循环开始,
1.if (d->queue->serial == d->pkt_serial)不满足,因为上面的PacketQueue.serial=1,Decoder.pkt_serial = -1
2.接着走do while(1)--->packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial),取出数据,该数据为flush_pkt,重点!!!这时在packet_queue_get方法中Decoder.pkt_serial已经为1了
3.继续进入下面的if判断,因为是flush_pkt,所以执行avcodec_flush_buffers,d->finished = 0等操作
第2次for循环
1.if (d->queue->serial == d->pkt_serial)满足,因为此时PacketQueue.serial=1,Decoder.pkt_serial = 1
2.进入do while (ret != AVERROR(EAGAIN))---->avcodec_receive_frame,去获取解码后的数据,这时ret==AVERROR(EAGAIN),可以看下avcodec_receive_frame方法返回值说明
,AVERROR(EAGAIN)说明(output is not available in this state - user must try to send new input,大致意思就是这个状态下表明输入端还么有数据,就是avcodec_send_packet还没有输入数据)
3.跳出do while(ret != AVERROR(EAGAIN)),接着执行下面的do while (1)获取一个AVPacket,此时是avPacket1
4.执行下面的if判断,走else,然后将数据通过avcodec_send_packet输入
(这里多说一句,如果avcodec_send_packet返回AVERROR(EAGAIN),说明输出端应该及时调用avcodec_receive_frame读取数据,然后再重新发送一次该AVPacket。
可以看到,d->packet_pending = 1,然后do while(1)逻辑中
if (d->packet_pending)也是将数据再次赋值,而不是从packet_queue_get中获取)
第3次for循环
1.if (d->queue->serial == d->pkt_serial)满足,因为此时PacketQueue.serial=1,Decoder.pkt_serial = 1
2.进入do while (ret != AVERROR(EAGAIN))---->avcodec_receive_frame,去获取解码后的数据,此时正常逻辑ret==0,跳出整个for循环,decoder_decode_frame方法返回
综上decoder_decode_frame---->get_video_frame--->video_thread将解码得到的AVFrame存入FrameQueue---->queue_picture(....., is->viddec.pkt_serial)
---->Frame.serial=viddec.pkt_serial
到这里video_thread中的for循环已经走了一遍,此时总结下
PacketQueue.serial == MyAVPacketList.serial == 1,Decoder.pkt_serial == 1,存入FrameQueue中的Frame.serial=1
然后video_thread中的for循环一直执行,不停从PacketQueue中获取AVPacket去解码然后存入FrameQueue
到目前为止只剩下Clock的serial还未赋值(audclk,vidclk,extclk)
这个肯定是在去展示画面或者播放音频的时候去赋值的
1.在video_refresh--->update_video_pts--->set_clock_at和sync_clock_to_slave,会分别给vidclk.serial和extclk.serial赋值
2.在音频播放过程中sdl_audio_callback--->set_clock_at和sync_clock_to_slave,会分别给audclk.serial和extclk.serial赋值
并且赋值的serial都是来自Frame.serial,目前都是1
这时如果发生seek操作,那么上面说的read_thread线程中for循环读取AVPacket过程中,会检查是否有seek操作,一旦有就会将3个流各自的PacketQueue清空(不会清空PacketQueue.serial),并放入一个flush_pkt,且serial++。
此时PacketQueue.serial == MyAVPacketList.serial == 2,并且for循环继续往下执行读取AVPacket,假设此时读取一个AVPacket为avPacket2存入PacketQueue中
(记住此时PacketQueue.first_pkt==flush_pkt,PacketQueue.last_pkt==avPacket2)
目前因为seek操作之后
PacketQueue.serial == MyAVPacketList.serial == 2,Decoder.pkt_serial = 1
PacketQueue中的first_pkt==flush_pkt和last_pkt==avPacket2两个MyAVPacketList的serial都是2
3个流的decoder_decode_frame方法还在继续执行,关于decoder_decode_frame方法的再次for循环不用在分析了,和上面逻辑是一样的