H5直播系列九 flv.js源码之flv-demuxer.js

参考FLV.JS 代码解读--demux部分

flv.js源码看一下关于FLV格式解析部分,很多奇怪的逻辑和数字,都可以参考H5直播系列八 FLV文件格式得到解释,可以对照观看。

一、解析头
// function parseChunks(chunk: ArrayBuffer, byteStart: number): number;
parseChunks(chunk, byteStart) {
    if (!this._onError || !this._onMediaInfo || 
    !this._onTrackMetadata || !this._onDataAvailable) {
        throw new IllegalStateException('Flv: onError & onMediaInfo
        & onTrackMetadata & onDataAvailable callback must be specified');
    }

    let offset = 0;
    let le = this._littleEndian;

    if (byteStart === 0) {  // buffer with FLV header
        if (chunk.byteLength > 13) {
            let probeData = FLVDemuxer.probe(chunk);
            offset = probeData.dataOffset;
        } else {
            return 0;
        }
    }
    ...

这里判断 > 13,其实就是FLV Header的9字节,加上Body中第一个Previous Tag Size的4字节。

static probe(buffer) {
    let data = new Uint8Array(buffer);
    let mismatch = {match: false};

    if (data[0] !== 0x46 || data[1] !== 0x4C ||
    data[2] !== 0x56 || data[3] !== 0x01) {
        return mismatch;
    }

    let hasAudio = ((data[4] & 4) >>> 2) !== 0;
    let hasVideo = (data[4] & 1) !== 0;

这里判断开头是不是46 4C 56 01,又去判断hasAudio,hasVideo,对照FLV Header很容易理解

一、Header
头部分由一下几部分组成,Signature(3 Byte)+Version(1 Byte)+Flags(1 Bypte)+DataOffset(4 Byte),共9字节。
1.signature
46 4C 56 正是FLV这三个字符的ASCII编码,这个是固定标识,表示它是FLV文件。
2.version
版本号0x01
3.Flags
0x05,对应二进制00000101,前面一个1表示有音频数据,后面一个1表示有视频数据。
4.DataOffset
此4字节共同组成一个无符号32位整数(使用大头序),表示文件从FLV Header开始到Flv Body的字节数,当前版本固定为9(0x00,0x00,0x00,0x09)

parseChunks 后面的代码就是在不断解析 tag,flv把一段媒体数据称为 TAG,每个tag有不同的type,实际上真正用到的只有三种type,8、9、18 分别对应,音频、视频和Script Data。

 if (tagType !== 8 && tagType !== 9 && tagType !== 18) {
    Log.w(this.TAG, `Unsupported tag type ${tagType}, skipped`);
    // consume the whole tag (skip it)
    offset += 11 + dataSize + 4;
    continue;
}

Tag Header由11字节组成,Previous Tag Size有4字节

Tag Header由11字节组成:
第1字节type:标志当前Tag的类型,音频(0x08),视频(0x09),Script Data(0x12),除此之外,其他值非法;
第2-4字节tag data size:表示一个无符号24位整型数值,表示当前Tag Data的大小;
第5-7字节Timestreamp:无符号24位整型数值(UI24),当前Tag的时间戳(单位为ms),第一个Tag的时间戳总为0;
第8字节TimestampExtended:为时间戳的扩展字节,当前24位不够用时,该字节作为最高位,将时间戳扩展为32位无符号整数(UI32)
第9-11字节stream id:UI24类型,表示Stream ID,总是0

因为TimestampExtended的存在(24位若不够,扩展到32位),出现了下面这段代码:

let ts2 = v.getUint8(4);
let ts1 = v.getUint8(5);
let ts0 = v.getUint8(6);
let ts3 = v.getUint8(7);
let timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);

先取三个字节按照Big-Endian转换成整数再在高位放上第四个字节。

解析完了 tag header后面分别按照不同的 tag type调用不同的解析函数。

switch (tagType) {
    case 8:  // Audio
        this._parseAudioData(chunk, dataOffset, dataSize, timestamp);
        break;
    case 9:  // Video
        this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);
        break;
    case 18:  // ScriptDataObject
        this._parseScriptData(chunk, dataOffset, dataSize);
        break;
}
二、_parseVideoData
if (codecId !== 7) {
    this._onError(DemuxErrors.CODEC_UNSUPPORTED, 
    `Flv: Unsupported codec in video frame: ${codecId}`);
    return;
}

this._parseAVCVideoPacket(arrayBuffer, dataOffset + 1, 
dataSize - 1, tagTimestamp, tagPosition, frameType);
image.png

codecID=7是AVC,也就是H264。简单粗暴地讲,不是H264的,直接抛出错误,表示不支持。后面必然要解析pps,sps了。

视频的格式(CodecID)是AVC(H.264)的话,VideoTagHeader会多出4个字节的信息,AVCPacketType 和CompositionTime。AVCPacketType 占1个字节,CompositionTime 占3个字节

let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);

let packetType = v.getUint8(0);
let cts_unsigned = v.getUint32(0, !le) & 0x00FFFFFF;
let cts = (cts_unsigned << 8) >> 8;  // convert to 24-bit signed int

1.CompositionTime
解释下 CTS的概念,CompositionTime,我们前面在tag header里拿到过一个 timestamp,这个在视频里对应于DTS,就是解码时间戳,而CTS实际上是一个offset,表示 PTS相对于DTS的偏移量,就是 PTS和DTS的差值。

这里有个坑,参考adobe的文档,这是CTS是个有符号的24位整数,SI24,就是说它有可能是个负数。因为负数的24位整型到32位负数转换的时候要手工处理高位的符号位和补码问题。


image.png
if (packetType === 0) {  // AVCDecoderConfigurationRecord
    this._parseAVCDecoderConfigurationRecord(
    arrayBuffer, dataOffset + 4, dataSize - 4);
} else if (packetType === 1) {  // One or more Nalus
    this._parseAVCVideoData(arrayBuffer, dataOffset + 4,
    dataSize - 4, tagTimestamp, tagPosition, frameType, cts);
} else if (packetType === 2) {
    // empty, AVC end of sequence
} else {
    this._onError(DemuxErrors.FORMAT_ERROR,
    `Flv: Invalid video packet type ${packetType}`);
    return;
}

packetType有两种,0 表示 AVCDecoderConfigurationRecord,这个是H.264的视频信息头,包含了 sps 和 pps,AVCDecoderConfigurationRecord的格式不是flv定义的,而是264标准定义的,如果用ffmpeg去解码,这个结构可以直接放到 codec的extradata里送给ffmpeg去解释。

flv.js作者选择了自己来解析这个数据结构,也是迫不得已,因为JS环境下没有ffmpeg,解析这个结构主要是为了提取 sps和pps。虽然理论上sps允许有多个,但其实一般就一个。

pps的信息没什么用,所以作者只实现了sps的分析器,说明作者下了很大功夫去学习264的标准,其中的Golomb解码还是挺复杂的,能解对不容易,我在PC和手机平台都是用ffmpeg去解析的。SPS里面包括了视频分辨率,帧率,profile level等视频重要信息。

for (let i = 0; i < ppsCount; i++) {
    let len = v.getUint16(offset, !le);  // pictureParameterSetLength
    offset += 2;

    if (len === 0) {
        continue;
    }

    // pps is useless for extracting video information
    offset += len;
}
for (let i = 0; i < spsCount; i++) {
    let len = v.getUint16(offset, !le);  // sequenceParameterSetLength
    offset += 2;

    if (len === 0) {
        continue;
    }

    // Notice: Nalu without startcode header (00 00 00 01)
    let sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
    offset += len;

    let config = SPSParser.parseSPS(sps);
    if (i !== 0) {
        // ignore other sps's config
        continue;
    }

packetTtype 为 1 表示 NALU,NALU= network abstract layer unit,这是H.264的概念,网络抽象层数据单元,其实简单理解就是一帧视频数据。

NALU的头有两种标准,一种是用 00 00 00 01四个字节开头这叫 start code,另一个叫mp4风格以Big-endian的四字节size开头,flv用了后一种,而我们在H.264的裸流里常见的是前一种。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容