提取MP4文件中的h264/h265视频流

Table of Content:
  1. 背景
    1. MP4文件层级与组织
    2. h264/h265视频流
  2. 提取MP4文件中的h264/h265视频流
    1. h265的处理方法
  3. code

这是一个非常细节的问题,毫无疑问,没有多少人会真的关心这个问题。所以这篇就是写给极少数人看的。

这个问题,以及其逆问题“封装h264/h265视频流到mp4文件中”,定义在国际标准ISO-14496的第15部分。

这个国际标准,我看到了两个版本,2004年,仅仅有h264部分。2013年版本,这个版本则包含了更多的视频流格式h265,以及其他的扩展视频格式。

背景

到达这个问题之前,其实很有很多的其他细节问题:

MP4文件层级与组织

这个问题,不是本文讨论的重点,仅仅简单介绍下。

MP4文件,由层级的box/atom组成。box的头信息包含固定4个字节的长度信息和4个字节的类型信息。
类型一般都是类似FourCC的形式。box也有层级结构的,这样box可以嵌套或者包含多个box。一般真正的视频数据都在“mdat”类型的box中。

一般MP4文件中有音视频track,每个track则有很多chunk,chunk内有多个sample。从track一直到sample,都是按照层级/表进行组织的。

所以,从MP4文件中找到相应的sample,就可以把数据提取出来。

h264/h265视频流

这种视频流中的每个单元为NALU,NALU之间以start code(0x00000001)进行分割。

由上面的两点基础信息,就可回到问题本身了。

提取MP4文件中的h264/h265视频流

这个问题其实非常简单,核心问题其实只需要一句话。摘自ISO-IEC-14496-15 5.2.2.

ISO-IEC-14496-15 5.2.2
1
2
3
4
5
No start codes.
The elementary streams shall not include start codes. As stored, each NAL unit is
preceded by a length field as specified in 5.2.3; this enables easy scanning of the sample’s NAL units.
Systems that wish to deliver, from this file format, a stream using start codes will need to reformat the
stream to insert those start codes.

这部分的意思是
ES数据不包含start code,而是存储上NALU的长度。反向进行时候,需要将start code添加回来。

NALU的长度的长度信息则存储在MP4文件中的“avcC”中。具体的定义如下

aligned(8) class AVCDecoderConfigurationRecord {
    unsigned int(8) configurationVersion = 1;
    unsigned int(8) AVCProfileIndication;
    unsigned int(8) profile_compatibility;
    unsigned int(8) AVCLevelIndication;
    bit(6) reserved = ‘111111’b;
    unsigned int(2) lengthSizeMinusOne; // offset 4
    bit(3) reserved = ‘111’b;
    unsigned int(5) numOfSequenceParameterSets;
    for (i=0; i< numOfSequenceParameterSets; i++) {
        unsigned int(16) sequenceParameterSetLength ;
        bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;
    }
    unsigned int(8) numOfPictureParameterSets;
    for (i=0; i< numOfPictureParameterSets; i++) {
        unsigned int(16) pictureParameterSetLength;
        bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;
    }
}

一个典型的“avcC”内容如下:

1
2
3
01 42 c0 15 fd e1 00 17 67 42 c0 15 92 44 0f 04
7f 58 08 80 00 00 3e 80 00 0b b5 47 8b 17 50 01
00 04 68 ce 32 c8

解析的结果(内部的数值都是十六进制的)

1
2
3
4
5
6
7
8
9
10
11
12
version: 01
profile: 42
compatibilty: c0
level: 15
lengthSize: 2 (0xfd -> 2)
num of SPS: 1
SPS len: 17
SPS: 67 42 c0 15 92 44 0f 04 7f 58 08 80 00 00 3e 80
00 0b b5 47 8b 17 50
num of PPS: 1
PPS len: 4
PPS: 68 ce 32 c8

根据里面的lengthSize就可以得到“NALU的长度”这部分数据的长度。

+---+-------+---+-------+--+-------+---
|len|  NAL  |len|  NAL  |  |       |
+---+-------+---+-------+--+-------+---

注意,这里的顺序是大端(网络序)的。一般一个sample有一个NALU(当然不是绝对的)。

SPS和PPS是h264流中的元信息,在MP4文件中单独存放在“avcC”中。(如上所示),转换的时候,还需要将SPS和PPS提取出来,添加上0x00000001,放在h264视频流的开始位置。

h265的处理方法

其实几乎是一样的,对于h265,其元信息在”hvcC”类型box中。具体的语法结构就不贴了。其中lengthSize的偏移地址为21.

code

很多开源代码珠玉在前,我就不献丑了。

ffmpeg中,可以参考libavcodec/h264_mp4toannexb_bsf.c. 这个文件不长,只有300行左右。其功能完成就是本文讨论的内容,完全可以对比着一一参考。

- h264_extradata_to_annexb 处理avcC中的数据,并且提取出sps和pps信息
- h264_mp4toannexb_filter 从sample中提取nalu数据

Android的代码中可以参考libstagefright中的MPEG4Extractor.cpp文件。这个代码展示了完整的如何解析MP4文件的过程。关于本文讨论的内容,在read函数中3883行附近。在代码中搜索lengthsize也可以比较容易定位到。