05_解封装和解码

news2024/11/10 10:41:34

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 flv
	parser.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;
 
	// 视频类型Tag
	class CVideoTag : public Tag;
 
	// 音频类型Tag
	class 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.264
	int _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,总是0
 
		unsigned int nTotalTS;
 
		TagHeader() : nType(0), nDataSize(0), nTimeStamp(0), nTSEx(0), nStreamID(0), nTotalTS(0) {}
		~TagHeader() {}
	};
  • 标签数据
  1. script类型的标签
	// flv的tag
	class 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; // 指向标签body
		unsigned 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; // 类型
 
		// aac
		static 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); // 流的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;
}
解析视频标签
  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;
}
  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);
 
		// 解析NALU
		pParser->_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); // 流的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;
}
  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.coloradmin.cn/o/1939487.html

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

相关文章

【LINUX】pr_info函数开发摸索

1、打印开关可随时控制&#xff0c;开机如果要修改是否打印日志的话&#xff0c;需要修改代码重新编译内核才行&#xff0c;其实如果真要搞&#xff0c;应该有其他方法&#xff1b; 2、打印次数&#xff0c;当前代码里边写的是1000次&#xff0c;其实可以根据传参动态修改打印…

CUDA编程00 - 配置CUDA开发环境

第一步&#xff1a; 在一台装有Nvidia显卡和驱动的机器上&#xff0c;用nvidia-smi命令查看显卡所支持cuda版本 第二步&#xff1a; 到Nvidia官网下载CUDA Toolkit并安装&#xff0c;CUDA Toolkit Archive | NVIDIA Developer 安装时按提示下一步即可&#xff0c;安装完成用 …

Django cursor()增删改查和shell环境执行脚本

在Django中&#xff0c;cursor()方法是DatabaseWrapper对象&#xff08;由django.db.connectio提供&#xff09;的一个方法&#xff0c;用于创建一个游标对象。这个游标对象可以用来执行SQL命令&#xff0c;从而实现对数据库的增删改查操作。 查询&#xff08;Select&#xff0…

C++初学者指南-5.标准库(第一部分)--标准库查询存在算法

C初学者指南-5.标准库(第一部分)–标准库查询存在算法 文章目录 C初学者指南-5.标准库(第一部分)--标准库查询存在算法any_of / all_of / none_ofcountcount_if相关内容 不熟悉 C 的标准库算法&#xff1f; ⇒ 简介 any_of / all_of / none_of 如果在输入范围(所有元素…

2024最新教程,在docker中安装kali,并配置ssh连接

docker的基本使用&#xff1a;搭建高效攻防靶场vulfocus与Docker仓库管理实战&#xff1a;从听说到入门 拉取kali官方镜像 docker pull kalilinux/kali-rolling 启动一个kali镜像&#xff0c;将容器中的22端口映射到主机100端口&#xff0c;方便ssh直接连接 docker run -it…

Unity UGUI 之 Toggle

​本文仅作学习笔记与交流&#xff0c;不作任何商业用途本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正​ 1.什么是Toggle&#xff1f; Unity - Manual: Toggle 带复选框的开关&#xff0c;可…

linux-二元信号量和计数信号量-生产者消费者模型以及用二元信号量实现-死锁(2)-侠义消息队列(fifo)-proc文件系统

二元信号量和计数信号量的区别&#xff1a; 二元信号量和计数信号量在嵌入式系统和多任务环境中都是重要的同步机制&#xff0c;用于控制对共享资源的访问。它们之间的主要区别体现在以下几个方面&#xff1a; 1. 状态表示 二元信号量&#xff08;Binary Semaphore&#xff09;…

[计算机基础]一、计算机组成原理

计算机组成原理的考察目标为&#xff1a; 1. 掌握单处理器计算机系统中主要部件的工作原理、组成结构以及相互连接方式。 2. 掌握指令集体系结构的基本知识和基本实现方法&#xff0c;对计算机硬件相关问题进行分析&#xff0c;并能够对相关部件进行设计。 3. 理解计算机系统的…

HTML5-canvas1

1、canvas&#xff1a;创建画布 <canvas id"canvas"></canvas>2、画一条直线 var canvasdocument.getElementById(cancas&#xff09;; canvas.width800; canvas.height800; var contextcanvas.getContext(2d); //获得2d绘图上下文环境 //画一条直线 c…

算法 - 图论Dijkstra(原理、思路代码实现、以东南大学真题为例讲解手算方法)

Dijkstra 算法原理&#xff1a; Dijkstra算法是一种经典的用于计算单源最短路径的算法。它可以在带权重的图中找到从源节点到所有其他节点的最短路径。Dijkstra算法通过贪心策略&#xff0c;不断选择当前已知最短路径最小的节点&#xff0c;更新其邻接节点的路径长度&#xff…

Linux之旅:常用的指令,热键和权限管理

目录 前言 1. Linux指令 &#xff08;1&#xff09; ls &#xff08;2&#xff09; pwd 和 cd &#xff08;3&#xff09;touch 和 mkdir &#xff08;4&#xff09; rmdir 和 rm &#xff08;5&#xff09;cp &#xff08;6&#xff09;mv &#xff08;7&#xff09;…

Qt窗口介绍

Qt窗口 一、Qt窗口二、菜单栏创建菜单栏在菜单栏中添加菜单创建菜单项在菜单项之间添加分割线综合练习 三、工具栏创建工具栏设置停靠位置设置浮动属性设置移动属性综合练习 四、状态栏状态栏的创建在状态栏中显示实时消息在状态栏显示永久的消息 五、浮动窗口浮动窗口的创建设…

达梦数据库系列—29. DTS迁移ORACLE到DM

目录 1.ORACLE源端信息 2.DM目的端信息 3.DTS 迁移评估 4.数据库迁移 4.1 Oracle 源端数据库准备 4.2 目的端达梦数据库准备 初始化参数设置 兼容性参数设置 表空间规划 用户规划 创建迁移用户和表空间 4.3迁移步骤 创建迁移 配置数据源 配置迁移对象及策略 开…

Vue3 --- 路由

路由就是一组key-value的对应关系&#xff1b;多个路由&#xff0c;需要经过路由器的管理。 1. 基本切换效果 安装路由器 npm i vue-router /router/index.ts // import { createRouter, createWebHistory } from vue-router import Home from /components/Home.vue import…

[Armbian] 部署Docker版Home Assistent,安装HACS并连接米家设备

title: [Armbian] 部署Docker版Home Assistent&#xff0c;安装HACS并连接米家设备 date: 2024-07-21T10:51:23Z lastmod: 2024-07-21T11:40:39Z [Armbian] 部署Docker版Home Assistent&#xff0c;安装HACS并连接米家设备 官网&#xff1a;Home Assistant (home-assistant.i…

算法——滑动窗口(day6)

1004.最大连续1的个数 ||| 1004. 最大连续1的个数 III - 力扣&#xff08;LeetCode&#xff09; 题目解析&#xff1a; 这道题如果能转化为滑动窗口的话就会很简单&#xff0c;因为我们如果尝试去把0翻转为1再计数的话等到第2轮又得重新翻转回来&#xff0c;费时费力~ 那么我…

gihub导入gitee仓库实现仓库同步

昨天在GitHub里导入了gitee仓库&#xff0c;但是在仓库同步这里卡了很久&#xff0c;因为网上大多数都是从github导入gitee&#xff0c;然后github生成token放入实现同步&#xff0c;但是我找到一种更为方便的&#xff01; 1.首先找到项目文件下的.git文件里的config文件 2.在…

linux 下将文件安装的mysql.server服务添加到systemctl中管理

1、找到mysql 的 mysql.server/ 文件位置 find / -name mysql.server 2、编辑mysql.server脚本&#xff0c;并将mysql.server脚本添加到linux服务 3、将mysql.server 复制到etc/init.d/mysqld 目录 cp /app/db/mysql/support-files/mysql.server /etc/init.d/mysqld 4、授权…

08 B端产品业务调研的流程(1)

业务调研是掌握业务情况的有效方法。一般核心流程如下图&#xff1a; 明确调研目标&#xff1a;即调研的目标&#xff0c;解决为什么要做业务调研&#xff0c;期望解决什么问题&#xff0c;达到什么样效果&#xff0c;定向。 选取调研对象&#xff1a;涉及多角色&#xff0c;一…

【吊打面试官系列-ZooKeeper面试题】zookeeper 负载均衡和 nginx 负载均衡区别?

大家好&#xff0c;我是锋哥。今天分享关于 【zookeeper 负载均衡和 nginx 负载均衡区别? 】面试题&#xff0c;希望对大家有帮助&#xff1b; zookeeper 负载均衡和 nginx 负载均衡区别? zk 的负载均衡是可以调控&#xff0c;nginx 只是能调权重&#xff0c;其他需要可控的都…