使用 Python 快速给网络视频创建缩略图
使用 Python 快速给网络视频创建缩略图
type
status
date
slug
summary
tags
category
icon
password
 

MP4

MP4结构简介

notion image
 
MP4文件是由多个box组成,一般来说常见的大box有三种:
  • ftyp( file type,文件类型)box:标明自己是 MP4 并提供额外的兼容性信息
  • moov(movie, 电影)box:包含以其他框的嵌套结构组织的元数据
  • mdat(media data container,媒体数据)box:包含音频/视频有效载荷。
 
其中,moov是最重要的部分,包含的信息如下
  • mvhd:movie header,文件的总体信息,如时长,创建时间等
  • trak:track or stream container,存放视频/音频流的容器
    • tkhd:track header,track的总体信息,如时长,宽高等
    • mdia: track media information container ,不解释
      • mdhd:media header,定义TimeScale , track需要通过TimeScale换算成真实时间
      • hdlr: handler,表明本track类型,指明是video/audio/还是hint
      • minf : media information container
        • stbl: sample table box包含样本序号/时间/文件位置映射的信息
          • stsd: sample descriptions
          • stts: decoding time to sample, DTS sample序号的映射表
          • ctts: composition time to sample CTS(创作时间)DTS对应sample序号的映射表
          • stsc: sample-to-chunk, sample和chunk的映射表
          • stsz/stz2: sample size, 每个sample的大小
          • stss: sync sample table, 关键帧列表
          • stco/co64: chunk offset, 每个chunk的文件偏移
          •  

找到MOOV BOX

第一步就是要找到moov box,以便于获取文件的相关信息,但是一般来说能看到的视频header有两种:
(可使用在线网页HexEd.it — 基于浏览器的十六进制编辑器 ,也可以下载010 editor进行二进制信息分析,这里使用的是010 editor)
notion image
notion image
第一种比较容易,因为moov box直接在最前面,但是第二种就稍微麻烦了,因为mdat在最前面,所以必须跳过数据包部分,处理这个问题有一个分支,一个是小于4GB的文件,一个是大于4GB的文件,小于4GB的文件,数据字节量会在mdat前面,占用4个字节(),大于4GB的文件,数据字节量会在mdat后面,占用8个字节,8个字节足以存储绝大多数视频的数据部分所占用的字节数了。
一个32M的视频,字节数为33392096(1FD85E0 H)
一个32M的视频,字节数为33392096(1FD85E0 H)
 
一个10G的视频,字节数为11165926757(2998A8565 H)
一个10G的视频,字节数为11165926757(2998A8565 H)
 
Python 代码如下:
 
我们使用requests请求视频,通过流的形式进行请求

获取时长和宽高

之后我们就要解析moov了。首先解析出视频的总时间、宽、高,我们首先找到mvhd,mvhd所包含的信息如下:
010 editor 自带的MP4模板
010 editor 自带的MP4模板
字段
类型
描述
version
8 bit
取0或1,一般取o
flags
24 bit
标识
creation time
64/32 bit
创建时间,当version=0时取 32bit
modification _time
64/32 bit
修改时间,当version=0时取 32bit
timescale
32 bit
时间基
duration
64/32 bit
文件时长,当version=0 时取 32bit
rate
32 bit
播放速率,默认取 0x00010000,即1.0
volume
16 bit
音量,默认取0x0100,即1.0
reserved
16 bit
0
reserved
2x 32 bit
0
matrix
9 x 32 bit
可忽略
pre_defined
6x 32 bit
0
next_track_ID
32 bit
下一个紧邻的 track box id
我们找到Time Scale和Duration,知道这个文件是以1000毫秒为时基,有243544个时长,处理代码如下:
 
再获取视频的长宽,视频的宽高一般来说是在第一个trak的tkhd中,第二个trak一般来说是音频的信息,tkhd所包含的信息如下:
notion image
字段
类型
描述
version
8 bit
取0或1,一般取0
flags
24 bit
标识
creation_time
64/32 bit
创建时间,当version=0时取32bit
modification _time
64/32 bit
修改时间,当version=0时取32bit
trackID
32 bit
本track id
reserved
32 bit
0
duration
64/32 bit
本track时长,当version=0时取32bit
reserved
2x 32 bit
0
layer
16 bit
视频轨道的叠加顺序,数字越小越靠近观看者,比如1比2靠上,0比1靠上;
alternate_group
16 bit
当前track的分组lD, alternate_group值相同的track在同一个分组里面。同个分 组里的track,同一时间只能有一个track处于播放状态。当alternate_group为0 时,表示当前track没有跟其他track处于同个分组。一个分组里面,也可以只有 一个track;
volume
16 bit
音量,如果是audio track,则为0x0100,即1.0,否则取o
reserved
16 bit
0
matrix
9x 32 bit
可忽略
width
32 bit
宽,[16.16]格式,不必与sample的像素尺寸一致,用于播放时的展示宽高
height
32 bit
高,[16.16]格式,不必与sample的像素尺寸一致,用于播放时的展示宽高
则代码如下:
由于可能读到音频的tkhd,所以需要限定值大于零,原始宽高的数据是乘了一个65536,要除掉。(1920 × 65536 = 83886080)

H.264 编码简介

由于目前大部分MP4视频帧的封装格式都是h264,所以我们需要提取出h264的视频帧,然后加入h264的头,就可以把他们当做直播视频流之类的东西,通过PyAV库来进行解析,当然在这之前还需要确定这个视频是不是使用h264来进行编码的,此时需要检测是不是包含avc1 box,avc1 box包含在stsd(sample description box) 中。stsd的字段信息如下:
字段
类型
描述
version
8 bit
取0或1,一般取0
flags
24 bit
entry_ count
32 bit
entry 个数
开始循环
AudioSampleEntry0
不定大小
子box,当handler_ type='soun'时才有
VisualSampleEntry()
不定大小
子box,当handler_ type='vide'时才有
HintSampleEntry0
不定大小
子box,当handler_ type="hint 时才有
MetadataSampleEntry0
不定大小
子box,当handler_ type='meta'时才有
结束循环
notion image
 
有了AVC1 box,就可以提取出对解码比较重要的,在AVCC box中的几个关键信息SPS(Sequence Parameter Sets)和PPS(Picture Parameter Set),SPS 和 PPS 中存放了解码过程中所需要的各种参数,是 H.264 解码的前置条件,没有 SPS 和 PPS,视频将会无法解码,所以在解码的时候,我们总是首先把 SPS 和 PPS 传给解码器,供解码器初始化。AVC1和AVCC box的定义格式如下
 
AVC1:
字段
类型
描述
reserved
6 x 8 bit
0
data reference index
16 bit
可以检索与当前sample description关联的数据。数据引用存储在data reference box
pre_defined
16 bit
0
reserved
16 bit
0
pre_defined
3x32 bit
0
width
16 bit
像素宽度
height
16 bit
像素高度
horizresolution
32 bit
每英寸的像素值(dpi),[16.16]格式的数据,固定为 0x00480000,72 dpi
vertresolution
32 bit
每英寸的像素值(dpi),,[16.16]格式的数据,固定为 0x00480000,72 dpi
reserved
32 bit
0
frame_ count
16 bit
每个sample中的视频帧数,固定为1
compressorname
32 bit
0
depth
16 bit
0x0018,rgb24 位深
pre_defined
16 bit
-1
AVCC
 
AVCC:
字段
类型
描述
configurationVersion
8 bit
固定1
AVCProfilelndication
8 bit
编码时设:置的profile, 例如base、 main、 high等,具体 参见ISO_ IEC_ 14496-10-AVC 2003.pdf, page 45
profile_ compatibility
8 bit
AVCL evellndication
8 bit
编码时设置的level
reserved
6 bit
111111'b
lengthSizeMinusOne
2 bit
reserved
3 bit
'111'b
numOfSequenceParameterSets
5 bit
sps个数,一般为1
sps循环开始
sequenceParameterSetLength
16 bit
sps
8 * sequenceParameterSetLength
sps
sps循环结束
numOfPictureParameterSets
8 bit
pps个数,一般为1
pps循环开始
pictureParameterSetLength
16 bit
pps
8 * pictureParameterSetLength
pps
pps循环结束
我们还要了解H.264编码的三种帧I帧 P帧 B帧
  1. I 帧:帧内编码帧,帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)
    1. 它是一个全帧压缩编码帧,它将全帧图像信息进行JPEG压缩编码及传输
    2. 解码时仅用I 帧的数据就可重构完整图像
    3. I 帧描述了图像背景和运动主体的详情
    4. I 帧不需要参考其他画面而生成
    5. I 帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量)
    6. I 帧不需要考虑运动矢量
    7. I 帧所占数据的信息量比较大
  1. P 帧:前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
    1. P帧的预测与重构:P帧是以 I 帧为参考帧,在 I 帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运行矢量从 I 帧找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。
    2. P帧是 I 帧后面相隔1~2帧的编码帧
    3. P帧采用运动补偿的方法传送它与前面的I或P帧的差值及运动矢量(预测误差)
    4. 解码时必须将帧中的预测值与预测误差求和后才能重构完整的P帧图像
    5. P帧属于前向预测的帧间编码。它只参考前面最靠近它的 I 帧或P帧
    6. 由于P帧是参考帧,它可能造成解码错误的扩散
    7. 由于是差值传送,P帧的压缩比较高
  1. B帧:双向预测内插编码帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况,但我这样说简单些),换言之,要解码B帧。不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。
    1. B帧的预测与重构: B帧以前面的 I 或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。
    2. B帧是由前面的 I 或P帧和后面的P帧进行预测的
    3. B帧传送的是它与前面的 I 或P帧和后面的P帧之间的预测误差及运动矢量
    4. B帧是双向预测编码帧
    5. B帧压缩比最高,因为它只反映并参考帧间运动主体的变化情况,预测比较准确
    6. B帧不是参考帧,不会造成解码错误的扩散
    7. 💡
      注:I、B、P帧是根据压缩算法的需要,是人为定义的,他们都是实实在在的物理帧。 一般来说,帧的压缩率是7(跟JPG差不多), P帧是20,B帧可以达到50.可见使用B帧能节省大量空间, 节省出来的空间可以用来保存多一些帧,这样在相同码率下,可以提供更好的画质。
 
同时这解析的时候也不是顺序解析的,而是有一定的顺序,基本上来说就是I→P→B,看图:
notion image
这个顺序是在MP4 moov头中的cttc box里描述的,但是据我的测试,没有这个顺序描述box,ffmpeg也能解析出实际的画面,这可能与我们是使用h264裸流进行解析的有关。

通过FFMPEG将MP4转换为H264裸流

在H264中,每一个单元被称作NALU,这是H264的基本单位,在NALU之下,还有Slice和宏块(Macroblock),结构层级如下:
notion image
一般来说,一个NALU中含有一个Slice,一个Slice中含有多个宏块(Macroblock),在视频中,为了尽可能提高压缩率,上一帧和下一帧之间都是通过演算得到的,一般来说上一帧和下一帧都有共通点,比如出现了一个人站在一个场景里说话,实际上只是部分块发生了位移,这个时候就可以只把运动的块的位移算出来进行保存,就不用存储画面了,通过程序进行解析的效果如下:
通过ffmpeg生成包含空间向量的视频
通过ffmpeg生成包含空间向量的视频
或者我们也可使用 Intel Video Pro Analyzer(需要付费,但也有只能查看前 10 帧的免费试用版)。
或者我们也可使用 Intel Video Pro Analyzer(需要付费,但也有只能查看前 10 帧的免费试用版)。
这就是宏块的作用,也是为什么P、B帧能压缩,并且压缩率能这么高。
在H264裸流中,每一个NALU都以 0x00 00 01 开头,这也是判断上下NALU的关键。

IDR NALU

在分析h264裸流时,会经常遇到两种NALU:IDR和非IDR:
notion image
IDR(Instantaneous Decoding Refresh,即时解码刷新),从名字上可以看出,一遇到IDR,画面就应该立即刷新,同时IDR NALU比较大,我们可以认为IDR NALU ⇒ I 帧,但是I帧≠IDR。所有 IDR 帧都是 I 帧,但并非所有 I 帧都是 IDR 帧。IDR(瞬时解码器刷新)帧是特殊的 I 帧,它不仅包含完整的图片,还表示 IDR 之后的任何 P/B 帧都不允许引用 IDR 之前的帧。

多个NALU描述一个画面的情况

我在实际测试的时候遇到了这样的文件,几个I帧连在一起,其实我们不应该通过看头是不是65来判断是不是I帧,是不是I帧实际上是由NALU中Slice Header中的slice_type规定
notion image
notion image
可以看到第二项就是slice type,而它的大小为ue(v),这里指的是无符号指数哥伦布算法,这种算法比较简单
指数哥伦布编码
 

帧编码简述

H.264帧编码原理简述

参考资料

MP4

Windows 开启 FTP 但无法访问Scaleway IPv6 VPS 缩小硬盘到3G,实现 0.2欧/月