ffplay源码分析7-播放控制

本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10316225.html

ffplay 是 FFmpeg 工程自带的简单播放器,使用 FFmpeg 提供的解码器和 SDL 库进行视频播放。本文基于 FFmpeg 工程 8.0 版本进行分析,其中 ffplay 源码清单如下:
https://github.com/FFmpeg/FFmpeg/blob/n8.0/fftools/ffplay.c

在尝试分析源码前,可先阅读如下参考文章作为铺垫:
[1]. 雷霄骅,视音频编解码技术零基础学习方法
[2]. 视频编解码基础概念
[3]. 色彩空间与像素格式
[4]. 音频参数解析
[5]. FFmpeg基础概念

“ffplay源码分析”系列文章如下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制

7. 播放控制

7.1. 暂停/继续

暂停/继续状态的切换是由用户按空格键实现的,每按一次空格键,暂停/继续的状态翻转一次。

7.1.1 暂停/继续状态切换

函数调用关系如下:

main()
|-> event_loop()
    |-> toggle_pause()
        |-> stream_toggle_pause()

stream_toggle_pause() 实现播放状态翻转:

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;
}

7.1.2 暂停状态下的视频播放

在 video_refresh() 函数中有如下代码:

static void video_refresh(void *opaque, double *remaining_time)
{
    ...
    // 视频播放
    if (is->video_st) {
        ...
        // 暂停处理:不停播放上一帧图像
        if (is->paused)
            goto display;
        ...
    }
    ...
}

在暂停状态下,实际就是不停播放上一帧(最后一帧)图像。画面不更新。

7.2 逐帧播放

逐帧播放是用户每按一次 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;
}

static void video_refresh(void *opaque, double *remaining_time)
{
    ...
    if (is->video_st) {
        ...
        if (is->step && !is->paused)
            stream_toggle_pause(is);    // 逐帧播放模式下,播放一帧画面后暂停
        ...
    }
    ...
}

7.3 SEEK 操作

SEEK 操作就是由用户干预而改变播放进度的实现方式,比如鼠标拖动播放进度条。

7.3.1 数据结构及 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;

seek_pos 表示 SEEK 目标播放点 (后文简称 SEEK 点),seek_rel 表示 SEEK 增量。SEEK 点和 SEEK 增量有两种形式,第一种是文件中的位置 (pos in bytes,单位:字节),第二种是播放时刻 (timestamp,单位:微秒)。

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 各标志的详细说明参考下一节 avformat_seek_file() 的说明。

7.3.2 avformat_seek_file()

avformat_seek_file() 函数原型如下:

/**
 * Seek to timestamp ts.
 * Seeking will be done so that the point from which all active streams
 * can be presented successfully will be closest to ts and within min/max_ts.
 * Active streams are all streams that have AVStream.discard < AVDISCARD_ALL.
 *
 * If flags contain AVSEEK_FLAG_BYTE, then all timestamps are in bytes and
 * are the file position (this may not be supported by all demuxers).
 * If flags contain AVSEEK_FLAG_FRAME, then all timestamps are in frames
 * in the stream with stream_index (this may not be supported by all demuxers).
 * Otherwise all timestamps are in units of the stream selected by stream_index
 * or if stream_index is -1, in AV_TIME_BASE units.
 * If flags contain AVSEEK_FLAG_ANY, then non-keyframes are treated as
 * keyframes (this may not be supported by all demuxers).
 * If flags contain AVSEEK_FLAG_BACKWARD, it is ignored.
 *
 * @param s            media file handle
 * @param stream_index index of the stream which is used as time base reference
 * @param min_ts       smallest acceptable timestamp
 * @param ts           target timestamp
 * @param max_ts       largest acceptable timestamp
 * @param flags        flags
 * @return >=0 on success, error code otherwise
 *
 * @note This is part of the new seek API which is still under construction.
 */
int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);

avformat_seek_file() 函数功能:SEEK 到 ts 参数指定的时间戳位置。这个函数会等待 SEEK 操作完成才返回。实际的播放点力求最接近参数 ts,并确保在 [min_ts, max_ts] 区间内,之所以播放点不一定在 ts 位置,是因为 ts 位置未必能播放。

参数 flags 表示 SEEK 标志,根据 SEEK 标志的不同,SEEK 目标点相关参数 (参数 min_ts, ts, max_ts) 分为如下几种情况:
[1]. AVSEEK_FLAG_BYTE:三个时间戳参数表示文件中的位置 (字节表示)。有些解复用器可能不支持这种情况。
[2]. AVSEEK_FLAG_FRAME:三个时间戳参数表示 stream 中帧序号,stream 由 stream_index 指定。有些解复用器可能不支持这种情况。
[3]. 如果不含上述两种标志,则三个时间戳参数表示时间戳,时间戳的单位是 timebase,如果 stream_index 有效,则 timebase 是 stream_index 指定 stream 中的 timebase;如果 stream_index 无效 (值为 -1),则 timebase 等于 AV_TIME_BASE。
[4]. AVSEEK_FLAG_ANY:三个时间戳参数表示帧序号,支持非关键帧。有些解复用器可能不支持这种情况。
[5]. AVSEEK_FLAG_BACKWARD:忽略此参数。

其中 AV_TIME_BASE 是 FFmpeg 内部使用的时间基,定义如下:

/**
 * Internal time base represented as integer
 */

#define AV_TIME_BASE            1000000

AV_TIME_BASE 表示 1000000 us。

7.3.2 SEEK 的触发方式

当用户按下 "PAGEUP","PAGEDOWN","UP","DOWN","LEFT","RHIGHT" 按键以及用鼠标拖动进度条时,引起播放进度变化,会触发 SEEK 操作。

在 event_loop() 函数进行的 SDL 消息处理中有如下代码片段:

case SDLK_LEFT:
    incr = seek_interval ? -seek_interval : -10.0;  // 后退10.0秒
    goto do_seek;
case SDLK_RIGHT:
    incr = seek_interval ? seek_interval : 10.0;    // 前进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;     // 增量为1秒的数据量
            else
                incr *= 180000.0;
            pos += incr;
            stream_seek(cur_stream, pos, incr, 1);
        } else {
            // 获取同步主时钟的时钟值,实际就是最后播放帧的pts(单位秒,double型)加上流逝至今的时间
            pos = get_master_clock(cur_stream);
            if (isnan(pos))
                pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
            // 时钟值加上增量,作为预期的SEEK点
            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;
            // 先将SEEK相关值记录下来,这些值供后面SEEK操作时使用。此处表示期望的SEEK点是(pos*AV_TIME_BASE)us处
            stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
        }
    break;

前面 7.3.1 节提到过 SEEK 点和 SEEK 增量有两种形式,这两种形式由 seek_by_bytes 变量值决定,seek_by_bytes 变量值和容器格式有关。第一种情况,seek_by_bytes 为真时,AVSEEK_FLAG_BYTE 标志生效,SEEK 点和 SEEK 增量都换算为文件中的位置;第二种情况,seek_by_bytes 为假时,SEEK 点和 SEEK 增量都换算为播放时刻 (pts,单位换算成微秒)。

第一种情况相对简单一些,我们以第二种情况为例对 SEEK 操作进行说明:将同步主时钟 (最后播放帧的 pts,单位:秒) 加上进度增量,即可得到 SEEK 点,然后将 SEEK 点和 SEEK 增量从秒转换为微秒,将数值记录下来,供后续 SEEK 操作时使用。stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0) 就是记录目标播放点和播放进度增量两个参数的。

再看一下 stream_seak() 函数的实现,仅仅是变量赋值:

static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int by_bytes)
{
    if (!is->seek_req) {
        is->seek_pos = pos;
        is->seek_rel = rel;
        is->seek_flags &= ~AVSEEK_FLAG_BYTE;
        if (by_bytes)
            is->seek_flags |= AVSEEK_FLAG_BYTE;
        is->seek_req = 1;
        SDL_CondSignal(is->continue_read_thread);
    }
}

7.3.3 SEEK 操作的实现

在解复用线程主循环中处理了 SEEK 操作,如下:

static int read_thread(void *arg)
{
    ...
    for (;;) {
        ...
        // seek操作
        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);
                if (is->subtitle_stream >= 0)
                    packet_queue_flush(&is->subtitleq);
                if (is->video_stream >= 0)
                    packet_queue_flush(&is->videoq);
                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] 区间内。三个 SEEK 点相关的参数 (实参 seek_min, seek_target, seek_max) 取值方式与 SEEK 标志 (实参 is->seek_flags) 有关。

[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

posted @ 2019-01-24 18:42  叶余  阅读(5879)  评论(2)    收藏  举报