美文网首页
H5直播系列九 flv.js源码之flv-demuxer.js

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

作者: 合肥黑 | 来源:发表于2018-10-11 18:13 被阅读230次

    参考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的裸流里常见的是前一种。

    相关文章

      网友评论

          本文标题:H5直播系列九 flv.js源码之flv-demuxer.js

          本文链接:https://www.haomeiwen.com/subject/ijabaftx.html