前言

使用ffmpeg编译好的程序在电脑上进行音视频转换,可以参考这篇:《windows电脑FFmpeg安装教程手把手详解_windows安装ffmpeg》,而我们要做的是在游戏引擎中集成ffmpeg源码用来录制游戏视频。

我们游戏目前只支持录制avi格式的视频,但是近期有个运营需求:在上架商品的时候在游戏内录制一段视频提供给网页端进行播放。

首先简单的了解了一下,ffmpeg是支持录制mp4格式的,于是简短地改了几行代码就实现了录制mp4,然后把录制出来的视频发给网页同学部署测试。

第二天收到反馈我们录制的视频无法在网页上播放,由于我也是第一次接触ffmpeg,不知道为什么mp4无法在浏览器上播放,整个过程就是不断通过chatgpt查阅资料,不断修改代码调试,最终在某个夜晚跑通了。

问题:浏览器上无法播放mp4

我们游戏录制出来的mp4,右键 - 打开方式,选择浏览器,或者直接拖动mp4文件到浏览器里面,让它打开,表现为无法播放

查看视频详细信息

通过ffmpeg工具提供的一些指令用来查看视频的详细信息,有助于调试

ffprobe -v quiet -print_format json -show_format -show_streams 你的文件名

ffprobe -v quiet -print_format json -show_format -show_streams video.mp4

查看视频每一帧的信息:

ffprobe -show_packets -of xml -i video.mp4

使用ffmpeg将mp4转为h264文件

我的第一个验证想法,使用ffmpeg把游戏录制的视频转换看看转换之后的视频是否在浏览器上播放,结果:转换后就可以在浏览器中就可以播放了

指令:ffmpeg -i video.mp4 -vcodec h264 -crf 10 test.mp4

说明:-crf 的数值是0~51,代表压缩等级,值越大画质越差,体积越小

使用h264编码

通过chatgpt查阅资料,了解到需要把mp4使用h264编码,于是就改了这个接口,这样来看格式虽然是h264了,但是仍然无法在网页上播放

avformat_alloc_output_context2(&oc, NULL, NULL, file_name);
//把第三个参数,输出格式强制指定为H264
avformat_alloc_output_context2(&oc, NULL, "h264", file_name);

对比差异

既然通过格式转换是可以播放的,那就对比一下两个视频文件的详细对比差异,差异如下所示:

可以播放的视频 游戏录制的视频
"codec_tag_string": "avc1", "codec_tag_string": "[0][0][0][0]",
"is_avc": "true", "is_avc": "false",

游戏录制的缺少了以下部分字段

"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 45824,
"duration": "3.580000",
"bit_rate": "2161030",
"nb_frames": "179",

手动设置codec_tag

一开始我的重点方向是:codec_tag

在 MP4 文件中,codec_tag 是一个用于标识视频和音频编解码器的标签。它通常是一个四个字母的代码,例如“avc1”表示 H.264 视频编解码器,“mp4a”表示 AAC 音频编解码器。codec_tag 可以帮助播放器确定正确的解码器来解码视频和音频流。在使用 MP4 文件时,确保你的播放器支持所使用的 codec_tag。

然后使用chatgpt查到示例代码加到游戏内但代码编译不通过,原因我们是自己编译的ffmpeg.lib,还需要修改export才能用某些接口,这个问题后面再说

但是手动设置tag之后,问题依赖存在

断点查证

在断点的时候发现调用:avcodec_find_encoder传入的格式并不是h264,而是mpeg4

于是手动在上面添加了一 行用来修改编码格式,但是还是一样的结果

其实在这个时候,我还是有些分不清楚codec_tag和codec的关系

查一些正确的示例

所以让chatgpt给我举例一些使用ffmpeg来编码h264的视频,然后对照我们的代码来分析是问题出在那里,后面了解到某位同事对ffmpeg比较懂,已于向他请教,大大加速了查证过程。

视频每一帧的数据中无pts和dst

ffprobe -show_packets -of xml -i video.mp4,使用这个指令来查看视频中每帧的数据,发现录制出来的视频没有pts和dst

<ffprobe>
Input #0, h264, from 'video.mp4':
Duration: N/A, bitrate: N/A
Stream #0:0: Video: h264 (Main), yuv420p(progressive), 1904x1002 [SAR 1:1 DAR 952:501], 25 fps, 100 tbr, 1200k tbn
<packets>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="34080" pos="0" flags="K__"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="4995" pos="34080" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="817" pos="39075" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="428" pos="39892" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="89" pos="40320" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="169" pos="40409" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="75" pos="40578" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="71" pos="40653" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="71" pos="40724" flags="___"/>
<packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="672" pos="40795" flags="___"/>
</packets>
</ffprobe>

正常情况下每帧的视频数据中是有pts和dst的,于是在写入每帧数据时手动给pts和dst赋值

av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index;
this->video_st.write_size += pkt->size;
int step = 1000 / this->fps;//ptk.pts每帧加的数值=1000/帧率
pkt->pts = step * m_frameNum;
pkt->dts = step * m_frameNum;
/* Write the compressed frame to the media file. */
return av_interleaved_write_frame(fmt_ctx, pkt);

在av_interleaved_write_frame之前写入pts,之后pts的值就无效了

手动计算pts播放起来非常卡

这样改完之后pts的值是有了,但是播放速度不正常,表现会非常卡,原因就是pts计算错误

assert中断

试过强制修改codec_id,但是碰到问题:在结束录制的时候会中多线程的Assert导致录制得到的视频数据是空的,代码:lib\cstdmf\concurrency.hpp void grab() MF_ASSERT(id_ != gid);

让ffmpeg自动选择输出格式

反复阅读代码然后不断尝试,发现在调用avformat_alloc_output_context2接口不指定格式,而只给输出文件的后缀,录制出来的视频是有pts和dts的!!!

这一下就回到最初的地方,在最早的时候我就是没有设置这个选项的。

接口定义和解释如下:

int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);

ctx:输出格式上下文的指针,函数执行成功后会将创建的上下文赋值给该指针。

oformat:输出格式,可以为 NULL,表示让 FFmpeg 自动选择输出格式。

format_name:输出格式的名称,可以为 NULL,表示让 FFmpeg 自动选择输出格式。

filename:输出文件名,可以为 NULL,表示不需要输出到文件。

pts有了但格式是mpeg4

然后修改接口,再次编译验证,这一次pts在视频帧数据中有了,但为啥视频格式会变成mpeg4???

测试了一下mpeg4在浏览器上也无法播放,但是可以在win10自带的播放上可以播放。

mp4好了但avi坏了

查看代码,确实有一处地方指定了mpeg4,代码:video_codec = avcodec_find_encoder(AV_CODEC_ID_MPEG4);

于是把它改为video_codec = avcodec_find_encoder(AV_CODEC_ID_H264);,这样终于好了。

再来验证一下以前的录制avi格式,结果发现坏了,以前的avi格式录制不了,断点一下,发现中上面提到的assert了。盲猜是avi不能使用h264,于是修改了一下mp4使用h264,其它格式使用mpeg4,再编译验证,这样就好了。

总结一下:

  1. 初始化avformat_alloc_output_context2不要指定格式,让ffmpeg自动调用,但需要输出的文件后缀为mp4
  2. 调用avcodec_find_encoder把mp4设置h246格式,但是对于avi改为AV_CODEC_ID_MPEG4
  3. 指定视频的文件头
  4. 其它地方不要再手动去修改codec_id,否则在结束录制的时候会出错,导致视频为空
  5. 多观察ffmpeg每一个接口的返回值,特别是非成功的情况下要进行处理

这几个接口需要关注返回值是否成功:

  1. avformat_alloc_output_context2 初始化
  2. avcodec_find_encoder 查找编码器的函数
  3. avformat_write_header 写入视频的header
  4. av_interleaved_write_frame 写入视频每一帧的数据
  5. av_write_trailer 结束录制

参考内容

ffmpeg实现将H264裸流封装成.mp4或.avi文件_ffmpeg对裸流封装-CSDN博客

[原]零基础学习视频解码之FFMpeg中比较重要的函数以及数据结构 - 雪夜&流星 - 博客园 (cnblogs.com)

FFmpeg从入门到入魔(3):提取MP4中的H.264和AAC - 掘金 (juejin.cn)

YUV编码为H264 H264封装为MP4 - 知乎 (zhihu.com)

使用工具

MP4封装格式—音视频基础知识 · FFmpeg原理 (xianwaizhiyin.net)

下载 Mp4 Explorer (apponic.com)

踩坑ffmpeg录制的mp4无法在浏览器上播放的更多相关文章

  1. 小程序踩坑记录-上传图片及canvas裁剪图片后上传至服务器

    最近在写微信小程序的上传图片功能,趟过了一些坑记录一下. 想要满足的需求是,从手机端上传图片至服务器,为了避免图片过大影响传输效率,需要把图片裁剪至适当大小后再传输 主要思路是,通过wx.choose ...

  2. 踩坑录- Spring Boot - CORS 跨域 - 浏览器同源策略

    1.解决办法,创建一个过滤器,处理所有response响应头 import java.io.IOException; import javax.servlet.Filter; import javax ...

  3. [转载]为什么有些MP4文件在Chrome浏览器上播放不了?

    http://blog.sina.com.cn/s/blog_6bb7ebcc0101c2ja.html Chrome浏览器支持HTML5,它支持原生播放部分的MP4格式(不用通过Flash等插件). ...

  4. ffmpeg 踩坑实录 添加实时水印(二)

    一.背景介绍 最近领导要求做一个视频录制的相关项目.其中,需要对视频文件进行添加 实时时间水印.于是,我想到了使用之前的ffmpeg来做. 二.ffmpeg实际操作 首先把需要添加水印的视频文件,上传 ...

  5. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  6. OpenCV+Qt+CMake安装+十种踩坑

    平台:win10 x64+opencv-3.4.1 + qt-x86-5.9.0 + cmake3.13.4 x64 OpenCV+Qt+CMake安装,及目前安装完后打包:mingw32-make时 ...

  7. I.MX6 FFmpeg 录制视频

    /************************************************************************* * I.MX6 FFmpeg 录制视频 * 说明: ...

  8. Android原生编解码接口 MediaCodec 之——踩坑

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/gb702250823/article/d ...

  9. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

  10. Spark踩坑记——数据库(Hbase+Mysql)

    [TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...

随机推荐

  1. SQL运用full join连接,decode函数,nvl函数进行列转行

    背景说明:full join连接,decode函数,nvl函数,结合生产背景理解更深刻: test_A表是现金业主某时间段的收益:test_B表是非现金业主某时间段的收益:对于业主,test_A表与t ...

  2. ios ipa apple company 开发者账号申请分享攻略

    ios公司开发者账号申请分享攻略 好不容易终于申请下来了ios 公司开发者账号,真是一路艰辛和漫长啊,特别是对于远在大洋彼岸的大中华国家.以下我就分享一下这一路下来的经验,希望对于那些新手同仁们有所帮 ...

  3. csps区间dp

    加分二叉树 我们可以枚举中间这个 k 的位置,然后分别递归计算左右子树,这就让我们想到这是一个和区间有关的,我们可以用区间dp来解决. \(f[i][j]\) 表示 i, j 这个区间的最大分值.用一 ...

  4. 使用 GitHub Action 自动更新 Sealos 集群的应用镜像

    在 IT 领域,自动化无疑已成为提高工作效率和减少人为错误的关键.Sealos 作为一个强大的云操作系统,已经为许多企业和开发者提供了稳定可靠的服务.与此同时,随着技术不断发展,集成更多的功能和服务变 ...

  5. 轻松掌握组件启动之Redis集群扩展秘籍:轻松扩容与缩容,释放高性能潜能

    扩展集群操作 扩容 在我们原始的集群基础上,我们决定增加一台主节点(8007)和一台从节点(8008),这样新增的节点将会在下图中以虚线框的形式显示在集群中. 1: 首先,在 /usr/local/r ...

  6. Nacos有几种负载均衡策略?

    Nacos 作为目前主流的微服务中间件,包含了两个顶级的微服务功能:配置中心和注册中心. 1.配置中心扫盲 配置中心是一种集中化管理配置的服务,通俗易懂的说就是将本地配置文件"云端化&quo ...

  7. NativeBuffering,一种高性能、零内存分配的序列化解决方案[性能测试篇]

    第一版的NativeBuffering([上篇].[下篇])发布之后,我又对它作了多轮迭代,对性能作了较大的优化.比如确保所有类型的数据都是内存对齐的,内部采用了池化机器确保真正的"零内存分 ...

  8. 来世再不选Java!

    危机感 距离上一次找工作面试已经过去快2年了,那时候正值疫情肆虐,虽然还未感受到"寒潮来临"的苗头,但最终还是成功通过了几轮面试,顺利签约.在目前公司待了2年了,在大环境的影响下, ...

  9. JavaScript高级程序设计笔记04 变量、作用域与内存

    变量.作用域与内存 变量 特定时间点一个特定值的名称. 分类 原始值:按值访问 复制:两个独立使用.互不干扰 引用值(由多个值构成的对象):按引用访问 操作对象时,实际上操作的是对该对象的引用(ref ...

  10. ZYNQ国产化替代-FMQL100TAI 核心板

     概述 FMQL100TAI 核心板基于复旦微的PSOC 架构处理器,型号为FMQL100TAI9000, 板卡 100%采用国产芯片设计,板卡质量等级分为国产 I 级和国产 J 级.具备强大的运算能 ...