本文为做者原创,转载请注明出处:http://www.javashuo.com/article/p-ykoqdepd-go.htmlhtml
ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放。本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单以下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.cgit
在尝试分析源码前,可先阅读以下参考文章做为铺垫:
[1]. 雷霄骅,视音频编解码技术零基础学习方法
[2]. 视频编解码基础概念
[3]. 色彩空间与像素格式
[4]. 音频参数解析
[5]. FFmpeg基础概念github
“ffplay源码分析”系列文章以下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制缓存
暂停/继续状态的切换是由用户按空格键实现的,每按一次空格键,暂停/继续的状态翻转一次。数据结构
函数调用关系以下:框架
main() --> event_loop() --> toggle_pause() --> stream_toggle_pause()
stream_toggle_pause()实现状态翻转:ide
/* pause or resume the video */ static void stream_toggle_pause(VideoState *is) { if (is->paused) { // 这里表示当前是暂停状态,将切换到继续播放状态。在继续播放以前,先将暂停期间流逝的时间加到frame_timer中 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; }
在video_refresh()函数中有以下代码:函数
/* called to display each frame */ static void video_refresh(void *opaque, double *remaining_time) { ...... // 视频播放 if (is->video_st) { ...... // 暂停处理:不停播放上一帧图像 if (is->paused) goto display; ...... } ...... }
在暂停状态下,实际就是不停播放上一帧(最后一帧)图像。画面不更新。oop
逐帧播放是用户每按一次s键,播放器播放一帧画现。
逐帧播放实现的方法是:每次按了s键,就将状态切换为播放,播放一帧画面后,将状态切换为暂停。
函数调用关系以下:源码分析
main() --> event_loop() --> step_to_next_frame() --> stream_toggle_pause()
实现代码比较简单,以下:
static void step_to_next_frame(VideoState *is) { /* if the stream is paused unpause it, then step */ if (is->paused) stream_toggle_pause(is); // 确保切换到播放状态,播放一帧画面 is->step = 1; }
/* called to display each frame */ static void video_refresh(void *opaque, double *remaining_time) { ...... // 视频播放 if (is->video_st) { ...... if (is->step && !is->paused) stream_toggle_pause(is); // 逐帧播放模式下,播放一帧画面后暂停 ...... } ...... }
待补充
SEEK操做就是由用户干预而改变播放进度的实现方式,好比鼠标拖动播放进度条。
相关数据变量定义以下:
typedef struct VideoState { ...... int seek_req; // 标识一次SEEK请求 int seek_flags; // SEEK标志,诸如AVSEEK_FLAG_BYTE等 int64_t seek_pos; // SEEK的目标位置(当前位置+增量) int64_t seek_rel; // 本次SEEK的位置增量 ...... } VideoState;
“VideoState.seek_flags”表示SEEK标志。SEEK标志的类型定义以下:
#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward #define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes #define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes #define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
SEEK目标播放点(后文简称SEEK点)的肯定,根据SEEK标志的不一样,分为以下几种状况:
[1]. AVSEEK_FLAG_BYTE
:SEEK点对应文件中的位置(字节表示)。有些解复用器可能不支持这种状况。
[2]. AVSEEK_FLAG_FRAME
:SEEK点对应stream中frame序号(?frame序号仍是frame 的PTS?),stream由stream_index指定。有些解复用器可能不支持这种状况。
[3]. 若是不含上述两种标志且stream_index有效:SEEK点对应时间戳,单位是stream中的timebase,stream由stream_index指定。SEEK点的值由“目标frame中的pts(秒) × stream中的timebase”获得。
[4]. 若是不含上述两种标志且stream_index是-1:SEEK点对应时间戳,单位是AV_TIME_BASE。SEEK点的值由“目标frame中的pts(秒) × AV_TIME_BASE”获得。
[5]. AVSEEK_FLAG_ANY
:SEEK点对应帧序号(待肯定),播放点可停留在任意帧(包括非关键帧)。有些解复用器可能不支持这种状况。
[6]. AVSEEK_FLAG_BACKWARD
:忽略。
其中AV_TIME_BASE
是FFmpeg内部使用的时间基,定义以下:
/** * Internal time base represented as integer */ #define AV_TIME_BASE 1000000
AV_TIME_BASE表示1000000us。
当用户按下“PAGEUP”,“PAGEDOWN”,“UP”,“DOWN”,“LEFT”,“RHIGHT”按键以及用鼠标拖动进度条时,引发播放进度变化,会触发SEEK操做。
在event_loop()
函数进行的SDL消息处理中有以下代码片断:
case SDLK_LEFT: incr = seek_interval ? -seek_interval : -10.0; goto do_seek; case SDLK_RIGHT: incr = seek_interval ? seek_interval : 10.0; goto do_seek; case SDLK_UP: incr = 60.0; goto do_seek; case SDLK_DOWN: incr = -60.0; do_seek: if (seek_by_bytes) { pos = -1; if (pos < 0 && cur_stream->video_stream >= 0) pos = frame_queue_last_pos(&cur_stream->pictq); if (pos < 0 && cur_stream->audio_stream >= 0) pos = frame_queue_last_pos(&cur_stream->sampq); if (pos < 0) pos = avio_tell(cur_stream->ic->pb); if (cur_stream->ic->bit_rate) incr *= cur_stream->ic->bit_rate / 8.0; else incr *= 180000.0; pos += incr; stream_seek(cur_stream, pos, incr, 1); } else { pos = get_master_clock(cur_stream); if (isnan(pos)) pos = (double)cur_stream->seek_pos / AV_TIME_BASE; pos += incr; if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE) pos = cur_stream->ic->start_time / (double)AV_TIME_BASE; stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0); } break;
seek_by_bytes生效(对应AVSEEK_FLAG_BYTE标志)时,SEEK点对应文件中的位置,上述代码中设置了对应1秒数据量的播放增量;不生效时,SEEK点对应于播放时刻。咱们暂不考虑seek_by_bytes生效这种状况。
此函数实现以下功能:
[1]. 首先肯定SEEK操做的播放进度增量(SEEK增量)和目标播放点(SEEK点),seek_by_bytes不生效时,将增量设为选定值,如10.0秒(用户按“RHIGHT”键的状况)。
[2]. 将同步主时钟加上进度增量,便可获得SEEK点。先将相关数值记录下来,供后续SEEK操做时使用。stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
就是记录目标播放点和播放进度增量两个参数的,精确到微秒。调用这个函数的前提是,咱们只考虑8.1节中的第[4]种状况。
再看一下stream_seak()
函数的实现,仅仅是变量赋值:
/* 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; SDL_CondSignal(is->continue_read_thread); } }
在解复用线程主循环中处理了SEEK操做。
static int read_thread(void *arg) { ...... for (;;) { if (is->seek_req) { 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; // FIXME the +-2 is due to rounding being not done in the correct direction in generation // of the seek_pos/seek_rel variables 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 { 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); } 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); } } ...... }
上述代码中的SEEK操做执行以下步骤:
[1]. 调用avformat_seek_file()
完成解复用器中的SEEK点切换操做
// 函数原型 int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags); // 调用代码 ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
这个函数会等待SEEK操做完成才返回。实际的播放点力求最接近参数ts
,并确保在[min_ts, max_ts]区间内,之因此播放点不必定在ts
位置,是由于ts
位置未必能正常播放。
函数与SEEK点相关的三个参数(实参“seek_min”,“seek_target”,“seek_max”)取值方式与SEEK标志有关(实参“is->seek_flags”),此处“is->seek_flags”值为0,对应7.4.1节中的第[4]中状况。
[2]. 冲洗各解码器缓存帧,使当前播放序列中的帧播放完成,而后再开始新的播放序列(播放序列由各数据结构中的“serial”变量标志,此处不展开)。代码以下:
if (is->video_stream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); }
[3]. 清除本次SEEK请求标志is->seek_req = 0;