山西省建设银行网站首页,做室内设计的网站,渭南企业网站建设,苏州seo营销系列文章目录
基于 FFmpeg 的跨平台视频播放器简明教程#xff08;一#xff09;#xff1a;FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程#xff08;二#xff09;#xff1a;基础知识和解封装#xff08;demux#xff09;基于 FFmpeg 的跨平台视频…系列文章目录
基于 FFmpeg 的跨平台视频播放器简明教程一FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程二基础知识和解封装demux基于 FFmpeg 的跨平台视频播放器简明教程三视频解码基于 FFmpeg 的跨平台视频播放器简明教程四像素格式与格式转换基于 FFmpeg 的跨平台视频播放器简明教程五使用 SDL 播放视频基于 FFmpeg 的跨平台视频播放器简明教程六使用 SDL 播放音频和视频基于 FFmpeg 的跨平台视频播放器简明教程七使用多线程解码视频和音频基于 FFmpeg 的跨平台视频播放器简明教程八音画同步 文章目录 系列文章目录前言FFmpeg API 中的 Seek 方法avformat_seek_fileSeek 到关键帧GOP Show me the Code发出 seek 命令Demux 线程进行 seek 操作解码线程进行 seek 操作 精准 SeekShow Me The Code Seek 性能优化解码前丢弃非参考帧。GOP 内向后 seek 逻辑优化 总结参考 前言
经过前面八章的学习与代码实现我们的播放器已经能够正常播放视频了接下来我们将加入最常用的 seek 能力让你能够快进/快退。
本文参考文章来自 An ffmpeg and SDL Tutorial -Tutorial 07: Seeking。这个系列对新手较为友好但 2015 后就不再更新了以至于文章中的 ffmpeg api 已经被弃用了。幸运的是有人对该教程的代码进行重写使用了较新的 api你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。
本文的代码在 ffmpeg_video_player_tutorial-my_tutorial07.cpp 和 ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp。
FFmpeg API 中的 Seek 方法
我们想做的事情很简单当用户按下左右键时播放进度快进 5s 或者回退 5s。例如当前播放进度为 1s当按下右键时期望播放进度能够跳跃到 6s。
FFmpeg 提供了 seek 的接口但它没法精确的跳跃到我们期望的位置它有一些限制。关于精准 seek 我们稍后会进行讨论目前让我们将目光关注到 FFmpeg 提供的 seek api 上。
avformat_seek_file
FFmpeg 提供了 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);函数的参数和返回值说明如下
AVFormatContext *s: 媒体文件句柄。它是函数的主要输入指向处理的媒体文件的上下文。int stream_index: 用作时间基准参考的流的索引。如果流索引为-1则所有的时间戳都以AV_TIME_BASE单位处理。如果此标志包含AVSEEK_FLAG_FRAME则流索引中的所有时间戳都以帧为单位。int64_t min_ts: 可接受的最小时间戳。此时间戳定义了ts可能到达的范围下限。int64_t ts: 目标时间戳。这是函数尝试寻找并尽可能接近的时间戳。int64_t max_ts: 可接受的最大时间戳。这个时间戳定义了ts可能到达的范围上限。int flags: 表示寻址标志包含 AVSEEK_FLAG_BYTE、AVSEEK_FLAG_FRAME、AVSEEK_FLAG_ANY、AVSEEK_FLAG_BACKWARD 四个选项用于指定时间戳的单位、是否将非关键帧视为关键帧等等。
首先明确一点seek 到目标时间 ts 上这个 ts 的时间单位是什么在 avformat_seek_file 注释中对 ts 的时间单位做了说明总结下来
如果“flags”包含 AVSEEK_FLAG_BYTE那么所有时间戳都是以字节为单位的它们代表的是文件位置不过这种方式可能不会被所有的demuxers所支持如果“flags”包含 AVSEEK_FLAG_FRAME那么所有的时间戳都是以帧为单位的这些帧在由stream_index 参数指定的那个流中这种方式同样可能不会被所有的demuxers所支持如果 stream_index 为 -1则以 AV_TIME_BASE 为单位否则所有时间戳都以选定的流单元中的时间基为单位表示
在我们的代码实现中并不会使用到 AVSEEK_FLAG_BYTE 或者 AVSEEK_FLAG_FRAME因此忽略上面的 1、2 点。将 stream_index 设置为 -1 是一种常见的选择使得 ts 时间单位为 AV_TIME_BASE也就是 1us。
注意到 avformat_seek_file 作用的对象是 AVFormatContext这也就说它影响的是解封装的结果经过 seek 之后av_read_frame 将读取到新位置的 AVPacket。
Seek 到关键帧
虽然在 “flags” 中包含 AVSEEK_FLAG_BYTE 或者 AVSEEK_FLAG_FRAME 使得可以 seek 到任意位置帧上但在实际使用上这两个 flag 很少被使用。我们最常用的还是 flags 0 或者 flags AVSEEK_FLAG_BACKWARD这种情况下 avformat_seek_file 将会跳转至符合要求的关键帧I帧位置上。
在跳转时如果指定了 AVSEEK_FLAG_BACKWARD 标志则会优先跳转到前一个关键帧否则会优先跳转到后一个关键帧。如果没有关键帧则会跳转到最接近的非关键帧。但是跳转到非关键帧可能会导致解码器出现错误或画面不完整的情况因此在实际应用中一般会尽量跳转到关键帧。
由于avformat_seek_file的特性它会跳转到最接近指定时间戳的关键帧因此在实际应用中你可能会发现跳转的位置并不完全符合预期。例如如果当前播放位置在1秒你希望快进到6秒但是在执行avformat_seek_file后播放位置可能会跳转到8秒。这是因为在8秒处有一个关键帧而在预期的6秒处没有关键帧。所以avformat_seek_file会选择最近的关键帧进行跳转。「精准 seek」中将说明如何处理这种情况此处不表。
GOP
两个关键帧的距离有一个专业的名词来描述即 GOP。GOP全称为Group of Pictures中文译为“图像组”是视频编码中的一个重要概念。
在视频编码中为了提高压缩效率通常会采用帧间预测的方式即利用前后帧之间的相关性只编码和前后帧的差异部分。这样可以大大减少需要编码的数据量从而提高压缩效率。而GOP就是帧间预测的基本单位。
一个GOP由一个I帧开始后面跟随若干个P帧和B帧。I帧是关键帧可以独立解码而P帧和B帧则需要依赖其他帧进行解码。GOP的长度即一个GOP中包含的帧数是可以调整的它直接影响到视频的压缩效率和错误恢复能力。GOP长度越短错误恢复能力越强但压缩效率较低反之GOP长度越长压缩效率越高但错误恢复能力较弱。
此外 GOP 还与视频画面质量有关详细说明请参考
Back to basics: GOPs explained关于GOP和帧率、码率的关系
Show me the Code
讲解往 ffmpeg 中关于 seek 的 api 后现在来说明如何在代码中实现 seek 的逻辑。大体步骤为
通过键盘发出 seek 命令例如按下左右键等Demux 线程收到 seek 的请求后调用 avformat_seek_file 进行 seek 操作Demux 完成 seek 后还需要通知视频/音频解码线程发生了 seek 操作解码线程收到 seek 通知后清理解码器上下文中的缓存信息
接下来对上述步骤做详细的说明
发出 seek 命令
void onEvent(const SDL_Event event) {switch (event.type) {case SDL_KEYDOWN: {switch (event.key.keysym.sym) {case SDLK_LEFT: {play_ctx-doSeekRelative(-5.0);break;}case SDLK_RIGHT: {play_ctx-doSeekRelative(5.0);break;}case SDLK_DOWN: {play_ctx-doSeekRelative(-60.0);break;}case SDLK_UP: {play_ctx-doSeekRelative(60.0);break;}}break;}// ....SDL 支持键盘事件当我们按了某些按键时通过 SDL_Event 中的信息来判断所按的键是哪个。在上述代码中支持左右上下键来快进和快退。
void doSeekRelative(double incr) {if (!seek_req) {std::lock_guard lg(seek_mut);auto pos getAudioClock();pos incr;if (pos 0) {pos 0;}seek_rel (int64_t)(incr * AV_TIME_BASE);seek_pos (int64_t)(pos * AV_TIME_BASE);seek_flags (incr 0) ? AVSEEK_FLAG_BACKWARD : 0;seek_req true;}}doSeekRelative函数中
getAudioClock 获取当前的音频时钟即播放到第几秒了。pos incr; 计算快进快退的目标位置接着计算 seek_rel 和 seek_pos这里将时间单位转换到 AV_TIME_BASE 是为了后面处理更方面seek_flags 如果是快退的话设置为 AVSEEK_FLAG_BACKWARD最后设置 seek_req true 等待 demux 线程消费
Demux 线程进行 seek 操作
if (ctx.seek_req) {// seek stuff goes hereint64_t seek_pos 0;int64_t seek_rel 0;int seek_flags 0;{std::lock_guard lg(ctx.seek_mut);seek_pos ctx.seek_pos;seek_rel ctx.seek_rel;seek_flags ctx.seek_flags;}auto min_ts (seek_rel 0) ? (seek_pos - seek_rel 2) : (INT64_MIN);auto max_ts (seek_rel 0) ? (seek_pos - seek_rel - 2) : (INT64_MAX);ret avformat_seek_file(ctx.decode_ctx-demuxer.getFormatContext(), -1,min_ts, seek_pos, max_ts, seek_flags);if (ret 0) {fprintf(stderr, %s: error while seeking %s\n,decode_ctx.demuxer.getFormatContext()-url, av_err2str(ret));} else {if (ctx.decode_ctx-video_stream_index 0) {decode_ctx.video_packet_sync_que.clear();decode_ctx.video_packet_sync_que.tryPush(flush_packet);}if (ctx.decode_ctx-audio_stream_index 0) {decode_ctx.audio_packet_sync_que.clear();decode_ctx.audio_packet_sync_que.tryPush(flush_packet);}}ctx.setClock(ctx.audio_clock_t, seek_pos / (double)AV_TIME_BASE);ctx.seek_req false;
}上述代码在 demux 线程中它展示了处理 seek 命令的逻辑操作。
在收到 seek 请求后将 seek_pos、seek_rel 和 seek_flags 保存起来为了线程安全接着调用 avformat_seek_file 接口至于 min_ts 和 max_ts 为啥这样算是直接抄的 ffplay 中代码哈哈哈哈。注意 avformat_seek_file 第二个参数是 -1这也意味着所有输入的时间戳单位是 AV_TIME_BASE 为单位的。接着清空视频和音频的 packet queue并向它们都发送了一个 flush packet通过这个 flush packet 去通知解码线程发生了 seek 操作。
解码线程进行 seek 操作
// seek stuff here
if (std::strcmp((char *)pkt-data, FLUSH_DATA) 0) {avcodec_flush_buffers(codec.getCodecContext());out_frame_queue.clear();return 0;
}在解码线程中要做的事情非常简单
判断当前 packet 是否是 flush packet如果是那么调用 avcodec_flush_buffers 清理解码器上下文缓存且清空 out_frame_queue 里的数据
精准 Seek
回到之前的那个问题avformat_seek_file 只能跳转到关键帧在实际应用中你可能会发现跳转的位置并不完全符合预期。例如如果当前播放位置在1秒你希望快进到6秒但是在执行avformat_seek_file后播放位置可能会跳转到8秒。这是因为在8秒处有一个关键帧而在预期的6秒处没有关键帧。
如何解决这个问题要做两件事情
调用 avformat_seek_file 时确保跳转到目标位置target position的前面的关键帧。例如 target_pos 2s 时跳转到关键帧应该满足 pos 2s。调用 avformat_seek_file 时将 flag 设置为 AVSEEK_FLAG_BACKWARD 即可如果没有设置这个标志那么找到的关键帧可能会超过目标时间戳。从关键帧开始解码直到当前位置current position大于目标位置。满足该条件后意味着我们 seek 到了目标位置可以进行该视频帧的播放。
接下来让我们看具体实现的代码 ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp
Show Me The Code
使用 SDL 处理键盘事件的代码与之前是一样的此处不再赘述。直接看 demux 线程与解码线程的修改。
Demux Thread:
if (ctx.seek_req) {// seek stuff goes hereint64_t seek_pos 0;int64_t seek_rel 0;int seek_flags 0;{std::lock_guard lg(ctx.seek_mut);seek_pos ctx.seek_pos;seek_rel ctx.seek_rel;seek_flags ctx.seek_flags;seek_flags AVSEEK_FLAG_BACKWARD;}auto min_ts (seek_rel 0) ? (seek_pos - seek_rel 2) : (INT64_MIN);auto max_ts (seek_rel 0) ? (seek_pos - seek_rel - 2) : (seek_pos);ret avformat_seek_file(ctx.decode_ctx-demuxer.getFormatContext(), -1,min_ts, seek_pos, max_ts, seek_flags);if (ret 0) {fprintf(stderr, %s: error while seeking %s\n,decode_ctx.demuxer.getFormatContext()-url, av_err2str(ret));} else {seek_packet.pos seek_pos;// ...
}Demux 线程代码与之前有两个差异点
seek_flags 被设置为 AVSEEK_FLAG_BACKWARD设置 seek_packet 的 pos 为目标位置告诉解码线程目标位置的值
Decode Thread:
// seek stuff here
if (pkt-stream_index FF_SEEK_PACKET_INDEX) {avcodec_flush_buffers(codec.getCodecContext());out_frame_queue.clear();seeking_flag true;target_seek_pos_avtimebase pkt-pos;return 0;
}解码线程收到 seek packet 后首先做了两件事情
清理缓存。调用 avcodec_flush_buffers 清理解码器上下文缓存清理 out_frame_queue 中的缓存帧保存 seek 信息以便后续的循环解码
while (ret 0) {ret codec.receiveFrame(out_frame);ON_SCOPE_EXIT([out_frame] { av_frame_unref(out_frame); });// need more packetif (ret AVERROR(EAGAIN)) {break;} else if (ret AVERROR_EOF || ret AVERROR(EINVAL)) {// EOF exit loopbreak;} else if (ret 0) {printf(Error while decoding.\n);return -1;}if (seeking_flag) {auto cur_frame_pts_avtimebase av_rescale_q(out_frame-pts, stream_time_base, AV_TIME_BASE_Q);if (cur_frame_pts_avtimebase target_seek_pos_avtimebase) {break;} else {seeking_flag false;}}out_frame_queue.waitAndPush(out_frame);
}在原有的解码逻辑中我们加入了对 seek 的判断
如果命中 seeking_flag判断当前解码帧的 pts 是否小于目标位置。如果是以为还没有解码到目标帧继续解码如果否那么当前帧已经满足目标位置。满足条件后将 seeking_flag 设置为 false将解码的视频帧放入 queue 中让 sdl 去播放。
Seek 性能优化
实现精准 seek 后可以发现在某些情况下需要解码很多很多帧才能到达 seek 的目标位置。举个例子假设 GOP100目标位置在 GOP 的最后一帧调用 avformat_seek_file 后seek 到了当前 GOP 的第一帧于是需要解码 100 帧。有啥优化的办法吗这里大致描述下具体代码留给各位自行实现了。
解码前丢弃非参考帧。
AVPacket 的 flags 标志位中可以知道当前的 packet 是否可以被解码器丢弃。以下是对上述标志位的解释
AV_PKT_FLAG_KEY这个标志表示该数据包包含一个关键帧。在视频编码中关键帧是完整的帧可以独立于其他帧进行解码。AV_PKT_FLAG_CORRUPT这个标志表示该数据包的内容已经损坏。这可能是由于数据传输错误或者编码错误导致的。AV_PKT_FLAG_DISCARD这个标志表示这个数据包在解码后可以被丢弃。这些数据包对于维持解码器的状态是必要的但是对于输出来说并不需要。AV_PKT_FLAG_TRUSTED这个标志表示这个数据包来自一个可信的源。这意味着即使数据包中包含一些不安全的结构如指向数据包外部数据的任意指针也可以被接受。AV_PKT_FLAG_DISPOSABLE这个标志表示这个数据包包含的帧可以被解码器丢弃也就是说这些帧不是参考帧。在视频编码中参考帧是其他帧在预测编码时需要参考的帧而非参考帧则不需要被其他帧参考。
或者使用 AVDiscard 来丢弃某些帧具体参考 百倍变速–解码到底能不能丢 非参考帧 FFmpeg 有话说
GOP 内向后 seek 逻辑优化
如果 seek 的目标位置与当前位置属于同一个 GOP且为向后 seek那么可以优化现有 seek 逻辑无需做其他操作只需等待解码器解码到目标位置即可。
总结
本文介绍了播放器中如何实现快进、快退功能并给出了具体的实现代码还讨论了如何实现精准 seek 逻辑并在最后给出了一些优化的思路。本文的代码在 ffmpeg_video_player_tutorial-my_tutorial07.cpp 和 ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp。
参考
An ffmpeg and SDL Tutorial -Tutorial 07: Seekingffmpeg_video_player_tutorial-my_tutorial07.cppffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp。Back to basics: GOPs explained关于GOP和帧率、码率的关系百倍变速–解码到底能不能丢 非参考帧 FFmpeg 有话说