ffplay中的播放控制有哪些?
简单来说就是开始、结束、暂停、继续、跳转。
开始和结束前面都有说明,
所以主要分析暂停/恢复和跳转,
暂停 / 恢复
暂停恢复主要通过,stream_toggle_pause接口实现,
这个接口虽然看起来只有暂停的功能,实际上它负责暂停和继续状态的转换,上一次如果是暂停,这一次就继续,反之亦然。当然,如果用户想知道目前是开始还是暂停的状态,可以通过监听开始和暂停的状态来实现,不用自己再去记录状态。
/* 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;
//TODO
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);
//设置所有clock的状态
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
}
使用到get_clock的地方,会发现时间仿佛静止了。。。。。
static double get_clock(Clock *c)
{
if (*c->queue_serial != c->serial)
return NAN;
if (c->paused) {
//当暂停时,返回当前pts,即时间不再流逝了
return c->pts;
} else {
double time = av_gettime_relative() / 1000000.0;
return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}
}
视频的暂停
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
......
//暂停状态下,不调用video_refresh
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
video_refresh(is, &remaining_time);
......
}
}
/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
......
// 视频播放
if (is->video_st) {
......
// 暂停处理:播放上⼀帧图像
if (is->paused)
goto display;
......
}
......
}
暂停时不再读取数据
static int read_thread(void *arg) {
......
//当暂停状态发生变化时
if (is->paused != is->last_paused) {
is->last_paused = is->paused;
if (is->paused)
//发送RTSP的暂停消息,让设备暂停发数据
is->read_pause_return = av_read_pause(ic);
else
//发送RTSP的恢复消息,让设置继续发数据
av_read_play(ic);
......
}
......
}
音频的暂停
static int audio_decode_frame(VideoState *is) {
......
//直接返回-1
if (is->paused)
return -1;
......
}
暂停时,音频播放器还在取数据,但取到的都是静音数据
/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
.......
audio_size = audio_decode_frame(is);
if (audio_size < 0) {
//静音时,audio_size
is->audio_buf = NULL;
is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
}
.......
//判断audio_buf为空,塞入静音包
memset(stream, 0, len1);
.......
}
SEEK
seek操作,就是从一个位置,跳到另外一个位置播放。
seek 的方式有两种,按字节跳转和按时间跳转,
按字节跳转一般用于特定的格式,比如ts流,每一个包都是188字节,解析器按这种方式处理可以很方便的跳到目标位置。
按时间跳转比较常见,比如在视频播放过程中快进30s,倒退30s,跳转到指定时间等。
VideoState中的seek相关标识位
typedef struct VideoState {
......
int seek_req; // 标识一次SEEK请求
/*
#define AVSEEK_FLAG_BACKWARD 1 ///< 往指定时间戳后跳转(默认往前找)
#define AVSEEK_FLAG_BYTE 2 ///< 按字节跳转
#define AVSEEK_FLAG_ANY 4 ///< 可以跳转到非关键帧的读取位置
#define AVSEEK_FLAG_FRAME 8 ///< 按帧序号跳转
*/
int seek_flags; // SEEK标志,定义如上
int64_t seek_pos; // SEEK的目标位置(当前位置+增量)
int64_t seek_rel; // 本次SEEK的位置增量
......
} VideoState;
seek源码解析
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
//判断是否已有seek请求,seek过程中用户操作无效
if (!is->seek_req) {
//设置seek的目标位置
is->seek_pos = pos;
//设置seek的增量
is->seek_rel = rel;
//设置按字节跳转
is->seek_flags &= ~AVSEEK_FLAG_BYTE;
if (seek_by_bytes)
is->seek_flags |= AVSEEK_FLAG_BYTE;
is->seek_req = 1;
//触发一下read线程运行
SDL_CondSignal(is->continue_read_thread);
}
}
static int read_thread(void *arg)
{
......
for (;;) {
//判断seek请求
if (is->seek_req) {
//seek的目标位置
int64_t seek_target = is->seek_pos;
//seek的最小位置,如果增量 > 0,那最小位置为原位,否则为INT64_MIN
int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
//seek的最大位置,如果增量 < 0,那最大位置为原位,否则为INT64_MAX
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 {
//每次seek都要把前面的包清空,这样能快速的跳转,否则用户点了seek,还要把缓存中的帧给播完,用户操作起来就不太丝滑
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);
}
}
......
}
以上就是ffplay的播放控制的流程,用户操作主要是改变播放器的状态,然后收包线程和渲染线程根据状态做响应的处理即可,还是比较容易理解的。
ffplay源码分析(一)主函数
ffplay源码分析(二)stream_open
ffplay源码分析(六)音视频同步
未完待续…