05_解封装和解码

1. 基本概念

容器就是一种文件格式,比如flv、mkv、mp4等。包含下面5种流以及文件头信息。
是一种视频数据信息的传输方式,5种流:音频,视频,字幕,附件,数据。
在ffmpeg中代表已经编码好的一个单位的音频或者视频。
在ffmpeg中帧代表一幅静止的图像(yuv数据)或一些数量的音频采样。
编解码器是对视频进行压缩或者解压缩,CODEC =ENCode (编码) +DECode(解码)
复用/解复用(mux/demux)

  • 把不同的流按照某种容器的规则放入容器,这种行为叫做复用(mux)

  • 把不同的流从某种容器中解析出来,这种行为叫做解复用(demux)

下面ffmpeg将h264+aac编码的flv文件转码为 h265+mp3编码的mp4文件的处理流程:

flv文件
解复用器
音频包aac
视频包h264
aac音频解码器
原始音频采样pcm
mp3音频编码器
音频包mp3
h264视频解码器
原始视频数据yuv
h265视频编码器
视频包h265
mp4复用器
mp4文件

2. 解封装操作

2.1 封装相关API

◼ AVFormatContext* avformat_alloc_context();负责申请一个AVFormatContext结构的内存,并进行简单初始化
◼ avformat_free_context();释放该结构里的所有东西以及该结构本身
◼ avformat_close_input();关闭解复用器。关闭后就不再需要使用avformat_free_context 进行释放。
◼ avformat_open_input();打开输入视频文件
◼ avformat_find_stream_info():获取视频文件信息
◼ av_read_frame(); 读取音视频包
◼ avformat_seek_file(); 定位文件
◼ av_seek_frame():定位文件

2.2 解封装流程

  1. 创建avformat上下文(可选)

    AVFormatContext* avformat_alloc_context();
    

    因为调用avformat_open_input函数时会创建, 所以不是必须调用,结束时必须使用avformat_free_context()销毁AVFormatContext指针

  2. 打开输入文件

    // ps	指向AVFormatContext的指针,可以用avformat_alloc_context()提前申请,当然也可直接用  
    //      avformat_open_input函数生成,不论哪种方式,都必须使用avformat_free_context()销毁该指针
    //url	要打开的媒体流地址(或文件路径)
    //fmt	输入类型,如果此参数不为空,则强制设置输入媒体的类型(如flv、mp4等)
    //options 可选的选项,此处参考ffmpeg命令行操作里的一些输入参数,如reorder_queue_size、
    //        stimeout、scan_all_pmts 等等。使用av_dict_set()函数设置,使用av_dict_free()释放。
    int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);
    

    该函数作用是打开一个输入流(或者文件)并且读取媒体头信息(如音视频编码类型等等)。
    任务结束时使用函数 avformat_close_input()关闭。

  3. 获取码流信息

    区分不同的码流

    ◼ AVMEDIA_TYPE_VIDEO视频流
    video_index = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,-1,-1, NULL, 0)
    ◼ AVMEDIA_TYPE_AUDIO音频流
    audio_index = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,-1,-1, NULL, 0)
    AVPacket 里面也有一个index的字段
    

    由于需要读取数据包,avformat_find_stream_info接口会带来很大的延迟。

    //读取媒体文件的数据包以获取流信息。 这个对于没有header的文件格式(例如MPEG)很有用。
    // 总之使用该函数可以将输入流(文件)中的媒体信息(包括编码信息)解析出来。
    // 通过ic->streams[i]访问,streams的个数由ic->nb_streams获取。
    int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
    
  4. 读取音视频包

    //将输入文件或输入URL的流内容读取到AVPacket中
    int av_read_frame(AVFormatContext *s, AVPacket *pkt);
    

2.3 解复用代码示例

代码地址,打印信息如下
在这里插入图片描述

3. AAC ADTS格式分析

AAC⾳频格式:Advanced Audio Coding(⾼级⾳频解码),是⼀种由MPEG-4标准定义的有损⾳频压缩格式,由Fraunhofer发展,Dolby, Sony和AT&T是主要的贡献者。

  • ADIF:Audio Data Interchange Format ⾳频数据交换格式。这种格式的特征是可以确定的找到这个⾳频数据的开始,不需进⾏在⾳频数据流中间开始的解码,即它的解码必须在明确定义的开始处进⾏。故这种格式常⽤在磁盘⽂件中。

  • ADTS的全称是Audio Data Transport Stream。是AAC⾳频的传输流格式。AAC⾳频格式在MPEG-2(ISO-13318-7 2003)中有定义。AAC后来⼜被采⽤到MPEG-4标准中。这种格式的特征是它是⼀个有同步字的⽐特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。

简单说,ADTS可以在任意帧解码,也就是说它每⼀帧都有头信息。ADIF只有⼀个统⼀的头,所以必须得到所有的数据后解码。

且这两种的header的格式也是不同的,⽬前⼀般编码后的和抽取出的都是ADTS格式的⾳频流。两者具体的组织结构如下所示:

有的时候当你编码AAC裸流的时候,会遇到写出来的AAC⽂件并不能在PC和⼿机上播放,很⼤的可能就是AAC⽂件的每⼀帧⾥缺少了ADTS头信息⽂件的包装拼接, 比如MP4和FLV中的aac音频。解决方法:只需要加⼊头⽂件ADTS即可。⼀个AAC原始数据块⻓度是可变的,对原始帧加 上ADTS头进⾏ADTS的封装,就形成了ADTS帧。

AAC音频文件中的每一帧是由ADTS Header 和 AAC Audio Data组成,结构图如下:

每⼀帧的ADTS的头⽂件都包含了⾳频的采样率,声道,帧⻓度等信息,这样解码器才能解析读取。
⼀般情况下ADTS的头信息都是7个字节,分为2部分:

  • adts_fixed_header();
  • adts_variable_header();

adts_fixed_header为固定头信息,adts_variable_header是可变头信息。固定头信息中的数据每⼀帧都相同,⽽可变头信息则在帧与帧之间可变。

3.1 adts_fixed_header

  • syncword:同步头 总是0xFFF, all bits must be 1,代表着⼀个ADTS帧的开始

  • ID:MPEG标识符,0标识MPEG-4,1标识MPEG-2

  • Layer:always: ‘00’

  • protection_absent:表示是否误码校验。Warning, set to 1 if there is no CRC and 0 if there is CRC

  • profile:表示使⽤哪个级别的AAC,如01 Low Complexity(LC)— AAC LC。有些芯⽚只⽀持AAC LC;MPEG-2 AAC中定义了3种:

并且profile的值等于 Audio Object Type的值减1 profile = MPEG-4 Audio Object Type - 1
在这里插入图片描述
在ffmpeg源码中我们可以找到AAC级别被设成的值,就不用上面的那个公式来计算了:

   /*** profile* - encoding: Set by user.* - decoding: Set by libavcodec.*/int profile;#define FF_PROFILE_UNKNOWN -99#define FF_PROFILE_RESERVED -100#define FF_PROFILE_AAC_MAIN 0#define FF_PROFILE_AAC_LOW  1#define FF_PROFILE_AAC_SSR  2#define FF_PROFILE_AAC_LTP  3#define FF_PROFILE_AAC_HE   4#define FF_PROFILE_AAC_HE_V2 28#define FF_PROFILE_AAC_LD   22#define FF_PROFILE_AAC_ELD  38#define FF_PROFILE_MPEG2_AAC_LOW 128#define FF_PROFILE_MPEG2_AAC_HE  131
  • sampling_frequency_index:表示使⽤的采样率下标,通过这个下标在 Sampling Frequencies[ ]数组中查找得知采样率的值。

  • channel_configuration: 表示声道数,⽐如2表示⽴体声双声道

3.2 adts_variable_header

在这里插入图片描述

  • frame_length : ⼀个ADTS帧的⻓度包括ADTS头和AAC原始流. frame length, this value must include 7 or 9 bytes of header length: aac_frame_length = (protection_absent == 1 ? 7 : 9) + size(AACFrame)

    protection_absent=0时, header length=9bytes
    protection_absent=1时, header length=7bytes

  • adts_buffer_fullness:0x7FF 说明是码率可变的码流。number_of_raw_data_blocks_in_frame:表示ADTS帧中有 number_of_raw_data_blocks_in_frame + 1个AAC原始帧。所以说number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有⼀个 AAC数据块

帧长度计算

 unsigned int getFrameLength(unsigned char* str){if ( !str ){return 0;}unsigned int len = 0;int f_bit = str[3];int m_bit = str[4];int b_bit = str[5];len += (b_bit>>5);len += (m_bit<<3);len += ((f_bit&3)<<11);return len;
}

3.3 练习:提取MP4中的aac音频数据

代码可播放生成的aac文件

在这里插入图片描述

4. H264 NALU分析

⾳视频编码在流媒体和⽹络领域占有重要地位;流媒体编解码流程⼤致如下图所示:

4.1 h264编码原理

编码是为了将数据进行压缩,这样在传输的过程中就不会使资源被浪费,用一个简单的例子来说明编码的必要性:

⼀段分辨率为19201080,每个像素点为RGB占⽤3个字节,帧率是25的视频,对于传输带宽的要求是:19201080325/1024/1024=148.315MB/s,换成bps则意味着视频每秒带宽为1186.523Mbps,这样的速率对于⽹络存储是不可接受的。因此视频压缩和编码技术应运⽽⽣。

对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在相似性,因此视频帧图像可以进⾏图像压缩(内部压缩 | 空间压缩);H264采⽤了16*16的分块⼤⼩对,视频帧图像进⾏相似⽐较和压缩编码。如下图所示:

同理两帧图像之间也具有相似相似性,多个连续的图像帧也可以只记录差异,称为时间压缩|帧间压缩

4.2 帧分类(IPB)

  H264结构中,一个视频图像编码后的数据叫做一帧,一帧由一个片(slice)或多个片组成,一个片由一个或多个宏块(MB)组成,一个宏块由16x16的yuv数据组成。宏块作为H264编码的基本单位。
  在H264协议内定义了三种帧,分别是I帧、B帧与P帧。I帧就是之前所说的一个完整的图像帧,而B、帧与P帧所对应的就是之前说的不编码全部图像的帧。P帧与B帧的差别就是P帧是参考之前的I帧 或 P帧而生成的,而B帧是参考前后图像帧(前一个I或P,以及后一个P)编码生成的。
  压缩率 B > P > I

4.3 h264编码结构分析

  H264除了实现了对视频的压缩处理之外,为了⽅便⽹络传输,提供了对应的视频编码和分⽚策略;类似于⽹络数据封装成I帧,在H264中将其称为组(GOP, group of pictures)、⽚(slice)、宏块(
Macroblock)这些⼀起组成了H264的码流分层结构;H264将其组织成为序列(GOP)、图⽚(pictrue)、⽚(Slice)、宏块(Macroblock)、⼦块(subblock)五个层次。
  GOP (图像组)主要⽤作形容⼀个IDR帧 到下⼀个IDR帧之间的间隔了多少个帧。

  H264将视频分为连续的帧进⾏传输,在连续的帧之间使⽤I帧、P帧和B帧。同时对于帧内⽽⾔,将图像分块为⽚、宏块和字块进⾏分⽚传输;通过这个过程实现对视频⽂件的压缩包装。

IDRInstantaneous Decoding Refresh,即时解码刷新)

在编码解码中为了方便,将GOP中首个I帧要和其他I帧区别开,把第一个I帧叫IDR,这样方便控制编码和解码流程,所以IDR帧一定是I帧,但I帧不一定是IDR帧;IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始算新的序列开始编码。I帧有被跨帧参考的可能,IDR不会。

I帧不用参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样,例如:

  • IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15 这里的B8可以跨过I10去参考P7
    原始图像:IDR1 B2 B3 P4 B5 B6 P7 B8 B9 I10
  • IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12 这里的B9就只能参照IDR8和P11,不可以参考IDR8前面的帧

作用:
H.264引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。

由于B帧需要一直等下一个P帧,所以会有延迟,所以直播一般不会插入B帧。

4.4 h264分层结构

H264的主要目标是为了有高的视频压缩比和良好的网络亲和性,为了达成这两个目标,H264的解决方案是将系统框架分为两个层面,分别是视频编码层面(VCL)和网络抽象层面(NAL),如图
在这里插入图片描述
  VLC层是对核心算法引擎、块、宏块及片的语法级别的定义,负责有效表示视频数据的内容,最终输出编码完的数据SODB;
  NAL层定义了片级以上的语法级别(如序列参数集参数集和图像参数集,针对网络传输,后面会描述到),负责以网络所要求的恰当方式去格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。NAL层将SODB打包成RBSP然后加上NAL头组成一个NALU单元,具体NAL单元的组成也会在后面详细描述。

这里说一下SODB与RBSP的关联

SODB: 数据比特串,是编码后的原始数据;
RBSP: 原始字节序列载荷,是在原始编码数据后面添加了结尾比特,一个bit“1”和若干个比特“0”,用于字节对齐。

4.5 NALU 结构

在经过编码后的H264的码流如图所示, 从图中我们需要得到一个概念,H264码流是由一个个的NAL单元组成,其中SPS、PPS、IDR和SLICE是NAL单元某一类型的数据。
在这里插入图片描述
具体分析
在这里插入图片描述
SPS:序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。
PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。

发I帧之前,⾄少要发⼀次SPS和PPS。

H.264原始码流(裸流)是由⼀个接⼀个NALU组成,它的功能分为两层,VCL(视频编码层)和NAL(⽹络提取层):

  • VCL:包括核⼼压缩引擎和块,宏块和⽚的语法级别定义,设计⽬标是尽可能地独⽴于⽹络进⾏⾼效的编码;
  • NAL:负责将VCL产⽣的⽐特字符串适配到各种各样的⽹络和多元环境中,覆盖了所有⽚级以上的语法级别

在VCL进⾏数据传输或存储之前,这些编码的VCL数据,被映射或封装进NAL单元。

⼀个NALU = ⼀组对应于视频编码的NALU头部信息 + ⼀个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)

NALU结构单元的主体结构如下所示;⼀个原始的H.264 NALU单元通常由[StartCode] [NALU Header] [NALU Payload]三部分组成,其中 Start Code ⽤于标示这是⼀个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01" ,除此之外基本相当于⼀个NAL header + RBSP;
在这里插入图片描述
(对于FFmpeg解复⽤后,MP4⽂件读取出来的packet是不带startcode,但TS⽂件读取出来的packet带startcode)

每个NAL单元是⼀个⼀定语法元素的可变⻓字节字符串,包括包含⼀个字节的头信息(⽤来表示数据类型),以及若⼲整数字节的负荷数据。
在这里插入图片描述

  1. F(forbiden):禁止位,占用NAL头的第一个位,当禁止位值为1时表示语法错误;
  2. NRI:参考级别,占用NAL头的第二到第三个位;值越大,该NAL越重要。
  3. Type:Nal单元数据类型,也就是标识该NAL单元的数据类型是哪种,占用NAL头的第四到第8个位;
    (1~12由H.264使⽤,24~31由H.264以外的应⽤使⽤)

H.264标准指出,当数据流是储存在介质上时,在每个NALU 前添加起始码:0x000001 或0x00000001,⽤来指示⼀个NALU 的起始和终⽌位置:

  • 在这样的机制下,在码流中检测起始码,作为⼀个NALU得起始标识,当检测到下⼀个起始码时,当前NALU结束。
  • 3字节的0x000001只有⼀种场合下使⽤,就是⼀个完整的帧被编为多个slice(⽚)的时候,包含这些slice的NALU 使⽤3字节起始码。其余场合都是4字节0x00000001的。

例⼦:
0x00 00 00 01 67 …
0x00 00 00 01 68 …
0x00 00 00 01 65 …

67(NALU头):⼆进制:0110 0111 00111 = 7(⼗进制)

具体种类如下:

4.6 annexb模式

H264有两种封装

  • ⼀种是annexb模式,传统模式,有startcode,SPS和PPS是在ES中
  • ⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度

很多解码器只⽀持annexb这种模式,因此需要将mp4做转换:在ffmpeg中⽤h264_mp4toannexb_filter可以做转换(没有start_code 裸流无法播放)
实现:

const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;
// 2 初始化过滤器上下⽂
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
av_bsf_init(bsf_ctx);

补充:ts流可以不加start_code,与封装格式有关

具体测试代码见连接h264

5. FLV格式解析

  flv即flash video,是Adobe公司推出的一种音视频封装格式,这家公司在音视频、图像图形领域也算是神一般的存在,多少都用过他们家的产品。常见的Photoshop和Flash palyer就是他们家的。今天要讲到的flv也是他们推出来的,也是Flash palyer播放的标准音视频格式。在HTML5出来之前,想在web上播放音视频,基本都靠flash插件。
  FLV封装格式是由⼀个⽂件头(file 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解析流程如下:

5.1 FLV Header

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

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

5.2 FLV_Body

FLV Header之后,就是FLV File Body。FLV File Body是由⼀连串的back-pointers + tags构成。Back-pointer表示Previous Tag Size(前⼀个tag的字节数据⻓度),占4个字节。00 00 01 7F计算出⼤⼩ 383= 0x0000017F

5.3 FLV_Tag

每⼀个Tag也是由两部分组成:tag header和tag data。Tag Header⾥存放的是当前tag的类型、数据区(tag data)的⻓度等信息。

  • Tag Header
    tag header⼀般占11个字节的内存空间。FLV tag结构如下:
FieldTypeComment
Tag类型 TypeUI88:audio 9:video 18:Script data(脚本数据) all Others:reserved 其他所有值未使用
数据区大小UI24当前tag的数据域的大小,不包含tag header。
Length of the data in the Data field
时间戳 TimestampUI24当前帧时戳,单位是毫秒。相对值,第一个tag的时戳总是为0
时戳扩展字段 TimestampExtendedUI8如果时戳大于0xFFFFFF,将会使用这个字节。这个字节是时戳的高8位,上面的三个字节是低24位。
StreamIDUI24总是为0
数据域UI[8*n]数据域数据

注意:

  1. flv⽂件中Timestamp和TimestampExtended拼出来的是dts。也就是解码时间。Timestamp和TimestampExtended拼出来dts单位为ms。(如果不存在B帧,当然dts等于pts)
  2. CompositionTime 表示PTS相对于DTS的偏移值, 在每个视频tag的第14~16字节,显示时间(pts) = 解码时间(tag的第5~8字节) + CompositionTime CompositionTime的单位也是ms
  • Script Tag Data结构(脚本类型、帧类型)
    该类型Tag⼜被称为MetaData Tag,存放⼀些关于FLV视频和⾳频的元信息,⽐如:duration、width、height等。通常该类型Tag会作为FLV⽂件的第⼀个tag,并且只有⼀个,跟在File Header后。该类型TagDaTa的结构如下所示(source.200kbps.768x320.flv⽂件为例):

    • 第⼀个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时长(秒)210.732
    width视频宽度768.000
    height视频高度320.000
    videodatarate视频码率207.260
    framerate视频帧率25.000
    videocodecid视频编码ID7.000 (H264为7)
    audiodatarate音频码率29.329
    audiosamplerate音频采样率44100.000
    stereo是否立体声1
    audiocodecid音频编码ID10.000 (AAC为10)
    major_brand格式规范相关isom
    minor_version格式规范相关512
    compatible_brands格式规范相关isomiso2avc1mp41
    encoder封装工具名称Lavf54.63.104
    filesize文件大小(字节)6636853.000

5.4. Audio Tag Data结构(⾳频类型)

⾳频Tag Data区域开始的:

  • 第⼀个字节包含了⾳频数据的参数信息,
  • 第⼆个字节开始为⾳频流数据。

(这两个字节属于tag的data部分,不是header部分)

第⼀个字节为⾳频的信息(仔细看spec发现对于AAC⽽⾔,⽐较有⽤的字段是
SoundFormat),格式如下:

FieldTypeComment
音频格式 SoundFormatUB40 = 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
采样率 SoundRateUB20 = 5.5kHz
1 = 11kHz
2 = 22.05kHz
3 = 44.1kHz
对于AAC总是3。但实际上AAC是可以支持到48kHz以上的频率(这个参数对于AAC意义不大)。
采样精度 SoundSizeUB10 = snd8Bit
1 = snd16Bit
此参数仅适用于未压缩的格式,压缩后的格式都将其设为1
音频声道 SoundTypeUB10 = sndMono 单声道
1 = sndStereo 立体声,双声道
对于AAC总是1

如果 SoundFormat 表示 AAC,SoundType 应设置为 1(立体声),SoundRate 应设置为 3(44 kHz)。然而,这并不意味着 FLV 中的 AAC 音频总是立体声、44 kHz 数据。相反,Flash Player 会忽略这些值,而是从 AAC 位流中提取声道和采样率数据。

第⼆个字节开始为⾳频数据(需要判断该数据是真正的⾳频数据,还是⾳频config信息)

FieldTypeComment
音频数据UI[8*n]if SoundFormat == 10 (AAC类型) AACAUDIODATA
else Sound data—varies by format

AACAUDIODATA

FieldTypeComment
AACPacketTypeUI80: AAC sequence header
1: AAC raw
DataUI8[n]if AACPacketType == 0
    AudioSpecificConfig
else if AACPacketType == 1
    Raw AAC frame data

The AudioSpecificConfig is explained in ISO 14496-3. AAC sequence header存放的是AudioSpecificConfig结构,该结构则在“ ISO-14496-3 Audio”中描述。
如果是AAC数据,如果他是AAC RAW, tag data[3] 开始才是真正的AAC frame data。

5.5. Video Tag Data结构(视频类型)

视频Tag Data开始的:

  • 第⼀个字节包含视频数据的参数信息,
  • 第⼆个字节开始为视频流数据。

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

FieldTypeComment
帧类型UB41: 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
编码IDUB4使用哪种编码类型:
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

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

FieldTypeComment
视频数据UI[8*n]If CodecID == 2
    H263VIDEOPACKET
If CodecID == 3
    SCREENVIDEOPACKET
If CodecID == 4
    VP6FLVIDEOPACKET
If CodecID == 5
    VP6FLALPHAVIDEOPACKET
If CodecID == 6
    SCREENV2VIDEOPACKET
If CodecID == 7 (AVC格式)
    AVCVIDEOPACKET

(1)CompositionTime 单位毫秒
CompositionTime 每个视频tag(整个tag)的第14~16字节(如果是tag data偏移[3]~[5], [0],[1][2:AVCPackettype] )(表示PTS相对于DTS的偏移值 )。
CompositionTime 单位为ms : 显示时间 = 解码时间(tag的第5~ 8字节,位置索引[4]~[7])+CompositionTime

(2)AVCDecoderConfigurationRecord
AVC sequence header就是AVCDecoderConfigurationRecord结构.

5.6 FlvParser

5.6.1 main函数

流程:
1、读取输入文件(flv类型的视频文件)
2、调用Process进行处理
3、退出

int main(int argc, char* argv[])
{cout << "Hi, this is FLV parser test program!\n";if (argc != 3){cout << "FlvParser.exe [input flv] [output flv]" << endl;return 0;}fstream fin;fin.open(argv[1], ios_base::in | ios_base::binary);if (!fin){return 0;}Process(fin, argv[2]);fin.close();return 1;
}

5.6.2 处理函数Process

1、读取文件
2、开始解析
3、打印解析信息
4、把解析之后的数据输出到另外一个文件中
void Process(fstream &fin, const char *filename)
{CFlvParser parser;int nBufSize = 2000 * 1024;int nFlvPos = 0;unsigned char *pBuf, *pBak;pBuf = new unsigned char[nBufSize];pBak = new unsigned char[nBufSize];while (1){int nReadNum = 0;int nUsedLen = 0;fin.read((char *)pBuf + nFlvPos, nBufSize - nFlvPos);nReadNum = fin.gcount();if (nReadNum == 0)break;nFlvPos += nReadNum;parser.Parse(pBuf, nFlvPos, nUsedLen);if (nFlvPos != nUsedLen){memcpy(pBak, pBuf + nUsedLen, nFlvPos - nUsedLen);memcpy(pBuf, pBak, nFlvPos - nUsedLen);}nFlvPos -= nUsedLen;}parser.PrintInfo();/*parser.DumpH264("parser.264");parser.DumpAAC("parser.aac");*///dump into flvparser.DumpFlv(filename);delete []pBak;delete []pBuf;
}

5.6.3 解析函数

1、解析flv的头部
2、解析flv的Tag
int CFlvParser::Parse(unsigned char *pBuf, int nBufSize, int &nUsedLen)
{int nOffset = 0;if (_pFlvHeader == 0){CheckBuffer(9);_pFlvHeader = CreateFlvHeader(pBuf+nOffset);nOffset += _pFlvHeader->nHeadSize;}while (1){CheckBuffer(15);int nPrevSize = ShowU32(pBuf + nOffset); //nPrevSize(4字节) + Tag header(11字节)nOffset += 4;Tag *pTag = CreateTag(pBuf + nOffset, nBufSize-nOffset);if (pTag == NULL){nOffset -= 4;break;}nOffset += (11 + pTag->_header.nDataSize);_vpTag.push_back(pTag);}nUsedLen = nOffset;return 0;
}

5.6.4 FLV相关的数据结构

CFlvParser表示FLV解析器

FLV由FLV头部和FLV体构成,其中FLV体是由一系列的FLV tag构成的

class CFlvParser
{
public:CFlvParser();virtual ~CFlvParser();int Parse(unsigned char *pBuf, int nBufSize, int &nUsedLen);int PrintInfo();int DumpH264(const std::string &path);int DumpAAC(const std::string &path);int DumpFlv(const std::string &path);private:// flv的头typedef struct FlvHeader_s FlvHeader;// Tag头部struct TagHeader;// flv的tag(普通的script Tag)class Tag;// 视频类型Tagclass CVideoTag : public Tag;// 音频类型Tagclass CAudioTag : public Tag;// FLV的状态信息struct FlvStat;static unsigned int ShowU32(unsigned char *pBuf) { return (pBuf[0] << 24) | (pBuf[1] << 16) | (pBuf[2] << 8) | pBuf[3]; }static unsigned int ShowU24(unsigned char *pBuf) { return (pBuf[0] << 16) | (pBuf[1] << 8) | (pBuf[2]); }static unsigned int ShowU16(unsigned char *pBuf) { return (pBuf[0] << 8) | (pBuf[1]); }static unsigned int ShowU8(unsigned char *pBuf) { return (pBuf[0]); }static void WriteU64(uint64_t & x, int length, int value);static unsigned int WriteU32(unsigned int n);friend class Tag;private:FlvHeader *CreateFlvHeader(unsigned char *pBuf);int DestroyFlvHeader(FlvHeader *pHeader);Tag *CreateTag(unsigned char *pBuf, int nLeftLen);int DestroyTag(Tag *pTag);int Stat();int StatVideo(Tag *pTag);int IsUserDataTag(Tag *pTag);private:FlvHeader* _pFlvHeader;vector<Tag *> _vpTag;FlvStat _sStat;CVideojj *_vjj;// H.264int _nNalUnitLength;
};
FlvHeader表示FLV的头部
	// flv的头typedef struct FlvHeader_s{int nVersion; // 版本int bHaveVideo, bHaveAudio; // 是否包含音视频int nHeadSize; // FLV头部长度/*** 指向存放FLV头部的buffer** 上面的三个成员指明了FLV头部的信息,是从FLV的头部中“翻译”得到的,** 真实的FLV头部是一个二进制比特串,放在一个buffer中,由pFlvHeader成员指明*/unsigned char *pFlvHeader; } FlvHeader;
标签

标签包括标签头部和标签体,根据类型的不同,标签体可以分成三种:
script类型的标签,⾳频标签、视频标签

  • 标签头部
	// Tag头部struct TagHeader{int nType; // 类型int nDataSize; // 标签body的大小int nTimeStamp; // 时间戳int nTSEx; // 时间戳的扩展字节int nStreamID; // 流的ID,总是0unsigned int nTotalTS;TagHeader() : nType(0), nDataSize(0), nTimeStamp(0), nTSEx(0), nStreamID(0), nTotalTS(0) {}~TagHeader() {}};
  • 标签数据
  1. script类型的标签
	// flv的tagclass Tag{public:Tag() : _pTagHeader(NULL), _pTagData(NULL), _pMedia(NULL), _nMediaLen(0) {}void Init(TagHeader *pHeader, unsigned char *pBuf, int nLeftLen);TagHeader _header;unsigned char *_pTagHeader; // 指向标签头部unsigned char *_pTagData; // 指向标签bodyunsigned char *_pMedia; // 指向标签的元数据int _nMediaLen;};
  1. 音频标签
	class CAudioTag : public Tag{public:CAudioTag(TagHeader *pHeader, unsigned char *pBuf, int nLeftLen, CFlvParser *pParser);int _nSoundFormat; // 音频编码类型int _nSoundRate; // 采样率int _nSoundSize; // 精度int _nSoundType; // 类型// aacstatic int _aacProfile;static int _sampleRateIndex;static int _channelConfig;int ParseAACTag(CFlvParser *pParser);int ParseAudioSpecificConfig(CFlvParser *pParser, unsigned char *pTagData);int ParseRawAAC(CFlvParser *pParser, unsigned char *pTagData);};
  1. 视频标签
	class CVideoTag : public Tag{public:CVideoTag(TagHeader *pHeader, unsigned char *pBuf, int nLeftLen, CFlvParser *pParser);int _nFrameType; // 帧类型int _nCodecID; // 视频编解码类型int ParseH264Tag(CFlvParser *pParser);int ParseH264Configuration(CFlvParser *pParser, unsigned char *pTagData);int ParseNalu(CFlvParser *pParser, unsigned char *pTagData);};

5.6.5 解析FLV头部

int CFlvParser::Parse(unsigned char *pBuf, int nBufSize, int &nUsedLen)
{int nOffset = 0;if (_pFlvHeader == 0){CheckBuffer(9);// 解析FLV头部_pFlvHeader = CreateFlvHeader(pBuf+nOffset);nOffset += _pFlvHeader->nHeadSize;}while (1){CheckBuffer(15);int nPrevSize = ShowU32(pBuf + nOffset);nOffset += 4;Tag *pTag = CreateTag(pBuf + nOffset, nBufSize-nOffset);if (pTag == NULL){nOffset -= 4;break;}nOffset += (11 + pTag->_header.nDataSize);_vpTag.push_back(pTag);}nUsedLen = nOffset;return 0;
}
CFlvParser::FlvHeader *CFlvParser::CreateFlvHeader(unsigned char *pBuf)
{FlvHeader *pHeader = new FlvHeader;pHeader->nVersion = pBuf[3]; // 版本号pHeader->bHaveAudio = (pBuf[4] >> 2) & 0x01; // 是否有音频pHeader->bHaveVideo = (pBuf[4] >> 0) & 0x01; // 是否有视频pHeader->nHeadSize = ShowU32(pBuf + 5); // 头部长度pHeader->pFlvHeader = new unsigned char[pHeader->nHeadSize];memcpy(pHeader->pFlvHeader, pBuf, pHeader->nHeadSize);return pHeader;
}

5.6.6 解析Tag头部

1、CFlvParser::Parse调用CreateTag解析标签2、CFlvParser::CreateTag首先解析标签头部3、根据标签头部的类型字段,判断标签的类型4、如果是视频标签,那么解析视频标签5、如果是音频标签,那么解析音频标签6、如果是其他的标签,那么调用Tag::Init进行解析
解析标签头部的函数
CFlvParser::Tag *CFlvParser::CreateTag(unsigned char *pBuf, int nLeftLen)
{// 开始解析标签头部TagHeader header;header.nType = ShowU8(pBuf+0); // 类型header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段header.nStreamID = ShowU24(pBuf + 8); // 流的idheader.nTotalTS = (unsigned int)((header.nTSEx << 24)) + header.nTimeStamp;// 标签头部解析结束cout << "total TS : " << header.nTotalTS << endl;//cout << "nLeftLen : " << nLeftLen << " , nDataSize : " << pTag->header.nDataSize << endl;if ((header.nDataSize + 11) > nLeftLen){return NULL;}Tag *pTag;switch (header.nType) {case 0x09: // 视频类型的TagpTag = new CVideoTag(&header, pBuf, nLeftLen, this);break;case 0x08: // 音频类型的TagpTag = new CAudioTag(&header, pBuf, nLeftLen, this);break;default: // script类型的TagpTag = new Tag();pTag->Init(&header, pBuf, nLeftLen);}return pTag;
}
解析视频标签
  1. 入口函数CreateTag

    1、解析标签头部2、判断标签头部的类型3、根据标签头部的类型,解析不同的标签4、如果是视频类型的标签,那么就创建并解析视频标签
    
CFlvParser::Tag *CFlvParser::CreateTag(unsigned char *pBuf, int nLeftLen)
{// 开始解析标签头部TagHeader header;header.nType = ShowU8(pBuf+0); // 类型header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段header.nStreamID = ShowU24(pBuf + 8); // 流的idheader.nTotalTS = (unsigned int)((header.nTSEx << 24)) + header.nTimeStamp;// 标签头部解析结束cout << "total TS : " << header.nTotalTS << endl;//cout << "nLeftLen : " << nLeftLen << " , nDataSize : " << pTag->header.nDataSize << endl;if ((header.nDataSize + 11) > nLeftLen){return NULL;}Tag *pTag;switch (header.nType) {case 0x09: // 视频类型的TagpTag = new CVideoTag(&header, pBuf, nLeftLen, this);break;case 0x08: // 音频类型的TagpTag = new CAudioTag(&header, pBuf, nLeftLen, this);break;default: // script类型的TagpTag = new Tag();pTag->Init(&header, pBuf, nLeftLen);}return pTag;
}
  1. 创建视频标签

     1、初始化2、解析帧类型3、解析视频编码类型4、解析视频标签
    
CFlvParser::CVideoTag::CVideoTag(TagHeader *pHeader, unsigned char *pBuf, int nLeftLen, CFlvParser *pParser)
{// 初始化Init(pHeader, pBuf, nLeftLen);unsigned char *pd = _pTagData;_nFrameType = (pd[0] & 0xf0) >> 4; // 帧类型_nCodecID = pd[0] & 0x0f; // 视频编码类型// 开始解析if (_header.nType == 0x09 && _nCodecID == 7){ParseH264Tag(pParser);}
}
  1. 解析视频标签

    1、解析数据包类型

    2、如果数据包是配置信息,那么就解析配置信息

    3、如果数据包是视频数据,那么就解析视频数据

int CFlvParser::CVideoTag::ParseH264Tag(CFlvParser *pParser)
{unsigned char *pd = _pTagData;/* ** 数据包的类型** 视频数据被压缩之后被打包成数据包在网上传输** 有两种类型的数据包:视频信息包(sps、pps等)和视频数据包(视频的压缩数据)*/int nAVCPacketType = pd[1];int nCompositionTime = CFlvParser::ShowU24(pd + 2);// 如果是视频配置信息if (nAVCPacketType == 0){ParseH264Configuration(pParser, pd);}// 如果是视频数据else if (nAVCPacketType == 1){ParseNalu(pParser, pd);}else{}return 1;
}
  1. 解析视频配置信息
    1、解析配置信息的长度

    2、解析sps、pps的长度

    3、保存元数据,元数据即sps、pps等

int CFlvParser::CVideoTag::ParseH264Configuration(CFlvParser *pParser, unsigned char *pTagData)
{unsigned char *pd = pTagData;// 配置信息长度pParser->_nNalUnitLength = (pd[9] & 0x03) + 1;int sps_size, pps_size;// sps(序列参数集)的长度sps_size = CFlvParser::ShowU16(pd + 11);// pps(图像参数集)的长度pps_size = CFlvParser::ShowU16(pd + 11 + (2 + sps_size) + 1);// 元数据的长度_nMediaLen = 4 + sps_size + 4 + pps_size;_pMedia = new unsigned char[_nMediaLen];// 保存元数据memcpy(_pMedia, &nH264StartCode, 4);memcpy(_pMedia + 4, pd + 11 + 2, sps_size);memcpy(_pMedia + 4 + sps_size, &nH264StartCode, 4);memcpy(_pMedia + 4 + sps_size + 4, pd + 11 + 2 + sps_size + 2 + 1, pps_size);return 1;
}
  1. 解析视频数据
    1、如果一个Tag还没解析完成,那么执行下面步骤

    2、计算NALU的长度

    3、获取NALU的起始码

    4、保存NALU的数据

    5、调用自定义的处理函数对NALU数据进行处理

int CFlvParser::CVideoTag::ParseNalu(CFlvParser *pParser, unsigned char *pTagData)
{unsigned char *pd = pTagData;int nOffset = 0;_pMedia = new unsigned char[_header.nDataSize+10];_nMediaLen = 0;nOffset = 5;while (1){// 如果解析玩了一个Tag,那么就跳出循环if (nOffset >= _header.nDataSize)break;// 计算NALU(视频数据被包装成NALU在网上传输)的长度int nNaluLen;switch (pParser->_nNalUnitLength){case 4:nNaluLen = CFlvParser::ShowU32(pd + nOffset);break;case 3:nNaluLen = CFlvParser::ShowU24(pd + nOffset);break;case 2:nNaluLen = CFlvParser::ShowU16(pd + nOffset);break;default:nNaluLen = CFlvParser::ShowU8(pd + nOffset);}// 获取NALU的起始码memcpy(_pMedia + _nMediaLen, &nH264StartCode, 4);// 复制NALU的数据memcpy(_pMedia + _nMediaLen + 4, pd + nOffset + pParser->_nNalUnitLength, nNaluLen);// 解析NALUpParser->_vjj->Process(_pMedia+_nMediaLen, 4+nNaluLen, _header.nTotalTS);_nMediaLen += (4 + nNaluLen);nOffset += (pParser->_nNalUnitLength + nNaluLen);}return 1;
}
  1. 自定义的视频处理
    把视频的NALU解析出来之后,可以根据自己的需要往视频中添加内容
// 用户可以根据自己的需要,对该函数进行修改或者扩展
// 下面这个函数的功能大致就是往视频中写入SEI信息
int CVideojj::Process(unsigned char *pNalu, int nNaluLen, int nTimeStamp)
{// 如果起始码后面的两个字节是0x05或者0x06,那么表示IDR图像或者SEI信息if (pNalu[4] != 0x06 || pNalu[5] != 0x05)return 0;unsigned char *p = pNalu + 4 + 2;while (*p++ == 0xff);// 往NALU中写入SEI信息const char *szVideojjUUID = "VideojjLeonUUID";char *pp = (char *)p;for (int i = 0; i < strlen(szVideojjUUID); i++){if (pp[i] != szVideojjUUID[i])return 0;}VjjSEI sei;sei.nTimeStamp = nTimeStamp;sei.nLen = nNaluLen - (pp - (char *)pNalu) - 16 - 1;sei.szUD = new char[sei.nLen];memcpy(sei.szUD, pp + 16, sei.nLen);_vVjjSEI.push_back(sei);return 1;
}
解析音频标签
  1. 入口函数CreateTag

    1、解析标签头部

    2、判断标签头部的类型

    3、根据标签头部的类型,解析不同的标签

    4、如果是视频类型的标签,那么就创建并解析视频标签
    CFlvParser::Tag *CFlvParser::CreateTag(unsigned char *pBuf, int nLeftLen)
    {
    // 开始解析标签头部
    TagHeader header;
    header.nType = ShowU8(pBuf+0); // 类型
    header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度
    header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳
    header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段
    header.nStreamID = ShowU24(pBuf + 8); // 流的id
    header.nTotalTS = (unsigned int)((header.nTSEx << 24)) + header.nTimeStamp;
    // 标签头部解析结束

    cout << "total TS : " << header.nTotalTS << endl;
    //cout << "nLeftLen : " << nLeftLen << " , nDataSize : " << pTag->header.nDataSize << endl;
    if ((header.nDataSize + 11) > nLeftLen)
    {
    return NULL;
    }

    Tag *pTag;
    switch (header.nType) {
    case 0x09: // 视频类型的Tag
    pTag = new CVideoTag(&header, pBuf, nLeftLen, this);
    break;
    case 0x08: // 音频类型的Tag
    pTag = new CAudioTag(&header, pBuf, nLeftLen, this);
    break;
    default: // script类型的Tag
    pTag = new Tag();
    pTag->Init(&header, pBuf, nLeftLen);
    }

    return pTag;
    }

  2. 创建音频标签

    1、初始化

    2、解析音频编码类型

    3、解析采样率

    4、解析精度和类型

    5、解析音频标签

CFlvParser::CAudioTag::CAudioTag(TagHeader *pHeader, unsigned char *pBuf, int nLeftLen, CFlvParser *pParser)
{// 初始化Init(pHeader, pBuf, nLeftLen);unsigned char *pd = _pTagData;_nSoundFormat = (pd[0] & 0xf0) >> 4; // 音频编码类型_nSoundRate = (pd[0] & 0x0c) >> 2; //采样率_nSoundSize = (pd[0] & 0x02) >> 1; // 精度_nSoundType = (pd[0] & 0x01); // 类型// 解析音频标签if (_nSoundFormat == 10) // AAC{ParseAACTag(pParser);}
}
  1. 解析音频标签
    1、获取数据包的类型

    2、判断数据包的类型

    3、如果数据包是音频配置信息,那么解析有音频配置信息

    4、如果是原始音频数据,那么对原始音频数据进行处理

int CFlvParser::CAudioTag::ParseAACTag(CFlvParser *pParser)
{unsigned char *pd = _pTagData;// 数据包的类型:音频配置信息,音频数据int nAACPacketType = pd[1];// 如果是音频配置信息if (nAACPacketType == 0){// 解析配置信息ParseAudioSpecificConfig(pParser, pd);}// 如果是音频数据else if (nAACPacketType == 1){// 解析音频数据ParseRawAAC(pParser, pd);}else{}return 1;
}
  1. 处理始⾳频配置
    1、解析AAC的采样率

    2、解析采样率索引

    3、解析声道

int CFlvParser::CAudioTag::ParseAudioSpecificConfig(CFlvParser *pParser, unsigned char *pTagData)
{unsigned char *pd = _pTagData;// AAC的profile_aacProfile = ((pd[2]&0xf8)>>3) - 1;// 采样率索引_sampleRateIndex = ((pd[2]&0x07)<<1) | (pd[3]>>7);// 声道_channelConfig = (pd[3]>>3) & 0x0f;_pMedia = NULL;_nMediaLen = 0;return 1;
}
  1. 处理原始音频数据
    主要的功能是为原始的音频数据添加元数据,可以根据自己的需要进行改写
int CFlvParser::CAudioTag::ParseRawAAC(CFlvParser *pParser, unsigned char *pTagData)
{uint64_t bits = 0;// 数据长度int dataSize = _header.nDataSize - 2;// 制作元数据WriteU64(bits, 12, 0xFFF);WriteU64(bits, 1, 0);WriteU64(bits, 2, 0);WriteU64(bits, 1, 1);WriteU64(bits, 2, _aacProfile);WriteU64(bits, 4, _sampleRateIndex);WriteU64(bits, 1, 0);WriteU64(bits, 3, _channelConfig);WriteU64(bits, 1, 0);WriteU64(bits, 1, 0);WriteU64(bits, 1, 0);WriteU64(bits, 1, 0);WriteU64(bits, 13, 7 + dataSize);WriteU64(bits, 11, 0x7FF);WriteU64(bits, 2, 0);_nMediaLen = 7 + dataSize;_pMedia = new unsigned char[_nMediaLen];// 把元数据放进临时数组中unsigned char p64[8];p64[0] = (unsigned char)(bits >> 56);p64[1] = (unsigned char)(bits >> 48);p64[2] = (unsigned char)(bits >> 40);p64[3] = (unsigned char)(bits >> 32);p64[4] = (unsigned char)(bits >> 24);p64[5] = (unsigned char)(bits >> 16);p64[6] = (unsigned char)(bits >> 8);p64[7] = (unsigned char)(bits);// 把临时数组的数据复制给元数据memcpy(_pMedia, p64+1, 7);// 把读取到的数据复制到后面memcpy(_pMedia + 7, pTagData + 2, dataSize);return 1;
}
解析其他标签
  1. ⼊⼝函数CreateTag
    1、解析标签头部

    2、判断标签头部的类型

    3、根据标签头部的类型,解析不同的标签

    4、如果是视频类型的标签,那么就创建并解析视频标签

CFlvParser::Tag *CFlvParser::CreateTag(unsigned char *pBuf, int nLeftLen)
{// 开始解析标签头部TagHeader header;header.nType = ShowU8(pBuf+0); // 类型header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段header.nStreamID = ShowU24(pBuf + 8); // 流的idheader.nTotalTS = (unsigned int)((header.nTSEx << 24)) + header.nTimeStamp;// 标签头部解析结束cout << "total TS : " << header.nTotalTS << endl;//cout << "nLeftLen : " << nLeftLen << " , nDataSize : " << pTag->header.nDataSize << endl;if ((header.nDataSize + 11) > nLeftLen){return NULL;}Tag *pTag;switch (header.nType) {case 0x09: // 视频类型的TagpTag = new CVideoTag(&header, pBuf, nLeftLen, this);break;case 0x08: // 音频类型的TagpTag = new CAudioTag(&header, pBuf, nLeftLen, this);break;default: // script类型的TagpTag = new Tag();pTag->Init(&header, pBuf, nLeftLen);}return pTag;
}
  1. 解析普通标签
    没有太大的功能,就是数据的复制
void CFlvParser::Tag::Init(TagHeader *pHeader, unsigned char *pBuf, int nLeftLen)
{// 复制标签头部信息memcpy(&_header, pHeader, sizeof(TagHeader));_pTagHeader = new unsigned char[11];memcpy(_pTagHeader, pBuf, 11);// 复制标签body_pTagData = new unsigned char[_header.nDataSize];memcpy(_pTagData, pBuf + 11, _header.nDataSize);
}

6. 音频解码(裸流)

6.1 解码过程

音频流
解码器
PCM数据

6.2 ffmpeg处理流程

关键函数说明:

  • avcodec_find_decoder:根据指定的AVCodecID查找注册的解码器。
  • av_parser_init:初始化AVCodecParserContext。
  • avcodec_alloc_context3:为AVCodecContext分配内存。
  • avcodec_open2:打开解码器。
  • av_parser_parse2:解析获得⼀个Packet。
  • avcodec_send_packet:将AVPacket压缩数据给解码器。
  • avcodec_receive_frame:获取到解码后的AVFrame数据。
  • av_get_bytes_per_sample: 获取每个sample中的字节数。

关键数据结构

  • AVCodecParser:⽤于解析输⼊的数据流并把它分成⼀帧⼀帧的压缩编码数据。⽐较形象的说法就是把⻓⻓的⼀段连续的数据“切割”成⼀段段的数据。
 AVCodecParser ff_aac_parser = {codec_ids = { AV_CODEC_ID_AAC },priv_data_size = sizeof(AACAC3ParseContext),parser_init = aac_parse_init,   parser_parse =  = ff_aac_ac3_parse,  parser_close = ff_parse_close,   
};

从AVCodecParser结构的实例化我们可以看出来,不同编码类型的parser是和CODE_ID进⾏绑定的。所
以也就可以解释parser = av_parser_init(codec->id);

6.3 avcodec编解码API介绍

FFmpeg提供了两组函数,分别⽤于编码和解码:

  • 解码:avcodec_send_packet()、avcodec_receive_frame()。
  • 解码:avcodec_send_frame()、avcodec_receive_packet()。

建议的使⽤流程如下:

  1. 像以前⼀样设置并打开AVCodecContext。

  2. 输⼊有效的数据:

    • 解码:调⽤avcodec_send_packet()给解码器传⼊包含原始的压缩数据的AVPacket对象。
    • 编码:调⽤ avcodec_send_frame()给编码器传⼊包含解压数据的AVFrame对象。
    • 两种情况下推荐AVPacket和AVFrame都使⽤refcounted(引⽤计数)的模式,否则libavcodec可能不得不对输⼊的数据进⾏拷⻉。
  3. 在⼀个循环体内去接收codec的输出,即周期性地调⽤avcodec_receive_*()来接收codec输出的数据:

    • 解码:调⽤avcodec_receive_frame(),如果成功会返回⼀个包含未压缩数据的AVFrame。
    • 编码:调⽤avcodec_receive_packet(),如果成功会返回⼀个包含压缩数据的AVPacket。
      反复地调⽤avcodec_receive_packet()直到返回 AVERROR(EAGAIN)或其他错误。返回AVERROR(EAGAIN)错误表示codec需要新的输⼊来输出更多的数据。对于每个输⼊的packet或frame,codec⼀般会输出⼀个frame或packet,但是也有可能输出0个或者多于1个。
  4. 流处理结束的时候需要flush(冲刷) codec。因为codec可能在内部缓冲多个frame或packet,出于性能或其他必要的情况(如考虑B帧的情况)。 处理流程如下:

    • 调⽤avcodec_send_*()传⼊的AVFrame或AVPacket指针设置为NULL。 这将进⼊draining mode(排⽔模式)。
    • 反复地调⽤avcodec_receive_*()直到返回AVERROR_EOF,该⽅法在draining mode时不会返回AVERROR(EAGAIN)的错误,除⾮你没有进⼊draining mode。
    • 当重新开启codec时,需要先调⽤ avcodec_flush_buffers()来重置codec。

说明:
5. 编码或者解码刚开始的时候,codec可能接收了多个输⼊的frame或packet后还没有输出数据,直到内部的buffer被填充满。上⾯的使⽤流程可以处理这种情况。
6. 理论上,只有在输出数据没有被完全接收的情况调⽤avcodec_send_()的时候才可能会发⽣AVERROR(EAGAIN)的错误。你可以依赖这个机制来实现区别于上⾯建议流程的处理⽅式,⽐如每次循环都调⽤avcodec_send_(),在出现AVERROR(EAGAIN)错误的时候再去调⽤avcodec_receive_()。
7. 并不是所有的codec都遵循⼀个严格、可预测的数据处理流程,唯⼀可以保证的是 “调⽤avcodec_send_
()/avcodec_receive_()返回AVERROR(EAGAIN)的时候去avcodec_receive_()/avcodec_send_*()会成功,否则不应该返回AVERROR(EAGAIN)的错误。”⼀般来说,任何codec都不允许⽆限制地缓存输⼊或者输出。
8. 在同⼀个AVCodecContext上混合使⽤新旧API是不允许的,这将导致未定义的⾏为。

6.4 avcodec_send_packet

函数:int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
作⽤:⽀持将裸流数据包送给解码器
警告:

  • 输⼊的avpkt-data缓冲区必须⼤于AV_INPUT_PADDING_SIZE,因为优化的字节流读取器必须⼀次读取32或者64⽐特的数据
  • 不能跟之前的API(例如avcodec_decode_video2)混⽤,否则会返回不可预知的错误

备注:在将包发送给解码器的时候,AVCodecContext必须已经通过avcodec_open2打开
参数:
avctx:解码上下⽂
avpkt:输⼊AVPakcet.通常情况下,输⼊数据是⼀个单⼀的视频帧或者⼏个完整的⾳频帧。调⽤者保留包的原有属性,解码器不会修改包的内容。解码器可能创建对包的引⽤。如果包没有引⽤计数将拷⻉⼀份。跟以往的API不⼀样,输⼊的包的数据将被完全地消耗,如果包含有多个帧,要求多次调⽤avcodec_recvive_frame,直到avcodec_recvive_frame返回VERROR(EAGAIN)或AVERROR_EOF。输⼊参数可以为NULL,或者AVPacket的data域设置为NULL或者size域设置为0,表示将刷新所有的包,意味着数据流已经结束了。第⼀次发送刷新会总会成功,第⼆次发送刷新包是没有必要的,并且返回AVERROR_EOF,如果×××缓存了⼀些帧,返回⼀个刷新包,将会返回所有的解码包

返回值:

  • 0: 表示成功
  • AVERROR(EAGAIN):当前状态不接受输⼊,⽤户必须先使⽤avcodec_receive_frame() 读取数据帧;
  • AVERROR_EOF:解码器已刷新,不能再向其发送新包;
  • AVERROR(EINVAL):没有打开解码器,或者这是⼀个编码器,或者要求刷新;
  • AVERRO(ENOMEN):⽆法将数据包添加到内部队列。

6.5 avcodec_receive_frame

函数:int avcodec_receive_frame ( AVCodecContext * avctx, AVFrame * frame )
作⽤:从解码器返回已解码的输出数据。
参数:

  • avctx: 编解码器上下⽂
  • frame: 获取使⽤reference-counted机制的audio或者video帧(取决于解码器类型)。请注意,在执⾏其他操作之前,函数内部将始终先调⽤av_frame_unref(frame)。

返回值:

  • 0: 成功,返回⼀个帧
  • AVERROR(EAGAIN): 该状态下没有帧输出,需要使⽤avcodec_send_packet发送新的packet到解码器
  • AVERROR_EOF: 解码器已经被完全刷新,不再有输出帧
  • AVERROR(EINVAL): 编解码器没打开
  • 其他<0的值: 具体查看对应的错误码

MP3解析

7. 视频解码

8. MP4分析

mp4⽂件由box组成,每个box分为Header和Data。其中Header部分包含了box的类型和⼤⼩,Data包含了⼦box或者数据,box可以嵌套⼦box。下图是⼀个典型mp4⽂件的基本结构:
在这里插入图片描述
链接
代码是前边的h264+aac

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/47372.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FPGA实验3:D触发器设计

一、实验目的及要求 熟悉Quartus II 的 VHDL 文本设计简单时序电路的方法&#xff1b; 掌握时序电路的描述方法、波形仿真和测试&#xff0c;特别是时钟信号的特性。 二、实验原理 运用Quartus II 集成环境下的VHDL文本设计方法设计简单时序电路——D触发器&#xff0c;依据…

三相PWM整流器滞环电流控制仿真matlab simulink

1、内容简介 略 88-可以交流、咨询、答疑 2、内容说明 略 三相&#xff30;&#xff37;&#xff2d;整流器已广泛应用工业与电气控制领域电流控制技术决定着三相&#xff30;&#xff37;&#xff2d;整流器系统的控制性能。综合比 较了各种电流控制方法应用较多的滞环比较…

C++ 类和对象 构造函数(下)

一 初始化列表&#xff1a; 1.1 构造函数体赋值&#xff1a; 在C中&#xff0c;构造函数用于创建对象并赋予其初始值。通常&#xff0c;我们可以在构造函数体内对成员变量进行赋值&#xff1a; class Date { public:Date(int year, int month, int day) {_year year;_mont…

golang 解压带密码的zip包

目录 Zip文件详解ZIP 文件格式主要特性常用算法Zip格式结构图总览Zip文件结构详解数据区本地文件头文件数据文件描述 中央目录记录区&#xff08;核心目录记录区 &#xff09;中央目录记录尾部区 压缩包解压过程方式1 通过解析中央目录区来解压方式2 通过读取本地文件头来解压两…

[言简意赅] Matlab生成FPGA端rom初始化文件.coe

&#x1f38e;Matlab生成FPGA端rom初始化文件.coe 本文主打言简意赅。 函数源码 function gencoeInitialROM(width, depth, signal, filepath)% gencoeInitialROM - 生成 Xilinx ROM 初始化格式的 COE 文件%% 输入参数:% width - ROM 数据位宽% depth - ROM 数据深度% s…

heic文件怎么转换成jpg?上百份文件转换3秒就能搞定(办公必备)

heic和jpg是两种不同的图片格式&#xff0c;平时整理图片素材时&#xff0c;如果需要将heic转为jpg格式&#xff0c;那么可以使用相关的heic图片转换工具。 ​ 为什么要将heic文件转换成jpg&#xff1f;虽然HEIC格式具有很多优点&#xff0c;但是目前并不是所有设备和应用程序…

好玩模拟游戏推荐:缺氧:眼冒金星 单机游戏分享

《缺氧》 是一款太空殖民模拟游戏。 在外太空岩深处&#xff0c;你手下的勤劳开拓者们需要熟练掌握科技&#xff0c;战胜新的陌生生命形式&#xff0c;以及利用难以置信的太空技术来生存。甚至&#xff0c;还有可能繁荣起来。 建立广阔的基地以及探索生存所需的资源&#xff1…

服务攻防_01数据库安全RedisCouchdbH2database

一、数据库-Redis-未授权RCE&CVE 1、未授权访问&#xff1a;CNVD-2015-07557 &#xff08;1&#xff09;漏洞描述 Redis默认情况下会绑定在6379端口 如果没有采取相关策略&#xff08;如添加防火墙规则阻止非信任来源IP访问&#xff09;&#xff0c;会将Redis暴露在公网…

HTML5实现好看的天气预报网站源码

文章目录 1.设计来源1.1 获取天气接口1.2 PC端页面设计1.3 手机端页面设计 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_4…

揭秘电子画册制作流程,打造独一无二的作品

在这个数字化的时代&#xff0c;电子画册已经成为了展示个人创意和品牌形象的重要工具。它不仅能够呈现出丰富多彩的内容&#xff0c;还能够实现互动性和传播性&#xff0c;吸引众多观众的目光。然而&#xff0c;许多人对于电子画册的制作流程仍然感到陌生。本文将揭秘电子画册…

企业VR展厅如何提升品牌形象,生动展示产品和企业文化?

一、提升产品展示效果 1、全方位展示产品细节 企业VR展厅可以通过3D建模和虚拟现实技术&#xff0c;将产品的每一个细节清晰地展示出来。客户可以全方位查看产品的外观、结构和功能。这种身临其境的体验远比传统的平面展示更加生动和详细。 细节展示&#xff1a;客户可以通过…

Ubuntu22 Qt6.6 ROS 环境搭建

Ubuntu22.04; Qt6.6; Qt Creator 13.01; ROS2 1. 安装 Qt ROS 插件 1.下载地址&#xff1a; https://github.com/ros-industrial/ros_qtc_plugin/releases 选择对应 Qt Creator 版本的安装包。 2. Qt Creator中&#xff0c;“Help - 关于插件”–>“install Plugin…

一个模板实现的工厂的编译问题的解决。牵扯到重载、特化等

简介 在一个项目里&#xff0c;调用了第三封的库&#xff0c;这个库里面有个类用的很多&#xff0c;而且其构造函数至少有6个&#xff0c;并且个人感觉还不够多。根据实际使用&#xff0c;还得增加一些。 需求 1、增加构造函数&#xff0c;比如除了下面的&#xff0c;还增加…

Gateway源码分析:路由Route、断言Predicate、Filter

文章目录 源码总流程图说明GateWayAutoConfigurationDispatcherHandlergetHandler()handleRequestWith()RouteToRequestUrlFilterReactiveLoadBalancerClientFilterNettyRoutingFilter 补充知识适配器模式 详细流程图 源码总流程图 在线总流程图 说明 Gateway的版本使用的是…

01常见控件

文章目录 控件各种响应事件获取控件类型CButton/CheckBox&#xff08;多选&#xff09;/RadioButton&#xff08;单选&#xff09;EditControl&#xff08;文本编辑框&#xff09;/ ListBox&#xff08;列表文本框&#xff09;/ComboBox&#xff08;可下拉列表&#xff09;Prog…

【Ubuntu】Ubuntu系统镜像

清华镜像源 Index of /ubuntu-releases/ | 清华大学开源软件镜像站 | Tsinghua Open Source MirrorIndex of /ubuntu-releases/ | 清华大学开源软件镜像站&#xff0c;致力于为国内和校内用户提供高质量的开源软件镜像、Linux 镜像源服务&#xff0c;帮助用户更方便地获取开源软…

stm32学习:(寄存器2)GPIO总体说明

目录 GPIO的主要特点 GPIO的8种工作模式 GPIO电路结构 GPIO输出模式 输出流程 复用输出模式 GPIO输入模式 输入流程 模拟输入流程 GPIO相关的7个寄存器 GPIOx_CRL GPIOx_CRH GPIOx_IDR GPIOx_ODR GPIOx_BSRR GPIOx_BRR GPIOx_LCKR 实例 三个灯流水灯 main.…

C语言基础 9. 指针

C语言基础 9. 指针 文章目录 C语言基础 9. 指针9.1. &9.2. 指针9.3. 指针的使用9.4. 指针与数组9.5. 指针与const9.6. 指针运算9.7. 动态内存分配 9.1. & 运算符&: scanf(“%d”, &i);里的& 获得变量的地址, 它的操作数必须是变量 int i;printf(“%x”, &…

【SpringBoot Web开发之静态资源访问】笔记

详细内容见官方文档&#xff1a;Static Content SpringBoot Web开发之静态资源访问 1.准备工作&#xff1a;创建WebDemo2.静态资源目录2.1官网原文2.2静态资源目录第一步&#xff1a;依照上面2.1官网原文中创建如下目录第二步&#xff1a;复制粘贴图片到静态资源目录中第三步…

MySQL:JOIN 多表查询

多表查询 在关系型数据库中&#xff0c;表与表之间是有联系的&#xff0c;它们通过 外键 联系在一起&#xff0c;所以在实际应用中&#xff0c;经常使用多表查询。多表查询就是同时查询两个或两个以上的表。 MySQL多表查询是数据库操作中非常重要的一部分&#xff0c;它允许你…