FLV封装格式介绍及解析

FLV封装格式

FLV(Flash Video)是Adobe公司推出的一种流媒体格式,由于其封装后的音视频文件体积小、封装简单等特点,非常适合于互联网上使用。目前主流的视频网站基本都支持FLV。采用FLV格式封装的文件后缀为.flv。

FLV封装格式是由一个文件头(flie header)和 文件体(file Body)组成。其中,FLV body由一对对的(Previous Tag Size字段 + tag)组成。Previous Tag Size字段 排列在Tag之前,占用4个字节。Previous Tag Size记录了前面一个Tag的大小,用于逆向读取处理。FLV header后的第一个Pervious Tag Size的值为0。Tag一般可以分为3种类型:脚本(帧)数据类型、音频数据类型、视频数据。FLV数据以大端序进行存储,在解析时需要注意。一个标准FLV文件结构如下图:


FLV文件结构

FLV文件的详细内容结构如下图:

FLV文件详细内容结构

FLV header

注:在下面的数据type中,UI表示无符号整形,后面跟的数字表示其长度是多少位。比如UI8,表示无法整形,长度一个字节。UI24是三个字节,UI[8*n]表示多个字节。UB表示位域,UB5表示一个字节的5位。可以参考c中的位域结构体。

FLV头占9个字节,用来标识文件为FLV类型,以及后续存储的音视频流。一个FLV文件,每种类型的tag都属于一个流,也就是一个flv文件最多只有一个音频流,一个视频流,不存在多个独立的音视频流在一个文件的情况。FLV头的结构如下:

Field Type Comment
签名 UI8 'F'(0x46)
签名 UI8 'L'(0x4C)
签名 UI8 'V'(0x56)
版本 UI8 FLV的版本。0x01表示FLV版本为1
保留字段 UB5 前五位都为0
音频流标识 UB1 是否存在音频流
保留字段 UB1 为0
视频流标识 UB1 是否存在视频流
文件头大小 UI32 FLV版本1时填写9,表明的是FLV头的大小,为后期的FLV版本扩展使用。包括这四个字节。数据的起始位置就是从文件开头偏移这么多的大小。

FLV Body

FLV Header之后,就是FLV File Body.FLV File Body是由一连串的back-pointers + tags构成。Back-pointer表示Previous Tag Size(前一个tag的字节数据长度),占4个字节。

FLV Body结构
FLV Tag

每一个Tag也是由两部分组成:tag header 和 tag data。Tag Header里存放的是当前tag的类型、数据区(tag data)的长度等信息。tag header一般占11个字节的内存空间。FLV tag结构如下:

Field Type Comment
Tag类型 UI8 8:audeo
9:video
18:Script data(脚本数据)
all Others:reserved 其他所有值未使用
数据区大小 UI24 当前tag的数据区的大小,不包含包头
时戳 UI24 当前帧时戳,单位是毫秒。相对值,第一个tag的时戳总是为0
时戳扩展字段 UI8 如果时戳大于0xFFFFFF,将会使用这个字节。这个字节是时戳的高8位,上面的三个字节是低24位。
StreamID UI24 总是为0
数据区 UI[8*n] 数据区数据




FLV Tag的类型可以是视频、音频和Script(脚本类型),下面分别介绍这三种Tag类型

Script Tag Data结构(脚本类型、帧类型)

该类型Tag又被称为MetaData Tag,存放一些关于FLV视频和音频的元信息,比如:duration、width、height等。通常该类型Tag会作为FLV文件的第一个tag,并且只有一个,跟在File Header后。该类型Tag DaTa的结构如下所示:

帧类型 Tag Data结构

第一个AMF包:
第1个字节表示AMF包类型,一般总是0x02,表示字符串。第2-3个字节为UI16类型值,标识字符串的长度,一般总是0x000A(“onMetaData”长度)。后面字节为具体的字符串,一般总为“onMetaData”(6F,6E,4D,65,74,61,44,61,74,61)。

第二个AMF包:
第1个字节表示AMF包类型,一般总是0x08,表示数组。第2-5个字节为UI32类型值,表示数组元素的个数。后面即为各数组元素的封装,数组元素为元素名称和值组成的对。常见的数组元素如下表所示。

Comment
duration 时长
width 视频宽度
heiht 视频高度
video data rate 视频码率
frame rate 视频帧率
video codec id 视频编码方式
audio sample rate 音频采样率
audio sample size 音频采样精度
stereo 是否为立体声
audio codec id 音频编码方式
filesize 文件大小
... ...
Audio Tag Data结构(音频类型)

音频Tag Data区域开始的第一个字节包含了音频数据的参数信息,从第二个字节开始为音频流数据。结构如下:

Audio Tag Data结构

第一个字节为音频的信息,格式如下:

Field Type Comment
音频格式 UB4 0 = Linear PCM, platform endian
1 =ADPCM
2 = MP3
3 = Linear PCM, little endian
4 = Nellymoser 16-kHz mono
5 = Nellymoser 8-kHz mono
6 = Nellymoser
7 = G.711 A-law logarithmic PCM
8 = G.711 mu-law logarithmic PCM
9 = reserved
10 = AAC
11 = Speex
14 = MP3 8-Khz
15 = Device-specific sound
flv是不支持g711a的,如果要用,可能要用线性音频。
采样率 UB2 0 = 5.5-kHz
1 = 11-kHz
2 = 22-kHz
3 = 44-kHz
对于AAC总是3。由此可以看出FLV封装格式并不支持48KHz的采样率
采样精度 UB1 0 = snd8Bit
1 = snd16Bit
压缩过的音频都是16bit
音频声道 UB1 0 = sndMono 单声道
1 = sndStereo 立体声,双声道
对于AAC总是1

第二个字节开始为音频数据。

Field Type Comment
音频数据 UI[8*n] 如果是PCM线性数据,存储的时候每个16bit小端存储,有符号。
如果音频格式是AAC,则存储的数据是AAC AUDIO DATA,否则为线性数组。
video Tag Data结构(视频类型)

视频Tag Data开始的第一个字节包含视频数据的参数信息,从第二个字节开始为视频流数据。结构如下:


video Tag Data结构

第一个字节包含视频信息,格式如下:

Field Type Comment
帧类型 UB4 1: keyframe (for AVC, a seekable frame)——h264的IDR,关键帧,可重入帧。
2: inter frame (for AVC, a non- seekable frame)——h264的普通帧
3: disposable inter frame (H.263 only)
4: generated keyframe (reserved for server use only)
5: video info/command frame
编码ID UB4 使用哪种编码类型:
1: JPEG (currently unused)
2: Sorenson H.263
3: Screen video
4: On2 VP6
5: On2 VP6 with alpha channel
6: Screen video version 2
7: AVC

第二个字节开始为视频数据

Field Type Comment
视频数据 UI[8*n] 如果是avc,则参考下面的介绍:AVC VIDEO PACKET
AVC VIDEO PACKET

关于下面这块内容有兴趣的话可以结合h264结构来看,不感兴趣的话可以直接跳过。

AVC VIDEO PACKET的结构:

Field Type Comment
AVC packet类型 UI8 0:AVC序列头
1:AVC NALU单元
2:AVC序列结束。低级别avc不需要。
CTS UI24 如果AVC packet类型是1,则为cts偏移(见下面的解释)。
如果AVC packet类型是0,则为0
数据 UI[8*n] 如果AVC packet类型是0,则是解码器配置,sps,pps。
如果是1,则是nalu单元,可以是多个。

关于CTS:这是一个比较难以理解的概念,需要和pts,dts配合一起理解。

首先,pts(presentation time stamps),dts(decoder timestamps),cts(CompositionTime)的概念:

pts:显示时间,也就是接收方在显示器显示这帧的时间。单位为1/90000 秒。

dts:解码时间,也就是rtp包中传输的时间戳,表明解码的顺序。单位单位为1/90000 秒。——根据后面的理解,pts就是标准中的CompositionTime

cts偏移:cts = (pts - dts) / 90 。cts的单位是毫秒。

pts和dts的时间不一样,应该只出现在含有B帧的情况下,也就是profile main以上。baseline是没有这个问题的,baseline的pts和dts一直相同,所以cts一直为0。

AVC VIDEO PACKET中Data的结构:

Field Type Comment
长度 UI32 nalu单元的长度,不包括长度字段。
nalu数据 UI[8*n] NALU数据,没有四个字节的nalu单元头,直接从h264头开始,比如:65 ** ** **,41 ** ** **
长度 UI32 nalu单元的长度,不包括长度字段。
nalu数据 UI[8*n] NALU数据,没有四个字节的nalu单元头,直接从h264头开始,比如:65 ** ** **,41 ** ** **
... ... ...

解析FLV

在理解了FLV结构的基础上,就可以尝试去解析一个FLV文件了。在阅读代码的过程中,遇到不懂的地方可以返回去看FLV结构,加深理解。

代码实现

下面是一段解析FLV文件的实例代码。
实现函数:

//
//  simplest_mediadata_flv.c
//  Codec_simple_demo
//
//  Created by guoqingping on 2018/4/25.
//  Copyright © 2018年 guoqingping. All rights reserved.
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//Important!
#pragma pack(1)


#define TAG_TYPE_SCRIPT 18  //帧tag
#define TAG_TYPE_AUDIO  8   //audio tag
#define TAG_TYPE_VIDEO  9   //video tag

typedef unsigned char byte;
typedef unsigned int uint;

typedef struct {
    byte Signature[3];  //文件标识,总是为FLV
    byte Version;   //版本号,总是为1
    byte Flags;   // 音视频流存在标识
    uint DataOffset;
} FLV_HEADER;   // FLV 文件头

typedef struct {
    byte TagType;  //tag类型
    byte DataSize[3];  //tag data字节数
    byte Timestamp[3];  //时戳
    uint Reserved;  //时间戳扩展字节+streamID 
} TAG_HEADER;  //tag头



//翻转字节,大段序转成小段序
uint reverse_bytes(byte *p, char c) {
    int r = 0;
    int i;
    for (i=0; i<c; i++)
        r |= ( *(p+i) << (((c-1)*8)-8*i));
    return r;
}


/**
 分析FLV 文件
 @param url FLV文件路径
 */
int simplest_flv_parser(char *url){
    
    //whether output audio/video stream
    int output_a=1;
    int output_v=1;
    //-------------
    FILE *ifh=NULL,*vfh=NULL, *afh = NULL;
    
    //FILE *myout=fopen("output_log.txt","wb+");
    FILE *myout=stdout;
    
    FLV_HEADER flv;
    TAG_HEADER tagheader;
    uint previoustagsize, previoustagsize_z=0;
    uint ts=0, ts_new=0;
    
    ifh = fopen(url, "rb+");
    if ( ifh== NULL) {
        printf("Failed to open files!");
        return -1;
    }
    
    //FLV file header 先读FLV头
    fread((char *)&flv,1,sizeof(FLV_HEADER),ifh);
    
    fprintf(myout,"============== FLV Header ==============\n");
    fprintf(myout,"Signature:  0x %c %c %c\n",flv.Signature[0],flv.Signature[1],flv.Signature[2]);
    fprintf(myout,"Version:    0x %X\n",flv.Version);
    fprintf(myout,"Flags  :    0x %X\n",flv.Flags);
    fprintf(myout,"HeaderSize: 0x %X\n",reverse_bytes((byte *)&flv.DataOffset, sizeof(flv.DataOffset)));
    fprintf(myout,"========================================\n");
    
    //move the file pointer to the end of the header
    fseek(ifh, reverse_bytes((byte *)&flv.DataOffset, sizeof(flv.DataOffset)), SEEK_SET);
    
    //process each tag
    do {
        //读取Previous tag size
        previoustagsize =  getw(ifh);
        // 读取tag header
        fread((void *)&tagheader,sizeof(TAG_HEADER),1,ifh);
        
        //int temp_datasize1=reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize));
        //获取tag data的字节数
        int tagheader_datasize=tagheader.DataSize[0]*pow(2, 16)+tagheader.DataSize[1]*pow(2, 8)+tagheader.DataSize[2];
        
        //获取时戳
        int tagheader_timestamp=tagheader.Timestamp[0]*pow(2, 16)+tagheader.Timestamp[1]*pow(2, 8)+tagheader.Timestamp[2];
        
        char tagtype_str[10];
        //获取tag类型
        switch(tagheader.TagType){
            case TAG_TYPE_AUDIO:sprintf(tagtype_str,"AUDIO");break;
            case TAG_TYPE_VIDEO:sprintf(tagtype_str,"VIDEO");break;
            case TAG_TYPE_SCRIPT:sprintf(tagtype_str,"SCRIPT");break;
            default:sprintf(tagtype_str,"UNKNOWN");break;
        }
        fprintf(myout,"[%6s] %6d %6d |",tagtype_str,tagheader_datasize,tagheader_timestamp);
        
        //if we are not past the end of file, process the tag
        if (feof(ifh)) {
            break;
        }
        
        //process tag by type
        switch (tagheader.TagType) {
                
            case TAG_TYPE_AUDIO:{  //音频
                char audiotag_str[100]={0};
                strcat(audiotag_str,"| ");
                char tagdata_first_byte;
                
                //读取一个字符,音频tag data区域的第一个字节,音频的信息
                tagdata_first_byte=fgetc(ifh);
                // &操作获取前四位,代表音频格式
                int x=tagdata_first_byte&0xF0;
                //右移4位
                x=x>>4;
                //判断音频格式
                switch (x)
                {
                    case 0:strcat(audiotag_str,"Linear PCM, platform endian");break;
                    case 1:strcat(audiotag_str,"ADPCM");break;
                    case 2:strcat(audiotag_str,"MP3");break;
                    case 3:strcat(audiotag_str,"Linear PCM, little endian");break;
                    case 4:strcat(audiotag_str,"Nellymoser 16-kHz mono");break;
                    case 5:strcat(audiotag_str,"Nellymoser 8-kHz mono");break;
                    case 6:strcat(audiotag_str,"Nellymoser");break;
                    case 7:strcat(audiotag_str,"G.711 A-law logarithmic PCM");break;
                    case 8:strcat(audiotag_str,"G.711 mu-law logarithmic PCM");break;
                    case 9:strcat(audiotag_str,"reserved");break;
                    case 10:strcat(audiotag_str,"AAC");break;
                    case 11:strcat(audiotag_str,"Speex");break;
                    case 14:strcat(audiotag_str,"MP3 8-Khz");break;
                    case 15:strcat(audiotag_str,"Device-specific sound");break;
                    default:strcat(audiotag_str,"UNKNOWN");break;
                }
                strcat(audiotag_str,"| ");
                
                //获取5~6位,采样率
                x=tagdata_first_byte&0x0C;
                //右移2位
                x=x>>2;
                //判断采样率
                switch (x)
                {
                    case 0:strcat(audiotag_str,"5.5-kHz");break;
                    case 1:strcat(audiotag_str,"1-kHz");break;
                    case 2:strcat(audiotag_str,"22-kHz");break;
                    case 3:strcat(audiotag_str,"44-kHz");break;
                    default:strcat(audiotag_str,"UNKNOWN");break;
                }
                strcat(audiotag_str,"| ");
                
                //获取第7位,采样精度
                x=tagdata_first_byte&0x02;
                x=x>>1;
                switch (x)
                {
                    case 0:strcat(audiotag_str,"8Bit");break;
                    case 1:strcat(audiotag_str,"16Bit");break;
                    default:strcat(audiotag_str,"UNKNOWN");break;
                }
                strcat(audiotag_str,"| ");
                
                //获取第8位,音频声道数
                x=tagdata_first_byte&0x01;
                switch (x)
                {
                    case 0:strcat(audiotag_str,"Mono");break;
                    case 1:strcat(audiotag_str,"Stereo");break;
                    default:strcat(audiotag_str,"UNKNOWN");break;
                }
                fprintf(myout,"%s",audiotag_str);
                
                //if the output file hasn't been opened, open it.
                if(output_a!=0&&afh == NULL){
                    afh = fopen("output.mp3", "wb");
                }
                
                //TagData - First Byte Data
                //获取tag Data字节数,需要减去Tag Data区域的第一个字节
                int data_size=reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize))-1;
                
                //循环获取字节写入文件
                if(output_a!=0){
                    //TagData+1
                    for (int i=0; i<data_size; i++)
                        fputc(fgetc(ifh),afh);
                    
                }else{
                    for (int i=0; i<data_size; i++)
                        fgetc(ifh);
                }
                break;
            }
            case TAG_TYPE_VIDEO:{   //视频
                char videotag_str[100]={0};
                strcat(videotag_str,"| ");
                //读取TagData区域第一个字节,取出前4位。包含视频帧类型
                char tagdata_first_byte;
                tagdata_first_byte=fgetc(ifh);
                int x=tagdata_first_byte&0xF0;
                x=x>>4;
                switch (x)
                {
                    case 1:strcat(videotag_str,"key frame  ");break;
                    case 2:strcat(videotag_str,"inter frame");break;
                    case 3:strcat(videotag_str,"disposable inter frame");break;
                    case 4:strcat(videotag_str,"generated keyframe");break;
                    case 5:strcat(videotag_str,"video info/command frame");break;
                    default:strcat(videotag_str,"UNKNOWN");break;
                }
                strcat(videotag_str,"| ");
                
                //读取TagData区域第一个字节,取出后4位。包含视频编码类型
                x=tagdata_first_byte&0x0F;
                switch (x)
                {
                    case 1:strcat(videotag_str,"JPEG (currently unused)");break;
                    case 2:strcat(videotag_str,"Sorenson H.263");break;
                    case 3:strcat(videotag_str,"Screen video");break;
                    case 4:strcat(videotag_str,"On2 VP6");break;
                    case 5:strcat(videotag_str,"On2 VP6 with alpha channel");break;
                    case 6:strcat(videotag_str,"Screen video version 2");break;
                    case 7:strcat(videotag_str,"AVC");break;
                    default:strcat(videotag_str,"UNKNOWN");break;
                }
                fprintf(myout,"%s",videotag_str);
                
                fseek(ifh, -1, SEEK_CUR);
                //if the output file hasn't been opened, open it.
                if (vfh == NULL&&output_v!=0) {
                    //write the flv header (reuse the original file's hdr) and first previoustagsize
                    vfh = fopen("output.flv", "wb");
                    fwrite((char *)&flv,1, sizeof(flv),vfh);
                    fwrite((char *)&previoustagsize_z,1,sizeof(previoustagsize_z),vfh);
                }
#if 0
                //Change Timestamp
                //Get Timestamp
                ts = reverse_bytes((byte *)&tagheader.Timestamp, sizeof(tagheader.Timestamp));
                ts=ts*2;
                //Writeback Timestamp
                ts_new = reverse_bytes((byte *)&ts, sizeof(ts));
                memcpy(&tagheader.Timestamp, ((char *)&ts_new) + 1, sizeof(tagheader.Timestamp));
#endif
                
                
                //TagData + Previous Tag Size
                int data_size=reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize))+4;
                if(output_v!=0){
                    //TagHeader
                    fwrite((char *)&tagheader,1, sizeof(tagheader),vfh);
                    //TagData
                    for (int i=0; i<data_size; i++)
                        fputc(fgetc(ifh),vfh);
                }else{
                    for (int i=0; i<data_size; i++)
                        fgetc(ifh);
                }
                //rewind 4 bytes, because we need to read the previoustagsize again for the loop's sake
                fseek(ifh, -4, SEEK_CUR);
                
                break;
            }
            default:
                
                //skip the data of this tag
                fseek(ifh, reverse_bytes((byte *)&tagheader.DataSize, sizeof(tagheader.DataSize)), SEEK_CUR);
        }
        
        fprintf(myout,"\n");
        
    } while (!feof(ifh));
    
//
    fclose(ifh);
    fclose(vfh);
    fclose(afh);
    
    return 0;
}

函数调用方法如下:

//解析FLV
 simplest_flv_parser("cuc_ieschool.flv");

执行结果如下:


解析结果

关于FLV格式介绍及解码暂时就说到这。如有错误,欢迎拍砖。因个别贴图取自网络,如有侵权,烦请下方留言。谢谢。

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

推荐阅读更多精彩内容