[解决思路]关于h264裸流合成mp4时时间戳添加问题

问题场景:

        使用GPU编码(Opencv)生成的h264视频片段中不包含时间戳信息,且含有B帧,直接合成mp4后会导致播放出现问题(瞬间播放完成)。因此,在合成时需要手动添加时间戳。

心路历程:

        发现生成的视频会瞬间播放完成后,意识到是时间戳的问题,检查时间戳代码:

while (av_read_frame(inputFormatContext, &packet) >= 0) {if (packet.pts == AV_NOPTS_VALUE){// 判断pts是否为空// 添加时间戳... }
}

        一开始没注意到有B帧,想着把pts、dts设置相等,并且按照间隔递增就可以了:

AVStream *inStream = inputFormatContext->streams[packet.stream_index];
AVStream *outStream = outputFormatContext->streams[packet.stream_index];// 计算两帧之间的间隔,以输入流的时间基计算
int64_t frameDuration = av_rescale_q(1, av_inv_q(inStream->time_base), inStream->r_frame_rate);
// 转换时间基为输出流的时间基
int64_t _t = av_rescale_q(frameDuration, inStream->time_base, outStream->time_base);// 当前帧的时间戳等于上一帧的时间戳加上两帧之间的间隔
packet.pts = last_pts + _t;
// dts设置成一样
packet.dts = packet.pts;

        对于只有I和P帧的h264流,上述方法可以领流正常播放,但如果h264中有B帧,则会导致在某些播放器上播放异常——帧显示顺序错误,物体“颤抖”着移动

        对于含有B帧的视频,其dts和pts应该是按照这种顺序进行的:

IBPBPBP
dts1234567
pts1325476

        注:dts需要递增,解码需要按照顺序解码。任何一帧的pts应大于等于dts,因为需要先解码后才能显示。因此上述需要调整:

IBPBPBP
dts1234567
pts2436587

        为了使视频打开时就有画面能播放,因此需要将第一帧的pts调整为0,dts是可以为负数的,但需要保持递增。调整后:

IBPBPBP
dts-1012345
pts0214365

        以上是最简单的例子,也可以推导出变形:

IBBPBBP
dts-2-101234
pts0231564

        因此,时间戳的规律和我们的视频中有多少连续的B帧有关

        以上讨论的假设都是一个帧组中只有一个I帧的情况下,如果一个GOP中有多个I帧时情况会更复杂一点(没做整理,本文暂不讨论):

I(IDR)BPBPBIBPBPI(IDR)...
dts123456789101112
pts132547698111012

        总结:加时间戳的前提是要知道h264文件帧的类型结构(是否有B帧、连续的B帧数量、一个GOP是否有多个I帧)

后记:

        由于我的文件都是我自己生成的,帧结构都是统一的,因此可以轻松的找出添加时间戳的规律。但对于不同帧结构的原始流,找出统一规律还是比较麻烦,本文不再阐述。

        附对于最简单的含有B帧的处理代码(IBPBPBPBP):

// 将多个h264文件合并成mp4文件
int mutexMp4File(){// 创建输出文件   // m_outputFile:输出文件名 xxx.mp4AVFormatContext* outputFormatContext = nullptr;if (avformat_alloc_output_context2(&outputFormatContext, nullptr, nullptr, m_outputFile.c_str()) < 0) {return -1;}// 准备工作:遍历输入文件列表   // m_inputFiles:输入文件名列表for (const auto& inputFile : m_inputFiles) {// 打开输入文件AVFormatContext* inputFormatContext = nullptr;if (avformat_open_input(&inputFormatContext, inputFile.c_str(), nullptr, nullptr) != 0) {return -1;}// 查找输入文件的流信息if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {avformat_close_input(&inputFormatContext);avformat_free_context(outputFormatContext);return -1;}// 复制输入文件的流到输出文件for (unsigned int i = 0; i < inputFormatContext->nb_streams; ++i) {// 获取输入流AVStream* inputStream = inputFormatContext->streams[i];// 创建输出流AVStream* outputStream = avformat_new_stream(outputFormatContext, nullptr);if (outputStream == nullptr) {avformat_close_input(&inputFormatContext);avformat_free_context(outputFormatContext);return -1;}// 复制输入流的编解码器参数到输出流if (avcodec_parameters_copy(outputStream->codecpar, inputStream->codecpar) < 0) {avformat_close_input(&inputFormatContext);avformat_free_context(outputFormatContext);return -1;}// 设置输出流的编解码器标志为"copy"outputStream->codecpar->codec_tag = 0;}// 关闭输入文件avformat_close_input(&inputFormatContext);}// 打开输出文件AVIOContext* outputIOContext = nullptr;if (avio_open(&outputFormatContext->pb, m_outputFile.c_str(), AVIO_FLAG_WRITE) < 0) {avformat_free_context(outputFormatContext);return -1;}// 写入文件头部if (avformat_write_header(outputFormatContext, nullptr) < 0) {avformat_free_context(outputFormatContext);return -1;}int64_t gdts_notime = 0;//拼接多个文件的时间戳// 遍历输入文件列表,写入数据到输出文件for (const auto& inputFile : m_inputFiles) {// 打开输入文件AVFormatContext* inputFormatContext = nullptr;if (avformat_open_input(&inputFormatContext, inputFile.c_str(), nullptr, nullptr) != 0) {avformat_free_context(outputFormatContext);return -1;}// 查找输入文件的流信息if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {avformat_close_input(&inputFormatContext);avformat_free_context(outputFormatContext);return -1;}// 从输入文件读取数据并写入输出文件AVPacket packet;int64_t p_max_dts = 0;int i = 0;while (av_read_frame(inputFormatContext, &packet) >= 0) {AVStream *inStream = inputFormatContext->streams[packet.stream_index];AVStream *outStream = outputFormatContext->streams[packet.stream_index];if (packet.pts == AV_NOPTS_VALUE){// 起始 只能针对 IBPBPBP 连续B帧为1的流int nalu_type = 0,startIndex = 0;if (packet.data[0]==0x00 && packet.data[1]==0x00 && packet.data[2]==0x01){nalu_type = 0x1f & packet.data[3];startIndex = 3;}else if(packet.data[0]==0x00 && packet.data[1]==0x00 && packet.data[2]==0x00 && packet.data[3]==0x01){nalu_type = 0x1f & packet.data[4];startIndex = 4;}int64_t frameDuration = av_rescale_q(1, av_inv_q(inStream->time_base), inStream->r_frame_rate);int64_t _t = av_rescale_q(frameDuration, inStream->time_base, outStream->time_base);p_max_dts = _t*(i+1);packet.dts = p_max_dts + gdts_notime - _t;int xt = (int)packet.data[startIndex+1] & 0xC0;if (nalu_type == 0x05 || nalu_type == 0x06 || nalu_type == 0x07 || nalu_type == 0x08){ //I帧开头packet.pts = packet.dts + _t;}else if(nalu_type == 0x01 && xt == 0x80){ //P帧packet.pts = packet.dts;}else if(nalu_type == 0x01 && xt == 0xC0){ //B帧packet.pts = packet.dts + _t*2;}else{// Error!packet.pts = packet.dts;}}else{// Error!}// 将包的流索引设置为输出流索引packet.stream_index = inStream->index;// 写入输出文件av_interleaved_write_frame(outputFormatContext, &packet);av_packet_unref(&packet);i++;}gdts_notime += p_max_dts;// 关闭输入文件avformat_close_input(&inputFormatContext);}// 写入文件尾部av_write_trailer(outputFormatContext);// 关闭输出文件avio_close(outputFormatContext->pb);// 释放资源avformat_free_context(outputFormatContext);return 0;
}

        代码中的求帧类型的方法是旁门左道且有针对性的,可能不适合其他场景。

       

        对于文中任何错误,欢迎指正。

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

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

相关文章

Mac/Linux虚拟机CrossOver2024新版下载使用教程

CrossOver不像Parallels或VMware的模拟器&#xff0c;而是实实在在Mac OS X系统上运行的一个软件&#xff0c;该软件可以让用户在mac是上直接运行windows软件&#xff0c;本文为大家带来的是CrossOver Mac版安装教程&#xff01; CrossOver Mac-安装包下载如下&#xff1a;http…

zookerper入门

zookerper介绍 ZooKeeper 是一个开源的分布式协调框架,主要用来解决分布式集群中应用系统的一致性问题. ZooKeeper本质上是一个分布式的小文件存储系统&#xff08;Zookeeper文件系统监听机制&#xff09;.提供基于类似于文件系统的目录树方式的数据存储&#xff0c;并且可以…

typora导出html添加目录

typora导出html添加目录 使用方法 首先要从typora导出html文件&#xff0c;之后用记事本编辑器html文件 找到文档最后面&#xff0c;如图&#xff1a; 用文字编辑类工具打开sideBar.txt&#xff0c;复制其中所有内容【内容在下面】 在如上图的位置插入所复制的内容 打开修改…

漏油控制器有用吗?漏油监测器多少钱一个?

漏油控制器也可以被称作漏油监测器、漏油传感器&#xff0c;是漏油检测系统里的一部分&#xff0c;一般是和漏油检测绳组合在一起使用&#xff0c;用来检测油罐、输油管道、油类化工厂等场合是否有油料泄露。很多人刚开始可能会觉得难以置信&#xff0c;这么一个小东西就可以检…

SPDK中常用的性能测试工具

本文主要介绍磁盘性能评估的方法&#xff0c;针对用户态驱动Kernel与SPDK中各种IO测试工具的使用方法做出总结。其中fio是一个常用的IO测试工具&#xff0c;可以运行在Linux、Windows等多种系统之上&#xff0c;可以用来测试本地磁盘、网络存储等的性能。为了和SPDK的fio工具相…

两周掌握Vue3(四):计算属性、监听属性、事件处理

文章目录 一、计算属性1.什么是计算属性2.代码示例 二、监听属性三、事件处理 代码仓库&#xff1a;跳转 当前分支&#xff1a;04 一、计算属性 1.什么是计算属性 Vue 中的计算属性具有以下作用&#xff1a; 数据处理&#xff1a;计算属性可以用于对数据进行处理和计算&…

医院患者满意度抽样方法

医院患者满意度调查的抽样方法是选择一部分患者&#xff0c;代表整体患者群体&#xff0c;以便获取可靠的数据&#xff0c;同时降低成本和时间开销。以下是一些医院患者满意度调查中常用的抽样方法&#xff1a; 简单随机抽样&#xff1a;这是一种最基本的抽样方法&#xff0c;…

格雷希尔G65系列快速接头满足汽车减震器的气压、油压测试要求

当汽车经过不平路面时&#xff0c;汽车减震器可以抑制弹簧吸震后因反弹带来的震荡和来自路面的冲击&#xff0c;为乘客带来平稳舒适的行车体验。减震器在出厂之前&#xff0c;需要模拟汽车的真实行驶环境&#xff0c;在模拟当中需要对它们进行气压和油压的轮番测试。 客户的测试…

ssm基于java web的防疫工作志愿者服务平台的设计与实现论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本防疫工作志愿者服务平台就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数…

PHP短链接url还原成长链接

在开发过程中&#xff0c;碰到了需要校验用户回填的短链接是不是系统所需要的&#xff0c;于是就需要还原找出短链接所对应的长链接。 长链接转短链接 在百度上搜索程序员&#xff0c;跳转页面后的url就是一个长链接。当然你可以从任何地方复制一个长链接过来。 长链接 http…

代码随想录 字符串

344.反转字符串 344. 反转字符串 简单 提示 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 示例 1&#xff1a…

解析七人拼团,一人出局:社交电商的新玩法

每天五分钟讲解一个电商模式&#xff0c;大家好我是模式策划啊浩Zeropan_HH 在当今的社交电商时代&#xff0c;各种创新的营销策略层出不穷。其中&#xff0c;“七人拼团&#xff0c;一人出局”的玩法在近年来逐渐崭露头角&#xff0c;成为一种颇受欢迎的营销模式。那么&#…

stm32---输入捕获实验实操(巨详细)

这次来分享上次没说完的输入捕获的知识点 实验中用到两个引脚&#xff0c;一个是通用定时器 TIM3 的通道 1&#xff0c;即 PA6&#xff0c;用于输出 PWM 信号&#xff0c;另一 个是高级控制定时器 TIM1 的通道 1&#xff0c;即 PA8&#xff0c;用于 PWM 输入捕获&#xff0c;实…

vue3 生命周期

与 2.x 版本生命周期相对应的组合式 API beforeCreate -> 使用 setup() created -> 使用 setup() beforeMount -> onBeforeMount mounted -> onMounted beforeUpdate -> onBeforeUpdate updated -> onUpdated beforeDestroy -> onBeforeUnmount destroye…

服务器新创建账号并设置密码与用户权限

1、创建用户&#xff1a; 在root权限下&#xff0c;输入命令useradd -m 用户名&#xff0c;如下 sudo useradd -m love2、设置密码&#xff1a; 输入命令passwd 用户名 回车&#xff0c;接着输入密码操作&#xff0c;需输入两次 sudo password good9993、给用户设置最高权限&…

海外動態IP與海外靜態IP的區別詳解 - okey proxy

根據分配方式的不同&#xff0c;IP地址可以分為靜態IP和動態IP。那麼&#xff0c;海外動態IP和海外靜態IP又有什麼區別呢&#xff1f;本文將詳細解析。 海外動態IP是什麼&#xff1f; 海外動態IP是動態分配的海外IP地址。每次用戶上網時&#xff0c;都會從服務提供商的IP地址池…

window中安装Apache http server(httpd-2.4.58-win64-VS17)

windows中安装Apache http server(httpd-2.4.58-win64-VS17) 1、下载windows版本的的httpd, https://httpd.apache.org/docs/current/platform/windows.html#down 这里选择的是Apache Lounge编译的版本 https://www.apachelounge.com/download/ 2、解压到指定目录&#xff0c;这…

python函数装饰器参数统计调用时间和次数

1 python函数装饰器参数统计调用时间和次数 python在函数装饰器外层定义一个函数生成封闭作用域来保存装饰器入参&#xff0c;供装饰器使用。 1.1 装饰器统计调用时间和次数 描述 通过类的可调用实例装饰器来统计函数每次调用时间和总调用时间&#xff0c;以及调用次数。 …

C#写windows服务,实现把检测软件崩溃工具写成服务 自动运行

一、打开Visual Studio&#xff0c;创建项目->Windows 服务(.NET Framework) 二、点击Service.cs 点击切换到代码视图 static Timer Timer; private Thread monitorThread; private static string logFilePath; private static Process winFormsProcess; public Service1(…

如何修复DLL错误或丢失的问题,这里提供几种方法

DLL错误是指DLL文件的任何错误&#xff0c;一种以.dll文件扩展名结尾的文件。 DLL错误可能出现在微软的任何操作系统中&#xff0c;包括Windows 10、Windows 8、Windows 7、Windows Vista和Windows XP。 DLL错误尤其麻烦&#xff0c;因为存在许多这样类型的文件&#xff0c;所…