jsmpeg系列五 源码mpeg1.js MPEG1码流结构
参考
MPEG1和MPEG2码流结构分析
mpeg文件格式分析
摘自百度百科:MPEG-1是MPEG组织制定的第一个视频和音频有损压缩标准。视频压缩算法于1990年定义完成。1992年底,MPEG-1正式被批准成为国际标准。MPEG-1是为CD光盘介质定制的视频和音频压缩格式。一张70分钟的CD光盘传输速率大约在1.4Mbps。而MPEG-1采用了块方式的运动补偿、离散余弦变换(DCT)、量化等技术,并为1.2Mbps传输速率进行了优化。主要特点有随机访问,灵活的帧率、可变的图像尺寸、定义了I-帧、P-帧和B-帧 、运动补偿可跨越多个帧 、半像素精度的运动向量 、量化矩阵、GOF结构 、slice结构 、技术细节、输入视频格式。MPEG-1随后被Video CD采用作为核心技术。VCD的分辨率只有约352×240,并使用固定的比特率(1.15Mbps),因此在播放快速动作的视频时,由于数据量不足,令压缩时宏区块无法全面调整,结果使视频画面出现模糊的方块。因此MPEG-1的输出质量大约和传统录像机VCR相当,这也许是Video CD在发达国家未获成功的原因。
Mpeg-1编码后的视频序列是一个如同计算机网络的OSI模型下的数据序列一样,数据被分成很多层的概念。视频序列层-画面组层-画面层-片层-宏块层-块层。层次的关系很明显,越往后越是底层,越接近实际的数据。
一、视频序列层(VideoStream)
视频序列是以一个序列标题开始,之后可以跟着一个或者多个画面组。最后以Sequence_end_code结束。紧挨着每一个画面组之前可以有一个序列标题。也就是说每个画面组,都可以有一个自己的序列标题。
序列标题是一个以sequence_header_code开始,后跟着一系列数据元素的结构。是视频流中用来解码的重要的参数之一。其中定义了量化矩阵(load_intra_quantizer_matrix和 load_non_intra_quantizer_matrix以及可选的intra_quantizer_matrix和non_intra_quantizer_ matrix)以及其它的一些重要的数据元素,其中量化矩阵是可以在视频流中重复的量化矩阵中变化的,并且在每次变化后,量化矩阵重新定义。其它的元素必须与第一个序列标题中的值相同。
由于MPEG1与MPEG2的结构类似,这里就主要以MPEG2来进行说明。首先来给一张MPEG2 video Sequence的一个结构图:
与采用Elecard Stream Analyer分析出的结果基本一致:
image.png
有了上面的图后,大家就可以清晰的看出MPEG2 video Sequence的一个大体结构了。
下面我们来以例子来进行码流结构的简单分析和说明:
比如:Test.m2v
00 00 01 B3 08 00 80 23 00 FA 20 30 00 00 01 B5
1.以视频系列头Sequence Header开始
Start code values: 00 00 01 B3 为起始码;
然后是08 00 80
horizontal_size_value(12 bits):08 0 = 128
vertical_size_value(12 bits):0 80 = 128
然后是 23
aspect_ratio_information(4 bits) = 2
frame_rate_code (4 bits) = 3
然后是00 FA 20 = 0000 0000,1111 1010,0010 0000
bit_rate_value(18 bits)= 0000 0000 1111 1010 00 = 1000
marker_bit(1 bit) = 1
然后是30=0011 0000
vbv_buffer_size_value(10 bits) = 0 0000 0011 0 = 6
constrained_parameters_flag(1 bit) = 0
load_intra_quantiser_matrix( 1bit) = 0
load_non_intra_quantiser_matrix(1bit) = 0
以上分析流程在mpeg1.js中可以找到相应代码
MPEG1.prototype.frameRate = 30;
MPEG1.prototype.decodeSequenceHeader = function() {
var newWidth = this.bits.read(12),
newHeight = this.bits.read(12);
// skip pixel aspect ratio
this.bits.skip(4);
this.frameRate = MPEG1.PICTURE_RATE[this.bits.read(4)];
// skip bitRate, marker, bufferSize and constrained bit
this.bits.skip(18 + 1 + 10 + 1);
if (newWidth !== this.width || newHeight !== this.height) {
this.width = newWidth;
this.height = newHeight;
this.initBuffers();
if (this.destination) {
this.destination.resize(newWidth, newHeight);
}
}
if (this.bits.read(1)) { // load custom intra quant matrix?
for (var i = 0; i < 64; i++) {
this.customIntraQuantMatrix[MPEG1.ZIG_ZAG[i]] = this.bits.read(8);
}
this.intraQuantMatrix = this.customIntraQuantMatrix;
}
if (this.bits.read(1)) { // load custom non intra quant matrix?
for (var i = 0; i < 64; i++) {
var idx = MPEG1.ZIG_ZAG[i];
this.customNonIntraQuantMatrix[idx] = this.bits.read(8);
}
this.nonIntraQuantMatrix = this.customNonIntraQuantMatrix;
}
this.hasSequenceHeader = true;
};
上述代码中的this.destination.resize(newWidth, newHeight);
destination正是在player.js中传入的render,也就是JSMpeg.Renderer.WebGL或JSMpeg.Renderer.Canvas2D。
另外,this.frameRate = MPEG1.PICTURE_RATE[this.bits.read(4)];
这个帧率也有常量数组对应。
2.00 00 01 B5正是extension_start_code,根据以上图片文字if no Sequence extension is present,the sequence is an MPEG-1 element viedo stream.In an MPEG-2 stream,every Sequence header must be followed by an extension.
我们可以得知,MPEG-1是没有extension_start_code 00 00 01 B5,以及后续的参数,所以mpeg1.js在decodeSequenceHeader后,直接跳到decodePicture了,跳过的这些参数参考MPEG1和MPEG2码流结构分析,本文不再引述。
二、开始码
对于各种start Code,在标准中亦有清楚地说明,见Table 6-1 — Start code values
Name | start code value(hexadecimal) |
---|---|
picture_start_code | 00 |
Slice_start_code | 01 through AF |
Reserved | B0 |
Reserved | B1 |
User_data_start_code | B2 |
sequence_header_code | B3 |
sequence_error_code | B4 |
extension_start_code | B5 |
Reserved | B6 |
Sequence_end_code | B7 |
group_start_code | B8 |
system start codes (see note) | B9 through FF |
在mpeg1.js中有常量如下
MPEG1.START = {
SEQUENCE: 0xB3,
SLICE_FIRST: 0x01,
SLICE_LAST: 0xAF,
PICTURE: 0x00,
EXTENSION: 0xB5,
USER_DATA: 0xB2
};
正是由于视频序列中存在很多开始码,或者称之为定位码、同步码。用来告诉解码器目前数据的区域信息,所以解码器才可以正确的处理各个数据区的数据。这些开始码都是一些特殊的32bits的比特序列,在视频码流中不会出现的。他们的起着标志的作用,具体可以从名称上面看出来。其中EXT_START_CODE和USER_START_CODE在每个层里面都会出现,用来标志扩展数据区和用户数据区,用来添加任意的数据,直到下一个开始码结束。
video_sequence()
{
next_start_code()
do
{
sequence_header();
do
{
group_of_pictures() ; //画面组
}while (nextbits()==GROUP_START_CODE)
}while(nextbits()==SEQUENCE_HEADER_CODE)
SEQUENCE_END_CODE
};
三、画面组层(GOP)
Mpeg流最终显示出来是一系列的画面,而画面组是mpeg流中可以独立编码的最小的单位,每个画面组由一个标题和一系列画面组成。GOP标题包含了时间和编辑的信息。
Mpeg画面组中必须至少有一个I帧画面,可以有数目可变的B帧和P帧画面,也可以没有P和B帧。画面组的第一幅编码画面是I画面,该画面之后跟随着任意数目的I或P画面,每对I、P画面之间可以插入任意数目的B画面。画面组是画面的集合,每幅画面按照显示的顺序相邻。画面组中的画面有两种排列顺序:
- 1.按比特流顺序 必须以I帧开头,后面可按任何的次序,跟上任意数目的I,P或B画面。
- 2.按显示顺序必须以I或B画面打头,且以I或P画面结束,最小的画面组由一个I画面组成。
从编码角度,可以精确的陈述的是,画面组以一个画面组标题开始,以最先出现的下一个画面组标题或者下一个序列标题或者序列结束码结束。
当然每个画面组层都是开始标志码:GOP_START_CODE 00 00 01 B8 为起始码;
00 00 01 B8 5F BF 6C 40
time_code(25): 0101 1111 1011 1111 0110 1100 0 = 49022
closed_gop(1): 1
broken_link(1):0
剩余5bit 0 0000
但是关于这部分解析,在mpeg1.js中没看到GOP layer,原因不明。
四、画面层(Pictures)
画面组层中的一幅幅画面就是画面层的数据了。包含了一幅画面的所有编码信息。一幅画面同样始于画面的标题。标题以画面开始码(PICTURE_START_CODE 0x00000100)打头。
Start code values : 00 00 01 00 为起始码
然后是00 00 01 00 00 0A 58 58 00 00 01 B5
其中00 0A=0000 0000 0000 1010
temporal_reference(10): 0000 0000 00
picture_coding_type(3): 00 1
然后是58 58=0101 1000 0101 1000
vbv_delay(16):001 1000 1001 1
extra_bit_picture(1):0
注:这里原文的vbv_delay解析错了,应该是010 0101 1000 0101 1
temprol_reference 时序编号,通常一组画面的编号都在1024以内,如果超过那么在1025幅画面出复位为0,重新计数。
vbv_delay 对于固定比特率的视频流,vbv_delay用与解码过程开始和随机存取之后,以保证在第一幅画面被显示之前,解码器已经读到正确数目的比特数
参考mpeg1.js的decodePicture
MPEG1.prototype.decodePicture = function(skipOutput) {
this.currentFrame++;
this.bits.skip(10); // skip temporalReference
this.pictureType = this.bits.read(3);
this.bits.skip(16); // skip vbv_delay
// Skip B and D frames or unknown coding type
if (this.pictureType <= 0 || this.pictureType >= MPEG1.PICTURE_TYPE.B) {
return;
}
这里显然,把B帧跳掉了,也就是说jsmpeg库是不支持B帧的。
JSMpeg only supports playback of MPEG-TS containers with the MPEG1 Video Codec and the MP2 Audio Codec. The Video Decoder does not handle B-Frames correctly (though no modern encoder seems to use these by default anyway) and the width of the video has to be a multiple of 2.
下面是对P帧的参数读取
// full_pel_forward, forward_f_code
if (this.pictureType === MPEG1.PICTURE_TYPE.PREDICTIVE) {
this.fullPelForward = this.bits.read(1);
this.forwardFCode = this.bits.read(3);
if (this.forwardFCode === 0) {
// Ignore picture with zero forward_f_code
return;
}
this.forwardRSize = this.forwardFCode - 1;
this.forwardF = 1 << this.forwardRSize;
}
然后就是
while (code >= MPEG1.START.SLICE_FIRST && code <= MPEG1.START.SLICE_LAST) {
this.decodeSlice(code & 0x000000FF);
code = this.bits.findNextStartCode();
}
五、片层(Slice)
片是任意数目宏块组成的序列,其中宏块必须从画面的左上位置开始,按照光栅扫描的方向从左到右,从上到下排列。片中至少包涵一个宏块,片与片之间没有重叠,也没有间隙。
首先给出识别出Slice层数据的头标slice_start_code
#define SLICE_MIN_START_CODE 0x00000101
#define SLICE_MAX_START_CODE 0x000001af
这部分的mpeg1.js代码,看不懂
MPEG1.prototype.decodeSlice = function(slice) {
this.sliceBegin = true;
this.macroblockAddress = (slice - 1) * this.mbWidth - 1;
// Reset motion vectors and DC predictors
this.motionFwH = this.motionFwHPrev = 0;
this.motionFwV = this.motionFwVPrev = 0;
this.dcPredictorY = 128;
this.dcPredictorCr = 128;
this.dcPredictorCb = 128;
this.quantizerScale = this.bits.read(5);
// skip extra bits
while (this.bits.read(1)) {
this.bits.skip(8);
}
do {
this.decodeMacroblock();
} while (!this.bits.nextBytesAreStartCode());
};
六、宏块层(Macroblock)
宏块是包含16pixels*16lines的亮度分量部分,以及在空间位置上对应的两个8pixels*8lines的色度分量部分,一个宏块有4个亮度块和2个色度块。宏块可以指源图像或者重构图像的数据,或者是量化后的DCT系数。
这部分的mpeg1.js代码decodeMacroblock,看不懂
七、块层(Block)
块是一个正交的8pixels*8lines的亮度或者色度分量,块可以指源画面数据或者相应的编码数据元素。8*8单位象素的源画面数据经过DCT变换后的成为了相应的DCT系数块。
这部分的mpeg1.js代码decodeBlock,看不懂
八、收尾
this.currentY = new Uint8ClampedArray(this.codedSize);
...
// Invoke decode callbacks
if (this.destination) {
this.destination.render(this.currentY, this.currentCr, this.currentCb);
}
终于解析完了,最后交给destination去渲染了。扔出去YCrCb分量数组,类型是Uint8ClampedArray的。具体参考canvas2d.js和webgl.js