首页 > 其他分享 >百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!

百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!

时间:2022-10-08 17:01:43浏览次数:50  
标签:index 有话 FFmpeg stream -- st AVDISCARD discard TYPE


昨天周六,群里面还有人在技术交流!!

默默吐槽一下:这些人真卷啊,大周末还搞技术,是游戏不好玩还是电影不好看,他们肯定不知道 EDG 总冠军吧。

百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!_参考帧

一开始是讨论剪映的 100 变速是如何实现,群主作为相关人士肯定就不方便透露这些了,不过也有其他大佬给出了思路。

讨论焦点还是围绕如何丢帧展开的。

百倍变速,比如正常速度下一帧是播放第 1s 时刻的内容,而变速后要播放 100s 时刻了。

此时的逻辑有以下几种情况:

  1. 如果下一个播放时刻要超过目前 GOP 大小了,那么就及时 seek 到离目标 pts 最近的关键帧,比如从 1s 变速后到了 100s ,那就 seek 到第 98s 。
  2. 如果下一个播放时刻在同一 GOP 内了,那就继续往下解码,判断解码后的帧 pts 不需要显示就直接丢弃再接着往后解 ,直到接近了目标时间点就显示。
  3. seek 后的时间点没达到目标时间点的情况,需要继续解码的可以重复第二步。

以上是针对群内大佬的总结,拿着小本本赶紧记下来

此时,还有大佬对解码丢帧给出了其他意见:

百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!_参考帧_02

主要是针对非参考帧的丢帧处理,也是文章的重点内容。

当我们通过 av_read_frame 得到一个 AVPacket 之后,可以判断它的 nal_ref_idc 值来决定是否要丢弃。

如果为 0 ,说明其他帧不需要参考该帧,可以直接丢弃不发送给解码器,而不是解码后再丢帧

如果你不清楚 nal_ref_idc 是什么意思 ?那么可以了解一下 H.264 码流 NALU 的概念。

H.264 码流传输时以 NALU 的形式进行,NALU 主要由一个字节的 HAL Header 和 RBSP 两部分组成。

HAL Header 的组成形式如下图所示:

百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!_参考帧_03

HAL Header 的计算如下:

forbidden_zero_bit(1bit) + nal_ref_idc(2bit) + nal_unit_type(5bit)

nal_unit_type 不同的值代表不同类型的帧,解析 AVPacket 完全可以得到如上的信息,后面在公众号音视频开发进阶继续更新文章详解如何计算

所以,在解码时完全是可以丢弃这些非参考帧的,放心大胆地操作吧。

百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!_ide_04

而且丢非参考帧的操作也是经过了产品亿级检测的,这一点我确实可以作证!!!

FFmpeg 中的丢帧

以上的丢帧逻辑是根据 H.264 规范来的,那么在 FFmpeg 的源码中有没有针对这一逻辑做处理呢?

那必然是有的啊!!!

如果仔细看 ffplay 的源码,在源码中有如下的调用方式:

/* this thread gets the stream from the disk or the network */
static int read_thread(void *arg)
{
// 省略部分代码
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
// AVDISCARD_ALL 抛弃所有的帧
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
st_index[type] = i;
}

// 省略部分代码
if (!video_disable)
st_index[AVMEDIA_TYPE_VIDEO] =
av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)
st_index[AVMEDIA_TYPE_AUDIO] =
av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO],
NULL, 0);

/* open the streams */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
// 开启解码线程
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
// 开启解码线程
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
}
// 省略代码
}

read_thread 方法运行在单独线程上,该方法首先进行解封装操作,然后开启一个线程进行解码,接下来调用 av_read_frame 方法读取 AVPacket 放到队列中供解码线程使用。

在 av_find_best_stream 方法之前先将 discard 置为 AVDISCARD_ALL ,过滤掉 AVStream 中的数据,接下来就是 stream_component_open 操作。


/* open a given stream. Return 0 if OK */
static int stream_component_open(VideoState *is, int stream_index)
{
// 省略部分代码
// AVDISCARD_DEFAULT 默认模式,过滤无效数据
ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
// 省略部分代码
if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
goto out;
// 省略部分代码
case AVMEDIA_TYPE_VIDEO:
// 省略部分代码
if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
goto out;
// 省略部分代码
}

stream_component_open 方法又将 discard 置为了 AVDISCARD_DEFAULT ,仅过滤掉无效数据。

继续跟进这个 discard 字段,就会有新发现了!

关于 discard 的所有类型值和使用方式,FFmpeg 中有如下定义:

/**
* @ingroup lavc_decoding
*/
enum AVDiscard{
/* We leave some space between them for extensions (drop some
* keyframes for intra-only or drop just some bidir frames). */
// 不抛弃,不放弃任何数据
AVDISCARD_NONE =-16, ///< discard nothing
// 丢掉无用的数据,比如 size 为 0 这种
AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
// 丢掉所有的非参考帧
AVDISCARD_NONREF = 8, ///< discard all non reference
// 丢掉所有的双向帧
AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
// 丢掉所有的非内帧
AVDISCARD_NONINTRA= 24, ///< discard all non intra frames
// 丢掉所有的非关键帧
AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
// 丢掉所有帧
AVDISCARD_ALL = 48, ///< discard all
};

所以,完全可以使用 discard 字段来标识解码时丢弃哪些帧。

另外,在 avcodec_send_packet 方法源码注释中也提示了可以通过 AVCodecContext.skip_frame 字段来决定丢弃哪些帧。

/**
* Internally, this call will copy relevant AVCodecContext fields, which can
* influence decoding per-packet, and apply them when the packet is actually
* decoded. (For example AVCodecContext.skip_frame, which might direct the
* decoder to drop the frame contained by the packet sent with this function.)
* 省略部分注释
*/
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

摘录了部分注释内容,写的就很清楚了。

所以,在解码时也可以不用自己解析 AVPacket 的 nal_ref_idc 字段值,直接通过 AVCodecContext.skip_frame 实现同样目的

亲测有效,过滤非关键帧之后,解码出来的全是关键帧了。


百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!_ide_05


标签:index,有话,FFmpeg,stream,--,st,AVDISCARD,discard,TYPE
From: https://blog.51cto.com/u_12127193/5738408

相关文章

  • RabbitMQ 集群迁移与监控
    一、集群迁移RabbitMQ中的集群迁移更多的是用来解决集群故障不可短时间内修复而将所有的数据、客户端连接等迁移到新的集群中,以确保服务的可用性。迁移过程包括元数......
  • DotnetCore3.1出现Method ‘Create‘ in type ... does not have an implementation
    错误提示如下:出现这个问题的原因是安装的Microsoft.EntityFrameworkCore.Design包版本是5.0.17,而它所依赖的其他包版本是3.1.25,解决方案就是将Microsoft.EntityFramework......
  • OpenGL 之 GPUImage 源码分析
    GPUImage是iOS上一个基于OpenGL进行图像处理的开源框架,后来有人借鉴它的想法实现了一个Android版本的GPUImage,本文也主要对Android版本的GPUImage进行分析。概......
  • 动手动脑
    一.Java字段初始化的规律。1.类的构造方法(1)“构造方法”,也称为“构造函数”,当创建一个对象时,它的构造方法会被自动调用。构造方法与类名相同,没有返回值。(2)如果类没......
  • K8s 网关选型:是Ingress 还是 Envoy?
    为了避免混淆,我们先对一些关键定义做一些厘清:传统网关:未作容器化改造,未启用K8s,通过流量网关与业务网关两层网关来构建,流量网关提供全局性的、与后端业务无关的策略配置,例如......
  • Oracle同义词
    Oraclesynonym(同义词)同义词是数据库当前用户通过给另外一个用户的对象创建一个别名,然后可以通过对别名进行查询和操作,等价于直接操作该数据库对象。Oracle同义词常......
  • Spark计算框架的优势及核心功能
    传统的离线计算常见问题是数据反馈慢,无法满足客户进行实时数据做决策的需求。如果说MapReduce计算框架的出现是为了解决离线计算问题,那么Spark计算框架的出现则解决了实时计......
  • Linux CMake 指定gcc编译版本
    背景:无root下手动升级gcc版本为5.5之后,但是由于默认目录/usr/bin下的gcc是4.8.5,在cmake默认使用老版本的gcc,导致cmake失败。解决方案:注意!将下面的yourpath替换成新的gc......
  • 5 分钟完成 ZooKeeper 数据迁移
    作者:草谷前言MSE提供了托管版的ZooKeeper,包含比开源ZooKeeper更强大更稳定的功能,能帮助您免去运维ZooKeeper集群的烦恼,当我们需要从自建ZooKeeper迁移到MSEZooKee......
  • 硬盘连接方式介绍
    硬盘连接方式:硬盘与主板之间有以下三种常见的连接方式 1、硬盘直连主板,连接示意图如下:(补实物图) 特点:(1)该连接方法仅适用于SATA接口的硬盘,SAS接口的硬盘不能直接与主......