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

ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放。本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单如下:

https://github.com/FFmpeg/FFmpeg/blob/n4.1/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()实现状态翻转:

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

7.1.2 暂停状态下的视频播放

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

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

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;
}
/* 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); // 逐帧播放模式下,播放一帧画面后暂停
......
} ......
}

7.3 播放速度控制

待补充

7.4. SEEK操作

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

7.4.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;

“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。

7.4.2 SEEK的触发方式

当用户按下“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);
}
}

7.4.3 SEEK操作的实现

在解复用线程主循环中处理了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;

ffplay源码分析7-播放控制的更多相关文章

  1. ffplay源码分析5-图像格式转换

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10311376.html ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg ...

  2. ffplay源码分析6-音频重采样

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10312713.html ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg ...

  3. ffplay源码分析4-音视频同步

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10307089.html ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg ...

  4. ffplay源码分析3-代码框架

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

  5. ffplay源码分析2-数据结构

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

  6. ffplay源码分析1-概述

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10301215.html ffplay是一个很简单的播放器,但是初次接触仍会感到概念和细节 ...

  7. 一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  8. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  9. vlc源码分析(三) 调用live555接收RTSP数据

    首先了解RTSP/RTP/RTCP相关概念,尤其是了解RTP协议:RTP与RTCP协议介绍(转载). vlc使用模块加载机制调用live555,调用live555的文件是live555.cpp. 一. ...

随机推荐

  1. NW.js安装原生node模块node-printer控制打印机

    1.安装原生node模块 #全局安装nw-gyp npm install -g nw-gyp #设置目标NW.js版本 set npm_config_target=0.31.4 #设置构建架构,ia3 ...

  2. 【转】ABP webapi三种方式

    作者:圣杰 链接:https://www.jianshu.com/p/d14733432dc2 來源:简书 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 1. 引言 上一节我们 ...

  3. Ubuntu18.04 - 安装深度桌面(Deepin Linux Desktop)

    深度Linux官网:https://www.deepin.org 深度桌面感觉确实不错,所以打算安装上来,以后体验一下,下面是安装的命令: 1, sudo add-apt-repository ppa ...

  4. 带你走进二进制-一次APT攻击分析

    原文:https://osandamalith.com/2017/06/04/apt-attack-in-bangladesh/ 由prison翻译整理,首发i春秋   引言;   这是一次来自遥远国 ...

  5. npm 包 升降版本

    今天用vue-awesome-swiper最新版本遇到些问题,需要调整到2.6.7版本.记录以下. 查看vue-awesome-swiper版本 npm list vue-awesome-swiper ...

  6. underscore.js源码研究(7)

    概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...

  7. 移动端font-size适配方案

    概述 这是我研究移动端页面时的思考,记录下来供以后开发时参考,相信对其他人也有用.由于我写移动端页面写的还比较少,一些问题都还没遇到,所以我的这篇博文不免有些错误的地方,还请大佬多多指正. 这篇文章是 ...

  8. java批量解压文件夹下的所有压缩文件(.rar、.zip、.gz、.tar.gz)

    // java批量解压文件夹下的所有压缩文件(.rar..zip..gz..tar.gz) 新建工具类: package com.mobile.utils; import com.github.jun ...

  9. POJ 2487

    #include<iostream> #include<stdio.h> using namespace std; int compare(void const * i,voi ...

  10. mxonline 总结

    课程相关 课程列表 课程的剪接 课程详情 课程章节 课程关联的授课机构,课程关联的授课教师 热门课程 相关课程推荐 课程留言 需要登录 若未登录,返回到登录页面 留言失败反馈信息 留言成功,异步刷新页 ...