js对flv提取h264、aac音视频流

摘要:
**ps:下面的图片很多是采用别人的,我也忘记备注来源了**1.flv的协议结构FLV文件由FLVheader和FLVbody组成,FLVbody由一系列的FLVtags组成,如下图所示:tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流,而每个tag又由tagheader和tagdata组成。
FLV提取里面的h264视频流

FLV和MP4支持的编码

js对flv提取h264、aac音视频流第1张

流媒体和媒体文件的区别

流媒体是指将一连串的多媒体资料压缩后,经过互联网分段发送资料,在互联网上即时传输影音以供观赏的一种技术与过程,此技术使得资料数据包得以像流水一样发送,如果不使用此技术,就必须在使用前下载整个媒体文件。flv属于流媒体格式,所以很适合做低延时的直播

对比hls和mp4

相对于mp4,flv更加灵活体积更小,mp4不是流媒体需要索引表才可以正常播放
相对于hls,flv可以做到延时更低,因为hls需要发起多次http短连接请求播放,而flv可以通过http长连接结合ReadableStream做到更小切片的播放。

** ps:下面的图片很多是采用别人的,我也忘记备注来源了 **

1.flv的协议结构

FLV文件由FLV header和FLV body组成,FLV body由一系列的FLV tags组成,如下图所示:
flv协议(图2)
tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流,而每个tag又由tag header和tag data组成。每个Tag前面还包含了Previous Tag Size字段,表示前面一个Tag的大小。整个FLV文件的详细的组成如下图所示:
flv协议(图1)

下面是一个flv视频的hex编码:

flv header

flv协议(图3)
这里前面9个字节为flv的header
0x46:ASCII编码里的"F"
0x4c: ASCII编码里的"L"
0x56: ASCII编码里的"V"
0x01: FLV的版本号
0x05: 对应二进制为0000 0101,意思为包含视频和音频
0x 00 00 00 09: 表示flv body的起始字节位置

flv的body:

flv协议(图4)
****这里flv body的前4个字节总是0

tag的结构

tag Header

js对flv提取h264、aac音视频流第6张
type:0x12 (18为元数据tag,9为视频tag,8为音频tag,占1个字节)
dataSize:0x0001AC = 428 (tagbody的长度,占3个字节)
timeStamp: 0x00000000 = 0 (tag对应的时间戳,其中最后一个字节代表高位,一共4个字节)
streamId:0x000000 = 0 (一直为0)

tag Data
video data构成

js对flv提取h264、aac音视频流第7张
视频Tag也用开始的第1个字节包含视频数据的参数信息,从第2个字节为视频流数据。结构如下图所示
js对flv提取h264、aac音视频流第8张
第1个字节的前4位表示帧类型,各个取值的含义如下:
js对flv提取h264、aac音视频流第9张
后4位表示视频编码类型,各个取值的含义如下:
js对flv提取h264、aac音视频流第10张

字节位置描述
1视频参数信息,帧类型和编码类型(如上图)
2该video tag data的类型,0为AVC packet type, 1为NALU,这里AVC packet type包含了该视频下面的一下公共信息,NALU则是h264的基本构成。2为结束标志
3~5composition time,AVC时,全0,无意义

看下截图的数据:
0x17:1-keyframe 7-avc
0x00:AVC sequence header -- AVC packet type
0x000000: composition time,AVC时,全0,无意义

  • AVC sequence header数据结构(video data第6个字节开始)
字节位置描述截图数据
6configurationVersion 配置版本0x01
7AVCProfileIndication AVC配置文件指示0x64
8profileCompatibility 配置文件兼容性0x00
9AVCLevelIndication AVC级别0x1e
10lengthSizeMinusOne FLV中NALU包长数据所使用的字节数,(lengthSizeMinusOne & 3)+1,实际测试时发现总为ff,计算结果为4(0xff & 3) + 1 = 4
11numOfSequenceParameterSets (E1 -- SPS 的个数,numOfSequenceParameterSets & 0x1F)0xe1 & 0x1f = 1
12~13sequenceParameterSetLength SPS 的长度,2个字节0x001a=26
14~14+sequenceParameterSetLengthSPS 数据0x27 ... 0x92
14+sequenceParameterSetLength+1PPS 的个数,实际测试时发现总为010x01
14+sequenceParameterSetLength+2pictureParameterSetLength PPS 的长度0x0004=4
14+sequenceParameterSetLength+3 ~ dataEndPPS 数据0x28ee3cb0

分析截图数据中,比较重要的只有lengthSizeMinusOne = 4字节,这里需要存起来因为下面的NALU解析时需要用到。

  • NALU数据结构

NALU的小知识
js对flv提取h264、aac音视频流第11张

类型描述
SPS序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数
PPS图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数
I帧帧内编码帧,可独⽴解码⽣成完整的图⽚
P帧前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚
B帧双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚

下面是第二个video tag的截图:
js对flv提取h264、aac音视频流第12张
第二个字节为0x01,说明下面是NALU包,一个tag可以包含多个NALU(h264的NALU之间需要用0X000000或0x00000000作为间隔,不过flv内是不包含的)
第3~5字节为composition time,可以忽略不记
所以由第6个字节开始,从第一个video tag的AVC sequence header可以得知每个NALU的数据长度由起始的4个字节描述。
所以第一个NALU的数据长度为:0x0000001A = 26byte
数据为:0x276400 ... 92
这里其中第一个字节的前5位为该NAL包的类型

0x27 & 0x1f = 7

NAl的类型对照表:

#define NALU_TYPE_SLICE 1
#define NALU_TYPE_DPA 2
#define NALU_TYPE_DPB 3
#define NALU_TYPE_DPC 4
#define NALU_TYPE_IDR 5
#define NALU_TYPE_SEI 6
#define NALU_TYPE_SPS 7
#define NALU_TYPE_PPS 8
#define NALU_TYPE_AUD 9
#define NALU_TYPE_EOSEQ 10
#define NALU_TYPE_EOSTREAM 11
#define NALU_TYPE_FILL 12

一个NALU结束后的4个字节为下个NALU的长度,以此下去。
代码实现抽取NALU:

uint8Array // 以获得的flv数据,下面只是针对video tag的解析,不是完整代码
let idx
dataLeng = (uint8Array[idx++] << 0x10) + (uint8Array[idx++] << 0x08) + uint8Array[idx++];
timeStamp = (uint8Array[idx + 3] << 24) + (uint8Array[idx++] << 16) + (uint8Array[idx++] << 8) + uint8Array[idx++]
idx+= (1 + 3)
const dataStartIdx = idx // data起始idx
videoTotalTime += timeStamp
const isIKeyframe = (uint8Array[idx] & 0xf0) === 16  // 是否为关键帧
const codeId = (uint8Array[idx++] & 0x0f)  // 视频编码类型(7为avc)
const isAVCSequenceHeader = uint8Array[idx++] === 0 // 是否为avc头部,只有一个
if (isAVCSequenceHeader) {
  const compositionTime = 0 // AVC时,全0,无意义(直接跳过3个字节)
  idx+=3
  const configurationVersion = uint8Array[idx++] // 配置版本
  const AVCProfileIndication = uint8Array[idx++] // AVC配置文件指示
  const profileCompatibility = uint8Array[idx++] // 配置文件兼容性
  const AVCLevelIndication = uint8Array[idx++] // AVC等级指示
  const lengthSizeMinusOne = (uint8Array[idx++] & 3) + 1 //FLV中NALU包长数据所使用的字节数,(lengthSizeMinusOne & 3)+1,实际测试时发现总为ff,计算结果为4
  const numOfSequenceParameterSets = uint8Array[idx++] & 0x1f //  01 -- SPS 的个数,numOfSequenceParameterSets & 0x1F
  const sequenceParameterSetLength = (uint8Array[idx++] << 8) + uint8Array[idx++] // SPS 的长度,2个字节
  videoArr.push(this.concatenate(Uint8Array, [new Uint8Array([0,0,0,1]), uint8Array.slice(idx, idx + sequenceParameterSetLength)]))
  idx += sequenceParameterSetLength
  const numOfPictureParameterSets = uint8Array[idx++] // PPS 的个数,实际测试时发现总为E1
  const pictureParameterSetLength = (uint8Array[idx++] << 8) + uint8Array[idx++] // PPS 的长度
  videoArr.push(this.concatenate(Uint8Array, [new Uint8Array([0,0,0,1]), uint8Array.slice(idx, idx + pictureParameterSetLength)]))
  idx += pictureParameterSetLength
  videoConfig = {
    compositionTime,
    configurationVersion,
    AVCProfileIndication,
    profileCompatibility,
    AVCLevelIndication,
    lengthSizeMinusOne,
  }
} else { // 非头部tag
  const compositionTime = (uint8Array[idx++] << 16) + (uint8Array[idx++] << 8) + uint8Array[idx++]
  // header得到的lengthSizeMinusOne
  while(dataLeng + dataStartIdx > idx) {
    let i = 1
    let naluLength = 0
    while(i <= videoConfig.lengthSizeMinusOne) {
      naluLength += (uint8Array[idx++] << ((videoConfig.lengthSizeMinusOne - i) * 8))
      i++
    }
    videoArr.push(this.concatenate(Uint8Array, [new Uint8Array([0,0,0,1]), uint8Array.slice(idx, idx + naluLength)]))
    idx += naluLength
  }
}
idx += 4 // preTagSize
audio data构成

js对flv提取h264、aac音视频流第13张
js对flv提取h264、aac音视频流第14张
前两个字节为公共头部

字节位置描述
1音频参数
2AACPacketType 0为AudioSpecificConfig, 1为AACframeData

音频参数数据结构

描述截图数据分析
1~4format编码类型0xAF&0xF0=10
5~6rate采样率(0xAF&0x0c)>>2=3
7sampleSize采样精度(0xAF & 0x02) >> 1=1
8audiotype音频类型0xAF&0x01=1

js对flv提取h264、aac音视频流第15张
js对flv提取h264、aac音视频流第16张
js对flv提取h264、aac音视频流第17张
js对flv提取h264、aac音视频流第18张
第二个字节为0x00,所以下面为AudioSpecificConfig数据,因为AudioSpecificConfig只出现一次,所以需要记录起来。
AudioSpecificConfig的数据可以由第3、4个字节获取。
具体数据结构如下:

字段描述
1~5audioObjectType编码结构类型
6~9samplingFrequencyIndex音频采样率索引值,44100对应值4
10~13channelConfiguration音频输出声道
14frameLengthFlag标志位,用于表明IMDCT窗口长度,0
15dependsOnCoreCoder标志位,表明是否依赖于corecoder,0
16extensionFlag延时标志位
  • flv存储的AAC数据为AAC为es数据流,不能直接播放,如果想要播放需要在每个es流前面加上ADTS头部,所以一个完整可播放的AAC为:
    js对flv提取h264、aac音视频流第19张

这里ADTS由adts_fixed_header和adts_variable_header组成
其一为固定头信息,紧接着是可变头信息。固定头信息中的数据每一帧都相同,而可变头信息则在帧与帧之间可变
adts_fixed_header:

字段描述长度(bits)
syncword同步头 总是0xFFF, all bits must be 1,代表着一个ADTS帧的开始12
IDMPEG标识符,0标识MPEG-4,1标识MPEG-21
Layeralways: '00'2
protection_absent表示是否误码校验。Warning, set to 1 if there is no CRC and 0 if there is CRC1
profile表示使用哪个级别的AAC,如01 Low Complexity(LC)--- AAC LC。有些芯片只支持AAC LC,值等于 Audio Object Type的值减12
sampling_frequency_index表示使用的采样率下标4
private bit01
channel_configuration表示声道数,比如2表示立体声双声道3
original01
home01

adts_variable_header:

字段描述长度(bits)
copyright_id_bit01
copyright_id_start01
aac_frame_length一个ADTS帧的长度包括ADTS头和AAC原始流13
adts_buffer_fullness0x7FF 说明是码率可变的码流11
number_of_raw_data_blocks_in_frame002

第二个audio data里的AACPacketType都会为1,所以只要便利所有的audio tag,给每个es流前面加上ADTS头部就可以了

实现代码:

uint8Array // 以获得的flv数据,下面只是针对audio tag的解析,不是完整代码
let idx
dataLeng = (uint8Array[idx++] << 0x10) + (uint8Array[idx++] << 0x08) + uint8Array[idx++];
timeStamp = (uint8Array[idx + 3] << 24) + (uint8Array[idx++] << 16) + (uint8Array[idx++] << 8) + uint8Array[idx++]
idx += (1 + 3)
const audioDataEndIdx = idx + dataLeng
const info = uint8Array[idx++]
const format = info & 0xF0 // 编码类型
const rate = (info & 0x0c) >> 2 // 采样率
const sampleSize = (info & 0x02) >> 1 // 采样精度
const audiotype = (info & 0x01) // 音频类型
const isAudioSpecificConfig = !uint8Array[idx++]
if (isAudioSpecificConfig) {
  audioSpecificConfig = this.getAudioSpecificConfig(uint8Array[idx++], uint8Array[idx++])
  idx = audioDataEndIdx
} else {
  const adtsLen = dataLeng - 2 + 7
  let ADTS = new Uint8Array(7)
  ADTS[0] = 0xff // syncword:0xfff                           高8bits
  ADTS[1] = 0xf0 // syncword:0xfff                           低4bits
  ADTS[1] |= (0 << 3) // MPEG Version:0 for MPEG-4,1 for MPEG-2   1bit
  ADTS[1] |= (0 << 1) // Layer:0                                  2bits
  ADTS[1] |= 1 // protection absent:1                      1bit

  ADTS[2] = (audioSpecificConfig.audioObjectType - 1) << 6 // profile:audio_object_type - 1                      2bits
  ADTS[2] |= (audioSpecificConfig.samplingFrequencyIndex & 0x0f) << 2 // sampling frequency index:sampling_frequency_index  4bits
  ADTS[2] |= (0 << 1) // private bit:0                                      1bit
  ADTS[2] |= (audioSpecificConfig.channelConfiguration & 0x04) >> 2 // channel configuration:channel_config               高1bit

  ADTS[3] = (audioSpecificConfig.channelConfiguration & 0x03) << 6 // channel configuration:channel_config     低2bits
  ADTS[3] |= (0 << 5) // original:0                               1bit
  ADTS[3] |= (0 << 4) // home:0                                   1bit
  ADTS[3] |= (0 << 3) // copyright id bit:0                       1bit
  ADTS[3] |= (0 << 2) // copyright id start:0                     1bit

  ADTS[3] |= (adtsLen & 0x1800) >> 11 // frame length:value    高2bits
  ADTS[4] = (adtsLen & 0x7f8) >> 3 // frame length:value    中间8bits
  ADTS[5] = (adtsLen & 0x7) << 5 // frame length:value    低3bits
  ADTS[5] |= 0x1f // buffer fullness:0x7ff 高5bits
  ADTS[6] = 0xfc
  audioArr.push(this.concatenate(Uint8Array, [ADTS, uint8Array.slice(idx, audioDataEndIdx)]))
  idx = audioDataEndIdx
}
idx += 4
Metadata Tag

主要是描述该flv的信息,例如宽高,时长等等。所处位置为第一个tag

播放h264和aac

js对flv提取h264、aac音视频流第20张

Fragmented MP4文件格式

在Fragmented MP4文件中都有三个非常关键的boxes:‘moov’、‘moof’和‘mdat’。

(1)‘moov’(movie metadata box)

和普通MP4文件的‘moov’一样,包含了file-level的metadata信息,用来描述file。
(2)‘mdat’(media data box)

和普通MP4文件的‘mdat’一样,用于存放媒体数据,不同的是普通MP4文件只有一个‘mdat’box,而Fragmented MP4文件中,每个fragment都会有一个‘mdat’类型的box。
(3)‘moof’(movie fragment box)
该类型的box存放的是fragment-level的metadata信息,用于描述所在的fragment。该类型的box在普通的MP4文件中是不存在的,而在Fragmented MP4文件中,每个fragment都会有一个‘moof’类型的box。

一个‘moof’和一个‘mdat’组成Fragmented MP4文件的一个fragment,这个fragment包含一个video track或audio track,并且包含足够的metadata以保证这部分数据可以单独解码

免责声明:文章转载自《js对flv提取h264、aac音视频流》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Android流量统计解决ubuntu10.04无线掉线下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

js实现代彩色文字的下落

<html><head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>闪烁</title></head><style> *{margin:0}/* 解决兼容性问题   ...

JS判断鼠标从哪个方向进入DIV容器

   写的不够高大上 , 不要介意哦。。。 Js: //进去 $(".flash").bind("mouseenter",function(e){ /** the width and height of the current div **/ var w = $(this).width(); var h = $(this...

留言板小程序开发笔记-4

前端多个html文件, 共用变量的时候, 使用cookie, 或者在多个页面之间通过 url传递变量... 前端多个html页面之间, 可以共用一个css文件, 因为, 在不同的页面包含同一个css文件, 即使另一个页面,不包含某些 dom节点的时候, 它不会出错, 而且也会自动执行. 而多个html文件,共用一个 js 文件时, 由于不同文档的dom结...

3人从小公寓创业,到世界最大引擎公司,Unity创始人谈14年...

Unity创始人David Helgason出席了5月11 - 13日在上海举办的Unite 2017 Shanghai,并在大会期间接受了游戏陀螺的专访,动情地讲述了这14年来从3人在公寓创业,到成为千人规模的世界级公司的心路历程,以及对Unity未来的方向展望。下面就来了解一下这位北欧青年的创业史吧。 “我们不仅仅要承受发工资的压力,同时因为很多客户...

原生JS实现九宫格拼图

实现这个案例,需要考虑到鼠标的拖拽效果(onmousedown/onmousemove/mouseup) 拖拽分解: 按下鼠标---->移动鼠标----->松开鼠标 1.给目标元素添加onmousedown事件,拖拽的前提是在目标元素按下鼠标左键 2.当onmousedown事件发生后,此刻给document添加onmousemove事件,意味着...

js 计算浮点数

JS的浮点计算 最近遇到了数值计算的时候,计算结果出现了类似于199.9999999999999999999的情况,但是被用来计算的两个数值都只是两位数。 就像这样      --------》         0.1 + 0.2 = 0.30000000000000004。 其实对于浮点数的四则运算,几乎所有的编程语言都会有类似精度误差的问题,...