前言
前面我们通过介绍音视频的基本概念,ffmpeg基本工作框架,以及使用一个小demo来实践我们的知识,可以说算是对ffmpeg有了一个基本的认识。
接下来想要在目前的基础之上更进一步的理解和学习ffmpeg,我们需要在前面的基础之上学习底层一些的知识。就比如我们今天准备分析的媒体文件格式。文件的格式会影响到ffmpeg媒体文件读取,解复用,解码,编码,复用的全过程,学习文件的格式有助于我们理解ffmpeg的这些处理流程。
分析工具
MP4情况介绍
MP4是一种基于MPEG-4 Part 12(2015)和MPEG-4 Part 14标准的数字多媒体容器格式。以存储数字音频及数字视频为主,但也可以存储字幕和静止图像。 MPEG-4 Part 12定义了基础媒体文件格式,Part 14在Part 12的基础上定义了MP4的文件格式。
因此本文分析也注意以这这两种标准的文档为基础进行分析。不过由于mpeg-4借鉴了苹果的QuickTimeg格式(mov文件),两者的视频文件的结构高度相似,我们也可以参考苹果的QuickTime File Format
PS:我今天才知道MP4这种媒体格式标准文档是收费的,哈哈,找了半天没有找到MPEG-4 Part 14全文文档。
术语与概念
MP4中有许多概念或者术语需要我们提前进行理解。
Box(Atom)
盒子是组成MP4文件的基本结构,MP4中有大量不同类型的box。在Apple的QuickTime-File_Format文档中则称作Atom(原子),两者是差不多的概念。我们主要以Box进行表述。
box由Header和Data(Body)组成。
box之间可以组合或者嵌套,形成新的box。
主要的box排列和嵌套方式如下
![](https://img.haomeiwen.com/i689802/c8c75997b455bc34.png)
![](https://img.haomeiwen.com/i689802/55bc68b3a3d8e269.png)
具体看MPEG-4 Part 12(2015)整个文档第29-30页.
我们关注的顶层的box主要有三个:ftyp,moov,mdat。
container box
可称作容器盒子,这种类型的box内部成员一般不是具体的信息,而是box。
Track
轨道,表示一类数据的集合。比如音频轨道,视频轨道等。
Sample
采样,对视频而言,sample是一帧画面;对音频而言,sample是一段时间内的音频数据。
Chunk
是轨道中一组采样数据组成的块。
mp4基本结构
mp4文件中的一个基本结构是box,box内部由box header和box data(或可以称作body)组成
![](https://img.haomeiwen.com/i689802/a433275b6f26a30e.png)
Box Header
正常情况下header只有两个字段,size和type
名称 | 大小(byte) | 含义 |
---|---|---|
size | 4 | box的大小(从文件头开始算计) |
type | 4 | box的类型(一般是一个固定值) |
在这正常的两个字段之外,还可能出现拓展的字段largesize,version,flags:
![](https://img.haomeiwen.com/i689802/720a1a415ad3d6f5.png)
在正常情况下,box占8字节,前4字节表示box大小,后4个字节表示box类型。假如box很大4个字节的size无法表示(这种情况不太常见),则size会被设置为1,然后往后拓展8个字节用largesize来表示box的大小。假如size=0,表示到了文件末尾。
version和flag这两个拓展空间则是FullBox这种拓展Box中会出现。
Box Data
box data是数据主体部分,这里并没有固定的结构,需要根据不同类型的box来分析。
Box种类
MP4中有大量不同类型的box,不同的类型box中存储了不同用处的信息。比如ftyp,moov,mvhd等等,后面会详细介绍。
box组织结构
在MP4中box是排列嵌套组合的树形结构,box中可以包含其他一个或者多个box。
![](https://img.haomeiwen.com/i689802/17eac8d235cb2483.png)
我们添加一个视频文件在线解析之后,得到box的结构如下:
![](https://img.haomeiwen.com/i689802/3b8a6c93bdcf9855.png)
图形化展示box相互隶属关系:
![](https://img.haomeiwen.com/i689802/489c6742ae89f940.png)
MP4中的box
File Type Box
- type:ftyp
- Container Box: NULL (ftpy没有父容器)
ftyp一般出现在文件的开头,描述文件的版本和兼容的协议等信息。
aligned(8) class FileTypeBox extends Box(‘ftyp’) {
unsigned int(32) major_brand;
unsigned int(32) minor_version;
unsigned int(32) compatible_brands[]; // to end of the box
}
结构
名称 | 大小(byte) | 含义 |
---|---|---|
size | 4 | box的大小 |
type | 4 | box的类型 |
Minor version | 4 | 版本号 |
Major brand | 4 | 品牌名称(见https://www.ftyps.com) |
Minor version | 4 | 版本号 |
Compatible brand | 不定 | 兼容协议 |
我们解析一个短视频看它的ftyp情况:
![](https://img.haomeiwen.com/i689802/51762968db49ff09.png)
由于Box Header属于Box中的公共结构,后面的Box介绍会忽略掉Header部分。
Media Data Box
- type: mdat
- Container: NULL
存储音频视频的媒体数据。
aligned(8) class MediaDataBox extends Box(‘mdat’) {
bit(8) data[];
}
测试视频展示的效果如下
![](https://img.haomeiwen.com/i689802/71d8d3b1225fe3b4.png)
Movie Box
- type: moov
- Container Box: NULL (moov没有父容器)
Movie Box是一个container box。内部包含一个Movie Header Box,若干个Track Box,Track Box中一般有一个音频轨道box,一个视频轨道box。
存储有MP4文件的元数据。
该box一般紧跟着开头的File Type Box或者跟在mdat后。
iso标准文档中提到一般在靠近开头,或者靠近结尾,但不是强制要求的。
Movie Box在MP4 文件中的结构大概是这样的
![](https://img.haomeiwen.com/i689802/13d9b717284f48d4.png)
它内部往往
Movie Header Box
- type: mvhd
- Container: Movie Box (‘moov’)
定义并存储MP4文件的整体信息。
// FullBox在header处多拓展出来version,flags两个字段
aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {
if (version==1) {
unsigned int(64) creation_time; // 创建时间
unsigned int(64) modification_time; // 修改时间
unsigned int(32) timescale; // 时间刻度,一秒钟有多少个时间刻度
unsigned int(64) duration; // 视频时长
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) timescale;
unsigned int(32) duration;
}
// 播放速率 高16位表示整数部分,低16位表示小数部分
template int(32) rate = 0x00010000; // typically 1.0
// 播放音量 高8位表示整数部分,低8位表示小数部分 1.0 (0x0100)
template int(16) volume = 0x0100; // typically, full volume
const bit(16) reserved = 0;
const unsigned int(32)[2] reserved = 0;
// 视频转换矩阵
template int(32)[9] matrix =
{ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
// Unity matrix
bit(32)[6] pre_defined = 0;
// 表示下一个轨道的轨道ID 要比当前轨道ID大 (不可以是0)
unsigned int(32) next_track_ID;
}
- timescale
音视频文件中的时间单位并不是我们日常使用的秒/毫秒/微秒,而是自定义的,这个定义通过timescale来实现。它是时间刻度的意思,表示一秒钟内经过多少个时间单位。它的倒数就是该轨道所采用的时间的一个基本单位。
mvhd 存储了媒体文件的公共整体信息,timescale一般为1000,在编辑媒体文件涉及时间的整体信息时会用到,比如duration。
不同轨道的数据(音频视频)会有各自的timescale,不会用mvhd box中的timescale。
测试视频显示情况如下:
![](https://img.haomeiwen.com/i689802/056cf8aa00c3fdb4.png)
Track Box
- type: trak
- Container: Movie Box (‘moov’)
track box是一个containerbox,里面会包含很多别的box,有2个很关键 Track Header Box
Media Box
Track Header Box
- type: tkhd
- Container: Track Box (‘trak’)
我们先来看看Track Header Box的信息
aligned(8) class TrackHeaderBox
extends FullBox(‘tkhd’, version, flags){
if (version==1) {
unsigned int(64) creation_time;
unsigned int(64) modification_time;
unsigned int(32) track_ID; // 轨道ID
const unsigned int(32) reserved = 0;
unsigned int(64) duration;
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) track_ID;
const unsigned int(32) reserved = 0;
unsigned int(32) duration;
}
const unsigned int(32)[2] reserved = 0;
template int(16) layer = 0;
template int(16) alternate_group = 0;
template int(16) volume = {if track_is_audio 0x0100 else 0};
const unsigned int(16) reserved = 0;
template int(32)[9] matrix=
{ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
// unity matrix
unsigned int(32) width;
unsigned int(32) height;
}
- creation_time:创建时间
- modification_time:修改时间;
- track_ID:轨道ID,当前track的唯一标识,不能为0,不能重复;
- duration:当前track的完整时长(需要除以timescale得到具体秒数);
- layer:视频轨道的叠加顺序,数字越小越靠近观看者,比如1比2靠上,0比1靠上;
- alternate_group:track的分组ID
- volume:audio track的音量,介于0.0~1.0之间;
- matrix:视频的变换矩阵;
- width、height:视频的宽高;
![](https://img.haomeiwen.com/i689802/396540aa481a09da.png)
Media Box
- type: mdia
- Container: Track Box (‘trak’)
Container Box。
Media Header Box
- type: mdhd
- Container: Track Box (‘mdia’)
Media Header Box中声明了非媒体数据的总体信息,但是和轨道的类型有关。比如音频轨道,则Media Header Box保存的就是音频媒体的总体信息(创建时间,修改时间,时长,时间刻度等)。
aligned(8) class MediaHeaderBox extends FullBox(‘mdhd’, version, 0) {
if (version==1) {
unsigned int(64) creation_time; // 该轨道数据的创建时间
unsigned int(64) modification_time;// 该轨道数据的修改时间
unsigned int(32) timescale; // 该轨道数据的时间刻度(一秒钟经过的时间单位数)
unsigned int(64) duration; // 该轨道数据的时长
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) timescale;
unsigned int(32) duration;
}
bit(1) pad = 0;
unsigned int(5)[3] language; // ISO-639-2/T language code
unsigned int(16) pre_defined = 0;
}
这里的timescale表示该轨道数据的timescale(音频和视频的timescale不同)。
![](https://img.haomeiwen.com/i689802/d6f905a39be2bbc5.png)
比如我们的测试视频的这个视频轨道,timescale=30_000,表示1秒钟内经过了30_000个时间单位,那么此时该轨道的时间基本单位就是1/30_000 秒。该视频轨道中的时间计算基本都以这个时间单位作为基准。
Handler Reference Box
- type: hdlr
- Container: Media Box (‘mdia’) or Meta Box (‘meta’)
在Media Box中,则用于标识该轨道的媒体类型;在Meta Box中,则用于声明Meta Box的结构或者格式。
aligned(8) class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
unsigned int(32) pre_defined = 0;
/*
* handler_type取值范围是如下三个
* vide(0x76 69 64 65),video track;
* soun(0x73 6f 75 6e),audio track;
* hint(0x68 69 6e 74),hint track;
*/
unsigned int(32) handler_type;
const unsigned int(32)[3] reserved = 0;
string name; // UTF-8 字符 轨道名称
}
测试视频数据显示如下
![](https://img.haomeiwen.com/i689802/a7bb656411f91b82.png)
Media Information Box
- type: minf
- Container: Media Box (‘mdia’)
一个Container Box,包含了轨道媒体数据的特征信息。
数据索引类的box
Sample Table Box
- type: stbl
- Container: Media Information Box (‘minf’)
一个Container Box,内部包含大量的box,用来存储所有时间和数据索引。使用此处的表,可以及时定位样本,确定它们的类型(例如是否为 I 帧),并确定它们的大小、容器以及在该容器中的偏移量。
![](https://img.haomeiwen.com/i689802/9f4667f73ee27d4f.png)
stblbox中的child box包括:
- stsd:(Container Box)媒体数据的描述信息
- stco:thunk在文件中的偏移;
- stsc:每个thunk中包含几个sample;
- stsz:每个sample的size(单位是字节);
- stts:每个sample的时长;
- stss:哪些sample是关键帧;
- ctts:帧解码到渲染的时间差值,通常用在B帧的场景;
接下来我们就一一介绍这些child box。
Sample Description Box
- type: stsd
- Container: Sample Table Box (‘stbl’)
提供了有关所使用的编码类型的详细信息,以及该编码所需的任何初始化信息。
aligned(8) abstract class SampleEntry (unsigned int(32) format) extends Box(format){
const unsigned int(8)[6] reserved = 0;
unsigned int(16) data_reference_index;
}
class BitRateBox extends Box(‘btrt’){
unsigned int(32) bufferSizeDB;
unsigned int(32) maxBitrate;
unsigned int(32) avgBitrate;
}
aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type)
extends FullBox('stsd', version, 0){
int i ;
unsigned int(32) entry_count;
for (i = 1 ; i <= entry_count ; i++){
SampleEntry(); // an instance of a class derived from SampleEntry
}
}
(Decoding)Time to Sample Box
- type: stts
- Container: Sample Table Box (‘stbl’)
记录每一个sample被解码的时间(解码时间戳)。
aligned(8) class TimeToSampleBox extends FullBox(’stts’, version = 0, 0) {
unsigned int(32) entry_count; // 条目数
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count; // 数据采样片的个数
unsigned int(32) sample_delta; //该采样片所代表的时长
}
}
我们以自己的测试视频为例,一共有451个采样片的数据(这里无法得知每个sample的大小),每个采样的时间是1001个时间单位,于是我们计算得知这个轨道的数据总时长是:
total_time = 451x1001 = 451451 //此时的单位不是秒,也不是毫秒,而是媒体文件time_scale的倒数
total_time_sec = 451451/30_000 = 15.05s // 采样数据总时长
![](https://img.haomeiwen.com/i689802/6797691ef634ccdd.png)
而我们计算采样数据的每一帧解码时间的公式为(时间轴为0):
DT(n+1) = DT(n) + STTS(n) // 一般以0开头
DT(1) = DT(0) +STTS(0) = 0 // 从0开始,表示第一个sample从0开始解码
DT(2) = DT(1) +STTS(1) = 0+1001 = 1001 // 第二个sample从1001个时间单位开始解码
sample index | 1 | 2 | 3 |
---|---|---|---|
sample delta | 1001 | 1001 | 1001 |
decode time | 0 | 1001 | 2002 |
Sync Sample Box
- type: stss
- Container: Sample Table Box (‘stbl’)
对于视频帧而言,I帧非常重要,是视频解码的关键,seek操作时都是找到对应时间点附近的I帧,然后开始读取数据解码。stss就是存储I帧的的索引信息的。(关于帧类型见视频的一些基本概念——帧类型)
aligned(8) class SyncSampleBox extends FullBox(‘stss’, version = 0, 0) {
unsigned int(32) entry_count; // I帧的个数
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_number; // I帧的在sample中序列号
}
}
我们看看测试视频的I帧索引情况:
![](https://img.haomeiwen.com/i689802/cb25395f41a1f943.png)
信息显示,一共有两个I帧,第一个在第一帧(一般视频的第一帧就是I帧),第二个在251个采样片位置
Composition Time to Sample Box
- type: ctts
- Container: Sample Table Box (‘stbl’)
该 Box 记录的是每个 sample 的 Composition time 和 Decode time 之间的偏移。Composition time 可以理解为显示时间。即显示时间与解码时间之间的偏移。
aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, version, 0) {
unsigned int(32) entry_count; // 条目数
int i;
if (version==0) {
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count; // 采样片的数据
unsigned int(32) sample_offset; // 采样的偏移,也即CT与DT之间的偏移时间
}
}else if (version == 1) {
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
signed int(32) sample_offset;
}
}
}
测试视频的ctts box显示如下
![](https://img.haomeiwen.com/i689802/d2b9cf5182d3f46c.png)
利用解码时间DT和这个ctts表里的偏移量,我们可以计算出显示时间CT。
CT的求解公式如下:
CT(n) = DT(n) + CTTS(n)
以测试视频为例
sample index(Frame Index) | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 |
---|---|---|---|---|---|---|---|---|---|
sample offset | 2002 | 5005 | 2002 | 0 | 1001 | 5005 | 2002 | 0 | 1001 |
decode time | 0 | 1001 | 2002 | 3003 | 4004 | 5005 | 6006 | 7007 | 8008 |
CT | 2002 | 6006 | 4004 | 3003 | 5005 | 10010 | 8008 | 7007 | 9009 |
显示顺序 | F1 | F4 | F3 | F5 | F2 | F8 | F7 | F9 | F6 |
从显示顺序来看,解码第1帧和显示第1帧一致,但是解码第2帧则在第5帧显示,解码第3帧在第三帧显示,解码第4帧在第2帧显示,解码第5帧在第4帧显示。
这说明解码第一帧是I帧,解码第二帧是P帧,其余的三帧都是B帧。因为I帧的解码不依赖其他帧,P帧的解码依赖前一帧,B帧的解码依赖前后两帧(关于视频的一些基本概念——帧类型)
我们通ffprobe命令来打印该测试视频的帧类型:
ffprobe -show_frames -select_streams v -of xml sample.mp4 >videoframes.xml
// 数据修剪过后如下:
<?xml version="1.0" encoding="UTF-8"?>
<ffprobe>
<frames>
// 解码帧数从0开始计数
<frame media_type="video" pict_type="I" coded_picture_number="0" />
<frame media_type="video" pict_type="B" coded_picture_number="3" />
<frame media_type="video" pict_type="B" coded_picture_number="2" />
<frame media_type="video" pict_type="B" coded_picture_number="4" />
<frame media_type="video" pict_type="P" coded_picture_number="1" />
<frame media_type="video" pict_type="B" coded_picture_number="7" />
<frame media_type="video" pict_type="B" coded_picture_number="6" />
<frame media_type="video" pict_type="B" coded_picture_number="8" />
<frame media_type="video" pict_type="P" coded_picture_number="5" />
...
</frames>
</ffprobe>
ffprobe显示出来的解码帧的顺序和我们计算的是一致的,而且帧类型与我们预想的一致。
Sample To Chunk Box
- type: stsc
- Container: Sample Table Box (‘stbl’)
保存每个chunk包含多少个sample
aligned(8) class SampleToChunkBox extends FullBox(‘stsc’, version = 0, 0) {
unsigned int(32) entry_count; // 条目数
for (i=1; i <= entry_count; i++) {
// 共享相同samples_per_chunk的chunk合并为一组,first_chunk就是组内第一个chunk的序号
unsigned int(32) first_chunk;
unsigned int(32) samples_per_chunk; // 每个chunk有多少个sample
unsigned int(32) sample_description_index; //指向stsd的 entry_count的数量
}
}
![](https://img.haomeiwen.com/i689802/a8e75f9f66665679.png)
测试视频的每个chunk都只有一个sample。这个表格太简单了,我们换一个测试视频来看下stsc表的复杂情形:
![](https://img.haomeiwen.com/i689802/63203a7bf4b50022.png)
该数据表应该做如下解读:
- first_chunk = 1,这组chunk的第一个chunk的序号为1,该组chunk中,每个chunk都含有14个sample。
- first_chunk = 66,这组chunk的第一个的序号为66,该组chunk中,每个chunk都含有13个sample。(由本组的first_chunk可以推知上一组的chunk为65个)
- first_chunk = 69,这组chunk的第一个的序号为69,该组chunk中,每个chunk都含有14个sample。(由第组的first_chunk可以推知上一组的chunk为3个)
Sample Size Boxes
- type: stsz
- Container: Sample Table Box (‘stbl’)
记录每个sample的size大小
aligned(8) class SampleSizeBox extends FullBox(‘stsz’, version = 0, 0) {
unsigned int(32) sample_size; //每个sample的size大小
unsigned int(32) sample_count; // sample的数量
if (sample_size==0) {
for (i=1; i <= sample_count; i++) {·························
unsigned int(32) entry_size; // 样本数的大小
}
}
}
![](https://img.haomeiwen.com/i689802/34a2408a92c1b1f8.png)
我们可以看到第一个sample是相对其他sample最大的,因为I帧保存相对完整的图片信息,B帧P帧都只保存部分图片信息,所以数据大小是不一样的。
一般数据量最大的是I帧,B帧数据量最小,P帧居中。
Sample Offset Box
- type: stco/co64
- Container: Sample Table Box (‘stbl’)
记录每个chunk在文件中的偏移量,stco有两种形式,正常是stco,假如视频轨道数据过大的时候,则使用新的box co64,它与stco的主要区别是chunk_offset定义改为64位int,从而可以容纳更大的数据所带来的偏移。
aligned(8) class ChunkOffsetBox extends FullBox(‘stco’, version = 0, 0) {
unsigned int(32) entry_count; //条数数量
for (i=1; i <= entry_count; i++) {
unsigned int(32) chunk_offset; // 偏移量
}
}
aligned(8) class ChunkLargeOffsetBox extends FullBox(‘co64’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
unsigned int(64) chunk_offset;// 偏移量(64位)
}
}
![](https://img.haomeiwen.com/i689802/155f8ebb2ae32016.png)
几个实践
以上就是MP4文件的数据基本结构以及主要的box的情况,下面来看看如何利用MP4的box表结构来解决实际的问题。
1.如何获取视频的宽高
需要找到文件中video类型的track,然后读取track内的tkhd表: tkhd->width tkhd->height。
2.如何seek time
下面我们假设要把测试视频sample.mp4拖动到第1秒,那么究竟应该怎么从哪里读取数据才能正确的显示第1秒的帧呢(以视频为例)?
- 首先进行时间转换
MP4中并不使用现实时间,需要转换为MP4中的时间单位,根据mvhd表,我们知道timescale=30_000,视频轨道的时间基本单位则为1/30_000,每个刻度那么1秒在MP4中的时间为
time = 1x30_000 = 30_000 个时间单位
- 找到30_000个时间单位之前第一个的I帧
为什么是I帧呢?因为视频的解码只有I帧不依赖外部帧独立解码,B帧P帧都需要依赖外部帧来解码,因此只能先找到I帧才能开启解码。
我们首先需要确定30_000个时间单位之前第一个帧。在stts表中,有每个sample的解码时间表,每个sample占用1001个时间单位,那么时间最近的解码帧应该是解码帧第29帧(第29029个时间单位)。
然后我们查找stss表发现只有两个I帧,分别在第一个和第251个,于是我们只能从第一帧开始解码
- 根据sample找到对应的chunk
stsc表中我们知道每个chunk都只包含1个sample,那么第一个sample对应的就是第一个chunk
- 找到chunk在文件中的偏移
根据stco表,我们找到第一个chunk在文件中的偏移 offset= 17731
于是我们从17731位置处读取文件开始解码。
总结一下
- 从mvhd中读取timescale
- 从stts表中找到最近的帧
- 从stss表中找到该帧最近的I帧
- 从stsc表读取chunk与sample的关系
- 从stco表中读到该chunk在文件的偏移
是会有点晕
3.如何提升MP4第一帧的展示(文件结构层面)
我们在前文讲解moov时提到它可以在ftyp之后,也可以在最后mdat之后。但一般而言,moov在末尾处(mdat之后)是比较合适的,首先保存完音视频数据之后才能得到数据的相关情况,紧接着存储moov很合理;其次,如果moov在mdat之前,那么假如我们需要修改moov->udta box中的信息时,mdat中的数据整体都需要移动,则索引数据也需要同时变动,这是很低效的。因此一般我们生成的视频默认moov box是在后面的。
![](https://img.haomeiwen.com/i689802/fcfc88f9f89f7768.png)
但是在流媒体播放MP4 文件时,假如moov box在文件末尾处,那么就需要花更长的时间才能读取到数据索引的相关信息,从而拿到第一帧数据的偏移位置的时机就更晚。
因此提升第一帧的方式就是把moov box的位置前提。我们可以使用ffmpeg达成这样的效果
ffmpeg -i input.mp4 -movflags faststart output.mp4
我们使用的测试视频是抖音下载的一个视频,显然抖音已经做了相关优化,把moov box移到ftyp之后。
![](https://img.haomeiwen.com/i689802/dc5c3328f262c211.png)
总结
我们基本上对MP4中比较重要常见的box进行了介绍,说实话还是很难记得住,很繁杂且没有规律,因此这个还是更适合作为一种资料文档,随时查阅即可。
但是如果能够比较熟悉媒体文件的结构,对于我们理解ffmpeg读取文件,解复用过程以及解码编码都会有很多帮助。
如果想要加深印象,建议用python或js来写个小demo,根据我们学习的box结构来读取一个MP4的信息。
网友评论