*****************************************************************************
* ffplay系列博客: *
* ffplay播放器原理剖析 *
* ffplay播放器音视频同步原理 *
* ffplay播放控制代码分析 *
* 视频主观质量对比工具(Visual comparision tool based on ffplay) *
*****************************************************************************
ffplay可以响应各种控制请求,比如快进快退,暂停,单帧播放等,具体是如何做的呢?
ffplay通过获取键盘和鼠标事情来执行各种控制逻辑,函数调用关系如下:
main() -> event_loop() --> refresh_loop_wait_event(cur_stream, &event)
|--> case key_p or SPACE: toggle_pause()
|--> case key_s: step_to_next_frame()
|--> case key_left, right, up, down: stream_seek()
main()函数完成初始化工作后,进入了事件队列的循环,循环中首先调用refresh_loop_wait_event(),该函数先获取事件队列中的事件,没有任何事件输入时,则进行video frame的渲染。如果有事件输入,则响应。
typedef struct Clock {
double pts; /* clock base */
double pts_drift; /* clock base minus time at which we updated the clock */
double last_updated;
double speed;
int serial; /* clock is based on a packet with this serial */
int paused;
int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;
static void toggle_pause(VideoState *is)
{
stream_toggle_pause(is);
is->step = 0;
}
static void stream_toggle_pause(VideoState *is)
{
if (is->paused) { //如果当前状态就是暂停,则接下来进入播放状态,需要更新vidclk
is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
if (is->read_pause_return != AVERROR(ENOSYS)) {
is->vidclk.paused = 0;
}
set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
}
set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused; // pause状态反转
}
static double get_clock(Clock *c)
{
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) { //如果当前是暂停状态,则返回最新的pts即可,因为暂停时时间没走
return c->pts;
} else { // 如果当前正处在播放状态,则返回的时间为最新的pts + 更新pts之后流逝的时间
double time = av_gettime_relative() / 1000000.0;
//这里返回的时间实际为 c->pts + time - c->last_updated
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed); //这里正常速度播放,speed=1.0
}
}
static void set_clock(Clock *c, double pts, int serial)
{
double time = av_gettime_relative() / 1000000.0;
set_clock_at(c, pts, serial, time);
}
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
c->pts = pts;
c->last_updated = time;
c->pts_drift = c->pts - time;
c->serial = serial;
}
// 暂停状态下的read_thread
static int read_thread(void *arg)
{
......
for (;;) {
......
/* if the queue are full, no need to read more */
// 暂停后,这个if条件满足,packet queue中有足够的包,将不再继续读包了
if (infinite_buffer<1 &&
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
......
ret = av_read_frame(ic, pkt);
......
}
}
对sdl_audio_callbakc的影响:sdl_audio_callback不再从audio sample queue中拿解码后的音频数据了,而是直接播放静音。
// 暂停状态下的audio_decode_frame
static int audio_decode_frame(VideoState *is)
{
int data_size, resampled_data_size;
int64_t dec_channel_layout;
av_unused double audio_clock0;
int wanted_nb_samples;
Frame *af;
if (is->paused) // pause状态下直接返回-1,不从audio sample queue拿audio sample了
return -1;
......
}
// 暂停状态下的sdl_audio_callback
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
......
while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {
audio_size = audio_decode_frame(is); //此时函数返回-1
if (audio_size < 0) { // 直接输入静音
/* if error, just output silence */
is->audio_buf = NULL;
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
} else {
if (is->show_mode != SHOW_MODE_VIDEO)
update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
is->audio_buf_size = audio_size;
}
is->audio_buf_index = 0;
}
len1 = is->audio_buf_size - is->audio_buf_index;
if (len1 > len)
len1 = len;
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
else { //暂停状态下直接往stream中填充0,静音
memset(stream, 0, len1);
if (!is->muted && is->audio_buf)
SDL_MixAudio(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1, is->audio_volume);
}
len -= len1;
stream += len1;
is->audio_buf_index += len1;
}
......
}
static Frame *frame_queue_peek_writable(FrameQueue *f)
{
/* wait until we have space to put a new frame */
SDL_LockMutex(f->mutex);
while (f->size >= f->max_size &&
!f->pktq->abort_request) {
printf("frame queue peek writable: f->size %d >= f->max_size %d\n", f->size, f->max_size);
SDL_CondWait(f->cond, f->mutex);
}
SDL_UnlockMutex(f->mutex);
if (f->pktq->abort_request)
return NULL;
return &f->queue[f->windex];
}
video渲染也会停止:
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
double remaining_time = 0.0;
SDL_PumpEvents();
while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_ALLEVENTS)) {
if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
SDL_ShowCursor(0);
cursor_hidden = 1;
}
if (remaining_time > 0.0)
av_usleep((int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)) // pause状态不再刷新video frame
video_refresh(is, &remaining_time);
SDL_PumpEvents();
}
}
/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
if (!is->seek_req) {
is->seek_pos = pos;
is->seek_rel = rel;
is->seek_flags &= ~AVSEEK_FLAG_BYTE;
if (seek_by_bytes)
is->seek_flags |= AVSEEK_FLAG_BYTE;
is->seek_req = 1; // 设置seek request = 1
SDL_CondSignal(is->continue_read_thread);
}
}
seek对read_thread的影响:
static int read_thread(void *arg)
{
VideoState *is = arg;
......
for (;;) {
......
if (is->seek_req) { //执行seek请求
int64_t seek_target = is->seek_pos;
int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
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->filename);
} else {
if (is->audio_stream >= 0) {
packet_queue_flush(&is->audioq); // 将audio packet queue清空
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); // 将video packet queue清空
packet_queue_put(&is->videoq, &flush_pkt);
}
if (is->seek_flags & AVSEEK_FLAG_BYTE) {
set_clock(&is->extclk, NAN, 0);
} else {
set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
}
}
is->seek_req = 0;
is->queue_attachments_req = 1;
is->eof = 0;
if (is->paused)
step_to_next_frame(is);
}
......
ret = av_read_frame(ic, pkt); // 从源文件中读取内容到pkt结构中
/* check if packet is in play range specified by user, then queue, otherwise discard */
stream_start_time = ic->streams[pkt->stream_index]->start_time;
// 下面的duration是通过命令传递给ffplay的指定播放时长的参数,所以判断pkt的时间戳是否在duration内
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
pkt_in_play_range = duration == AV_NOPTS_VALUE ||
(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
av_q2d(ic->streams[pkt->stream_index]->time_base) -
(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
<= ((double)duration / 1000000);
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
packet_queue_put(&is->audioq, pkt); // 读到的pkt为audio,放入audio queue(is->audioq)
} else if (pkt->stream_index == is->video_stream && pkt_in_play_range
&& !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
packet_queue_put(&is->videoq, pkt); // 读到的pkt为video,放入video queue(is->videoq)
} else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
packet_queue_put(&is->subtitleq, pkt); // 读到的pkt为subtitle,放到subtitile queue中
} else {
av_packet_unref(pkt);
}
}
......
}
static void step_to_next_frame(VideoState *is)
{
/* if the stream is paused unpause it, then step */
if (is->paused) // 如果本来是pause状态,则先进入播放状态
stream_toggle_pause(is);
is->step = 1; //设置标志位
}
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);
......
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) {
// nothing to do, no picture to display in the queue
} else {
double last_duration, duration, delay;
Frame *vp, *lastvp;
/* dequeue the picture */
lastvp = frame_queue_peek_last(&is->pictq); //取Video Frame Queue上一帧图像
vp = frame_queue_peek(&is->pictq); //取Video Frame Queue当前帧图像
......
if (is->paused)
goto display;
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp); //计算两帧之间的时间间隔
delay = compute_target_delay(last_duration, is); //计算当前帧与上一帧渲染的时间差
time= av_gettime_relative()/1000000.0;
//is->frame_timer + delay是当前帧渲染的时刻,如果当前时间还没到帧渲染的时刻,那就要sleep了
if (time < is->frame_timer + delay) { // remaining_time为需要sleep的时间
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
is->frame_timer += delay;
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
if (frame_queue_nb_remaining(&is->pictq) > 1) {
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
// 如果当前帧显示时刻早于实际时刻,说明解码慢了,帧到的晚了,需要丢弃不能用于显示了,不然音视频不同步了。
if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}
......
frame_queue_next(&is->pictq);
is->force_refresh = 1; //显示当前帧
if (is->step && !is->paused) //如果当前是单帧播放模式,渲染当前帧之后,马上进入暂停状态,且is->step置为0
stream_toggle_pause(is);
}
display:
/* display picture */
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is);
}
is->force_refresh = 0;
......
}
经过上面的分析,可以了解ffplay如何实现各种操作,对播放器有了更全面的理解了。
版权声明:本文为博主原创文章,未经博主允许请勿转载。