音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

news2024/11/15 10:09:44

一、引言

从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts:

打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显示时间戳;“dts”实际是AVPacket结构体中的成员变量dts,是以AVStream->time_base为单位的解码时间戳。音频跟视频不一样,音频没有B帧,所以音频的pts和dts输出顺序一样,即pts等于dts。上述的这些值都是通过fftools/ffprobe.c中的show_packet函数打印出来的:

static void show_packet(WriterContext *w, InputFile *ifile, AVPacket *pkt, int packet_idx)
{
//...
    print_ts  ("pts",             pkt->pts);
//...
    print_ts  ("dts",             pkt->dts);
//...
}

本文讲述上述pts、dts的值是怎样被计算出来的。如果想直接看结论,可以跳到本文的最后,直接看“总结”。

二、FFmpeg源码中计算WAV音频文件每个packet的pts和dts的实现

FFmpeg得到每个packet的pts和dts的过程,实际也是解封装(解复用)的过程。

(一)对FFFormatContext结构体的AVPacket类型成员变量pkt进行初始化

FFmpeg对WAV音频文件进行解封装(解复用)时,首先会调用avformat_alloc_context函数分配解复用器上下文(AVFormatContext)。而avformat_alloc_context函数内部会调用av_packet_alloc函数给FFFormatContext结构体的AVPacket类型成员变量pkt分配内存,对pkt的成员变量进行初始化:

AVFormatContext *avformat_alloc_context(void)
{
    FFFormatContext *const si = av_mallocz(sizeof(*si));
//...
    si->pkt = av_packet_alloc();
//...
    return s;
}

 从文章《FFmpeg源码:av_init_packet、get_packet_defaults、av_packet_alloc函数分析》中可以知道,av_packet_alloc函数内部会调用get_packet_defaults函数。所以执行av_packet_alloc函数后,FFFormatContext结构体的成员变量pkt的成员pts、dts的值会变为AV_NOPTS_VALUE,也就是十进制的:-9223372036854775808。

(二)对FFStream结构体的成员变量cur_dts进行初始化

调用完avformat_alloc_context函数后,FFmpeg会调用avformat_open_input函数打开WAV音频文件。而avformat_open_input函数内部会调用wav_read_header函数解码WAV Header,关于wav_read_header函数具体可以参考:《音视频入门基础:WAV专题(5)——FFmpeg源码中解码WAV Header的实现》。然后wav_read_header函数内部又会调用avformat_new_stream函数创建音频流。avformat_new_stream函数内部会执行语句:sti->cur_dts = RELATIVE_TS_BASE对FFStream结构体的成员变量cur_dts进行初始化:

AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c)
{
    FFFormatContext *const si = ffformatcontext(s);
    FFStream *sti;
//...
    sti = av_mallocz(sizeof(*sti));
//...
    if (s->iformat) {
//...
        /* we set the current DTS to 0 so that formats without any timestamps
         * but durations get some timestamps, formats with some unknown
         * timestamps have their first few packets buffered and the
         * timestamps corrected before they are returned to the user */
        sti->cur_dts = RELATIVE_TS_BASE;
//...
    }
    return NULL;
}

从《FFmpeg源码:RELATIVE_TS_BASE宏定义和is_relative函数分析》中可以知道,RELATIVE_TS_BASE的值为十进制的9223090561878065151,所以执行avformat_new_stream函数后,FFStream结构体的成员变量cur_dts会被初始化为9223090561878065151。

(三)compute_pkt_fields函数

调用完avformat_open_input函数后,FFmpeg会调用avformat_find_stream_info函数读取媒体的部分packet(数据包)以获取码流信息。而avformat_find_stream_info函数内部会调用read_frame_internal函数,read_frame_internal函数内部又会调用compute_pkt_fields函数。通过compute_pkt_fields函数可以获取每个packet的pts和dts:

static void compute_pkt_fields(AVFormatContext *s, AVStream *st,
                               AVCodecParserContext *pc, AVPacket *pkt,
                               int64_t next_dts, int64_t next_pts)
{
    FFFormatContext *const si = ffformatcontext(s);
    FFStream *const sti = ffstream(st);
    int num, den, presentation_delayed, delay;
    int onein_oneout = st->codecpar->codec_id != AV_CODEC_ID_H264 &&
                       st->codecpar->codec_id != AV_CODEC_ID_HEVC &&
                       st->codecpar->codec_id != AV_CODEC_ID_VVC;
//...
    /* do we have a video B-frame ? */
    delay = sti->avctx->has_b_frames;
    presentation_delayed = 0;
//...
    
    /* Interpolate PTS and DTS if they are not present. We skip H264
     * currently because delay and has_b_frames are not reliably set. */
    if ((delay == 0 || (delay == 1 && pc)) && onein_oneout) {
        if (presentation_delayed) {
        //...
        }else if (pkt->pts != AV_NOPTS_VALUE ||
                   pkt->dts != AV_NOPTS_VALUE ||
                   pkt->duration > 0             ) {

            /* presentation is not delayed : PTS and DTS are the same */
            if (pkt->pts == AV_NOPTS_VALUE)
                pkt->pts = pkt->dts;
            update_initial_timestamps(s, pkt->stream_index, pkt->pts,
                                      pkt->pts, pkt);
            if (pkt->pts == AV_NOPTS_VALUE)
                pkt->pts = sti->cur_dts;
            pkt->dts = pkt->pts;
            if (pkt->pts != AV_NOPTS_VALUE && duration.num >= 0)
                sti->cur_dts = av_add_stable(st->time_base, pkt->pts, duration, 1);
        }
    }
//...
}

compute_pkt_fields函数内部,由于音频的压缩编码格式不可能是H.264、HEVC(H.265)、VVC(H.266),所以局部变量onein_oneout的值为1:

int onein_oneout = st->codecpar->codec_id != AV_CODEC_ID_H264 &&
                       st->codecpar->codec_id != AV_CODEC_ID_HEVC &&
                       st->codecpar->codec_id != AV_CODEC_ID_VVC;

音频跟视频不一样,音频没有B帧,所以局部变量delay的值为0。局部变量presentation_delayed的值为0:

/* do we have a video B-frame ? */
delay = sti->avctx->has_b_frames;
presentation_delayed = 0;

所以表达式:(delay == 0 || (delay == 1 && pc)) && onein_oneout为真,执行大括号里的内容:

    /* Interpolate PTS and DTS if they are not present. We skip H264
     * currently because delay and has_b_frames are not reliably set. */
    if ((delay == 0 || (delay == 1 && pc)) &&
        onein_oneout) {

从《音视频入门基础:WAV专题(9)——FFmpeg源码中计算WAV音频文件每个packet的duration和duration_time的实现》中可以知道,音频文件的格式正常的情况下,pkt->duration 肯定是大于0的,所以会执行下面大括号里的内容:

else if (pkt->pts != AV_NOPTS_VALUE ||
                   pkt->dts != AV_NOPTS_VALUE ||
                   pkt->duration > 0             ) {

            /* presentation is not delayed : PTS and DTS are the same */
            if (pkt->pts == AV_NOPTS_VALUE)
                pkt->pts = pkt->dts;
            update_initial_timestamps(s, pkt->stream_index, pkt->pts,
                                      pkt->pts, pkt);
            if (pkt->pts == AV_NOPTS_VALUE)
                pkt->pts = sti->cur_dts;
            pkt->dts = pkt->pts;
            if (pkt->pts != AV_NOPTS_VALUE && duration.num >= 0)
                sti->cur_dts = av_add_stable(st->time_base, pkt->pts, duration, 1);
        }

 从上面可以知道FFFormatContext结构体的成员变量pkt的成员pts、dts的值会在avformat_alloc_context函数中被av_packet_alloc函数初始化为AV_NOPTS_VALUE,所以会执行下面语句,让pkt->pts = pkt->dts = AV_NOPTS_VALUE:

            /* presentation is not delayed : PTS and DTS are the same */
            if (pkt->pts == AV_NOPTS_VALUE)
                pkt->pts = pkt->dts;

然后由于pkt->pts等于AV_NOPTS_VALUE,所以会执行pkt->pts = sti->cur_dts:

if (pkt->pts == AV_NOPTS_VALUE)
    pkt->pts = sti->cur_dts;
pkt->dts = pkt->pts;

下面分情况讨论:

1.第一个packet的pts和dts

从上面可以知道,执行avformat_new_stream函数后,sti->cur_dts会被初始化为RELATIVE_TS_BASE(9223090561878065151)。所以对于第一个packet,其pkt->pts和pkt->dts的值会变为RELATIVE_TS_BASE(9223090561878065151):

if (pkt->pts == AV_NOPTS_VALUE)
    pkt->pts = sti->cur_dts;
pkt->dts = pkt->pts;

这时候表达式:pkt->pts != AV_NOPTS_VALUE && duration.num >= 0为真,所以执行语句:sti->cur_dts = av_add_stable(st->time_base, pkt->pts, duration, 1),让sti->cur_dts = pkt->pts + (1 × duration ÷ st->time_base)。关于av_add_stable函数的用法可以参考:《FFmpeg源码:av_rescale_rnd、av_rescale_q_rnd、av_rescale_q、av_add_stable函数分析》:

if (pkt->pts != AV_NOPTS_VALUE && duration.num >= 0)
    sti->cur_dts = av_add_stable(st->time_base, pkt->pts, duration, 1);

从《FFmpeg源码:compute_frame_duration函数分析》中可以知道,duration.num为该音频packet占用的以AVStream的time_base为单位的时间值,duration.den为该音频的采样频率(单位为Hz);从《音视频入门基础:WAV专题(8)——FFmpeg源码中计算WAV音频文件AVStream的time_base的实现》中可以知道st->time_base.num为1,st->time_base.den为音频采样频率;

所以语句sti->cur_dts = pkt->pts + (1 × duration ÷ st->time_base) 等价于

sti->cur_dts = pkt->pts + duration.num。

sti->cur_dts为下一个音频packet的pts和dts,也就是说下一个音频packet的pts和dts的值是在上一个音频packet的pts和dts基础上增加duration.num。

2.第一个packet之后的packet的pts和dts

对于第一个packet之后的packet,比如第二个packet。再次调用compute_pkt_fields函数时,会继续执行语句: pkt->pts = sti->cur_dts,得到sti->cur_dts中保存的下一个packet的dts和pts:

if (pkt->pts == AV_NOPTS_VALUE)
    pkt->pts = sti->cur_dts;

(四)av_read_frame函数

调用完avformat_find_stream_info函数后,FFmpeg会调用av_read_frame函数从文件中读取数据包。av_read_frame函数内部会执行:

    if (is_relative(pkt->dts))
        pkt->dts -= RELATIVE_TS_BASE;
    if (is_relative(pkt->pts))
        pkt->pts -= RELATIVE_TS_BASE;

让该packet的pts和dts减去RELATIVE_TS_BASE(9223090561878065151)。从而得到最终的pts和dts。

三、总结

1.音频跟视频不一样,音频没有B帧,所以音频的pts和dts输出顺序一样,即pts等于dts。

2.对于音频,其第1个packet的pts和dts的值为0。之后的每个packet的pts和dts值在上一个音频packet的pts和dts基础上增加duration,也就是增加该音频packet占用的以AVStream的time_base为单位的时间值。

举个例子,某音频文件,其第1个packet的pts和dts值为0,duration值为4096。所以第2个packet的pts和dts值为0 + 4096 = 4096。第3个packet的pts和dts值为4096 + 4096 = 8192:

关于duration的概念可以参考:《音视频入门基础:WAV专题(9)——FFmpeg源码中计算WAV音频文件每个packet的duration和duration_time的实现》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2117701.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无人机动力系统设计之桨叶推力计算

无人机动力系统设计之桨叶推力计算 1. 源由2. 关键参数2.1 特性参数2.1.1 材质(Material)2.1.2 叶片数量(Number of Blades)2.1.3 重量(Weight) 2.1.4 噪音水平(Noise Level)2.2 安装…

开敞式屋脊通风天窗,“0”差评厂房通风换气设备!

开敞式屋脊通风天窗作为常见的通风设备,在现代建筑设计中尤其是在工业厂房中扮演着重要角色。 开敞式屋脊通风天窗是安装在建筑屋顶屋脊位置且处于常开状态,没有安装启闭阀板的通风天窗。这种设计允许空气自由流通,无需依赖机械动力&#xff…

乱弹篇(46)白露闲谈

昨(7日)天11时11分,已经进入2024年的白露节气,今天本“人民体验官”推广人民日报官方微博文化产品《诗句里的秋天》,旨在提醒亲友已是“秋风何冽冽,白露为朝霜”时令,天气开始转凉,早…

AI模型应根据应用场景选择全能型或者专精型

文章目录 一、前言二、选择全能型2.1 优势2.2 劣势 三、选择专精型3.1 优势3.2 劣势 四、权衡选择五、总结 一、前言 AI模型的发展方向,在追求全能与专精之间并非简单的二选一,都取决于其应用场景、设计目标以及技术可行性等多个因素。这两种策略各有优…

CodeFormer——卓越的AI照片修复工具,能够轻松消除图片以及视频中的马赛克,还原清晰画质。

CodeFormer是什么 CodeFormer是一款由南洋理工大学和商汤科技联合开发的AI照片和视频修复工具。融合了变分自动编码器(VQGAN)和Transformer技术,对模糊和马赛克的照片或视频进行高质量的修复。CodeFormer通过先进的算法优化图像细节&#xf…

【软考中级攻略站】-软件设计师(5)- 软件工程

软件生存周期 什么是软件生存周期? 软件生存周期指的是一个软件从开始构思到最终停止使用(或被替换)的整个过程。就像人的生命一样,软件也有一个从出生到死亡的过程。 软件生存周期的几个阶段 软件生存周期通常可以分为以下几…

LabVIEW步进电机控制方式

在LabVIEW中控制步进电机可以通过多种方式实现。每种方法都有其独特的优缺点,适用于不同的应用场合。下面详细介绍几种常见的步进电机控制方式,并进行比较。 1. 开环控制(Open-Loop Control) 特点 通过定期发出脉冲信号来控制步进…

基于SSM的流浪动物管理系统的设计与实现82901

摘要 随着移动互联网的快速发展,小程序作为一种新型的应用形态,已经深入到人们的生活中。在高校中,实验室领养管理是一个重要的环节,但传统的领养方式存在着诸多问题,如领养流程繁琐、信息不透明等。因此,本…

陕西农信银行合规知识竞赛活动方案

初赛 1.利用赛易线上答题平台。参与人通过手机、平板或电脑等方式,注册个人基本信息登录进行答题。 2.答题平台在题库中随机抽取试题。 3.参与人在出现第一次答错后,答题平台即终止答题。 4.平台统计参与人答题时间、答题数,同时以正确答题数…

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label…

第12章 谁最便宜就选谁——MySQL基于成本的优化

12.1 什么是成本 I/O 成本 当我们想查询表中的记录时,需要先把数据或者索引从磁盘加载到内存中再操作。 CPU 成本 读取以及检测记录是否满足对应的搜索条件、对结构集进行排序等这些操作损耗。 对于InnoDB来说,页是磁盘和内存之间交互的基本单位。MyS…

《AI 大模型进阶指南:零基础迈向精通,看这一篇足矣!》

一、初聊大模型 (一)为什么要学习大模型? 在当今这个快速变化的时代,新技术和概念不断涌现,大模型因其强大的功能和广泛的应用而备受推崇。在学习大模型之前,不必担忧自身缺乏相关知识或觉得其难度过高。…

Finalshell上传文件失败或者进度总为百分之零解决

1.点击复制标签 2.关闭原标签后即可顺利上传 3.如果还是不行,则用chmod 777 文件路径,改变文件的权限

物联网控制箱

随着科技的飞速发展,物联网(Internet of Things, IoT)技术已经深入我们生活的方方面面,从智能家居到智慧城市,从工业制造到农业管理,物联网正以前所未有的方式改变着世界。唯众的物联网控制箱正是这一趋势下…

技术分享-商城篇-优惠券管理-功能介绍及种类(二十四)

前言 在前面文章中,我们聊到了商城的基础架构和基础功能,它们是构建商城体系的底座,是基础,没有构建出基础,是无法打造高楼大厦的,但是我们在做完基础之后,还得继续深挖其他的功能,…

QQ频道机器人零基础开发详解(基于QQ官方机器人文档)[第二期]

QQ频道机器人零基础开发详解(基于QQ官方机器人文档)[第二期] 第二期介绍:频道模块之频道管理 目录 QQ频道机器人零基础开发详解(基于QQ官方机器人文档)[第二期]第二期介绍:频道模块之频道管理获取用户详情获取用户频道列表获取频道详情获取子频道列表获…

❤ Node05-增删改查接口优化

❤ Node05-增删改查接口优化 1、优化用户查询接口,根据用户条件查询我们的列表 ​ 先拿sql数据命令试试 SELECT * FROM user WHERE name san娟; 我们的查询命令没问题,接下来把我们的传入的活数据给传入进去即可 (1)传入查询…

游泳馆押金管理+手牌管理+刷手牌 开通方法

一、游泳馆手牌押金管理 1. 减少手牌丢失:收取押金可以让顾客更加谨慎地保管手牌,降低手牌丢失的概率。 2. 保障设施安全:有助于防止顾客对手牌的不当使用或故意破坏,保护游泳馆的设施和资源。 3. 规范顾客行为:促使…

Flutter Button使用

Material 组件库中有多种按钮组件如ElevatedButton、TextButton、OutlineButton等,它们的父类是于ButtonStyleButton。 基本的按钮特点: 1.按下时都会有“水波文动画”。 2.onPressed属性设置点击回调,如果不提供该回调则按钮会处于禁…

从戴尔公司中国大饭店DTF大会,看科技外企如何在中国市场发展

【科技明说 | 科技热点关注】 2024戴尔科技峰会在8月如期举行,虽然因事未能抵达现场参加,我只是观看了网上在线直播,也未能采访到DTF现场重要与会者,但是通过数十年对戴尔的跟踪与观察,我觉得2024戴尔科技…