FFmpeg实战 - 解复用解码
文章目录
- 前置知识
- 音视频基础概念
- 解复用、解码的流程分析
- FFMPEG有8个常用库
- 常见音视频格式的介绍
- aac格式介绍
- h264格式介绍
- flv格式介绍
- mp4格式介绍
- FFmpeg解码解封装实战
- 数据包和数据帧(AVPacket/AVFrame)
- AVPacket/AVFrame的引用计数问题
- API介绍
- 注意事项
- 解复用
- 分离流
- 提取流
- demo样例
- 解码
- 用到的结构体与API
- 解码流程
- demo样例
前置知识
音视频基础概念
- 容器:即特定格式的多媒体文件,比如mp4、 flv、 mkv等都是指的容器格式。
- 压缩格式:音视频的压缩格式是指用来压缩和解压数据的算法和技术,它决定了数据如何从原始状态转换为压缩状态,以及如何从压缩状态还原回原始状态。例如音频压缩格式aac和视频压缩格式h264等。
- 媒体流(Stream):一个完整的视频当中一般包含多种媒体流,如音频流、视频流、字幕流等。例如一个mp4视频中如果有一个视频流和两个音频流,则这个视频中就有三个媒体流(stream)。其中视频流绝大多数情况都是经过压缩的,例如h264格式。音频流大多数也是经过压缩的,如aac格式,但有时也会使用未经压缩的pcm数据。
- 采样点(Sample):采样点是指在数字化的过程中,从连续信号中抽取出来的离散值。例如一个音频的采样频率(采样率)为48000Hz,就表示每秒钟采样48000次,即每秒钟会有48000个采样点。而音频采样点就等同于是像素。
- 帧内采样点的数量:音频中一个帧所包含的采样点数量。例如一个音频的采样率为44.1 kHz,帧内采样点为1024个,则每一帧所持续的时间就为 1024/44410≈0.0232 秒。
- 帧数据:视频是由一系列连续的图像组成的,每一幅图像被称为一帧。而一帧音频的长度是由其帧内采样点的数量决定的。
- 数据包(Packet)/数据帧(Frame):一个媒体流由大量的数据包或数据帧构成。压缩(未经解码处理)的媒体流是由数据包(Packet)构成的,未压缩(解码后)的原生媒体流是由数据帧(Frame)构成的。一个packet/frame就表示一帧音频或视频数据。
- 解复用器(AVformat):解复用器的主要功能有——容器识别(识别多媒体文件的容器格式,例如MP4、AVI、MKV、FLV等)、流提取(分离多媒体文件中的各个数据流,例如视频流、音频流、字幕流等)、解封装(将提取出来的媒体流解封装为大量的数据包packet)等。所以解复用器并不只是用于解封装
- 编解码器(AVcodec):解码器主要用于将packet解码(解压)为frame,编码器则负责将frame编码(压缩)为packet。
解复用、解码的流程分析
参照下图分析音视频解封装、解码的过程
解复用(解封装):要先经过解复用器(AVformat)对其处理,过滤器会分离出不同的媒体流,并将其拆分为packet,然后将产生的packet按照帧的先后顺序放到对应的packet队列中。
解码:接着解码器就从packet队列中拿数据,将packet解码为原生媒体流,也按照顺序将其放到frame队列中。
之所以要经过这一系列的过程,是因为一个压缩的媒体文件是无法直接播放,需要经解码处理后将其恢复为可播放的原始数据。
FFMPEG有8个常用库
- AVUtil:核心工具库,下面的许多其他模块都会依赖该库做一些基本的音视频处理操作。
- AVFormat:文件格式和协议库,该模块是最重要的模块之一,封装了Protocol层和Demuxer、 Muxer层,使得协议和格式对于开发者来说是透明的。
- AVCodec:编解码库,封装了Codec层,但是有一些Codec是具备自己的License的, FFmpeg是不会默认添加像libx264、 FDK-AAC等库的,但是FFmpeg就像一个平台一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开发者提供统一的接口。
- AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为音视频数据做特效处理是非常方便同时也非常高效的一种方式。
- AVDevice:输入输出设备库,比如,需要编译出播放声音或者视频的工具ffplay,就需要确保该模块是打开的,同时也需要SDL的预先编译,因为该设备模块播放声音与播放视频使用的都是SDL库。
- SwrRessample:该模块可用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换。
- SWScale:该模块是将图像进行格式转换的模块,比如,可以将YUV的数据转换为RGB的数据,缩放尺寸由1280720变为800480。
- PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数
常见音视频格式的介绍
aac格式介绍
这里只是对aac格式的简单介绍,内容拓展:AAC-ADTS格式分析【转载】-CSDN博客
aac的格式有两种:ADIF不常用,ADTS是主流,所以这里主要讲解ADTS。简单来说,ADTS可以在任意帧解码,也就是说它每⼀帧都有头信息。ADIF只有⼀个统⼀的header,所以必须得到所有的数据后解码。参考下图
⼀个AAC原始数据块⻓度是可变的,对原始帧加上ADTS头进⾏ADTS的封装,就形成了ADTS帧。参考下图
adts-header的长度一般为7字节,当protection_absent=0
时,表示需要校验码,此时的adts-header就会额外添加一个2字节的校验码,此时的adts-header长度就为9字节。
⼀般情况下ADTS的头信息都是7个字节,分为2部分:
- adts_fixed_header
- adts_variable_header
其中,adts_fixed_header
为固定头信息,adts_variable_header
是可变头信息。固定头信息中的数据每⼀帧都相同,⽽可变头信息则在帧与帧之间不同。 参考下图
注:ADTS Header的长度可能为7字节或9字节,当protection_absent字段为时,表示需要校验码,此时是9字节;否则为7字节。
常见的header字段如下:
- 同步字(syncword):2个字节(16位) 同步字是ADTS文件的标志符,它用于确定音频帧的开始位置和结束位置,通常为0xFFF。
- ID (MPEG Version):1个字节(8位) ID指示使用的MPEG版本。值为0表示MPEG-4,值为1表示MPEG-2。
- Layer:2个比特 Layer定义了音频流所属的层级,对于AAC来说,其值为0。
- Protection Absent:1个比特 Protection Absent指示是否启用CRC错误校验。当该比特为0时,表明音频数据经过CRC校验,否则未经过CRC校验。
- Profile:2个比特 Profile指示编码所使用的AAC规范类型,如AAC LC、AAC HE-AAC等。
- Sampling Frequency Index (Sampling Rate):4个比特 Sampling Frequency Index表示采样率的索引,它告诉解码器当前音频数据的采样率。这个值的范围是0到15,每个值表示一个特定的采样率。参考下图
- Private Bit:1个比特 Private Bit为私有比特,通常被设置为0,没有实际作用。
- Channel Configuration:3个比特 Channel Configuration指示音频的通道数,如单声道、立体声或多声道等。
- Originality:1个比特 Originality指示编码数据是否被原始产生,通常为0。
- Home:1个比特 Home bit通常被设置为0,没有实际作用。
- Emphasis:2个比特 Emphasis指示对信号进行强调处理的类型,一般不使用。
- sampling_frequency_index:表示使⽤的采样率下标,通过这个下标在Sampling Frequencies[ ]数组中查找得知采样率的值。
h264格式介绍
这里只是对h264格式的简单介绍,内容拓展:H264基础简介【转载】-CSDN博客
H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
图片出处:H.264/AVC 码流序列 - 简书 (jianshu.com)
H.264原始码流(裸流)是由一个接一个NALU组成, 每个NALU之间都使用start code(起始码)分隔,NALU单元通常由[StartCode] [NALU Header] [NALU Payload] 三部分组成,其中 Start Code 用于标示这是一个NALU 单元的开始,必须是00 00 00 01 或00 00 01。每个 NALU包括一个头部信息(NAL header)和一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)
NALU(NAL Unit),也就是NAL 单元。每个NALU包含了一个字节大小的NALU头信息(NAL header),以及一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。RBSP 指原始字节序列载荷,它是 NAL 单元的数据部分的封装格式。
flv格式介绍
这里只是对flv格式的简单介绍,内容拓展:FLV文件格式分析【转载】-CSDN博客
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。 参考下图
图片出处:FLV文件格式解析
mp4格式介绍
mp4协议本身没有多复杂,没啥特别难理解的地方,关键的“复杂”点就在于其“大”,嵌套的各种各样的子box。详情参考:整理mp4协议重点【转载】-CSDN博客
FFmpeg解码解封装实战
数据包和数据帧(AVPacket/AVFrame)
AVPacket/AVFrame的引用计数问题
在FFmepg中,数据包对应的结构体为AVPacket
,数据帧对应的结构体为AVFrame
。一个AVPacket/AVFrame就表示一帧视频数据或音频数据。
特别的是,AVPacket/AVFrame的内存模型比较特殊,因为可能出现多个AVPacket/AVFrame对应同一帧数据的情况,所以FFmepg采用了一种引用计数的方式,以避免内存浪费。
参考下图理解:
AVPacket/AVFrame变量本身并不直接存储数据,而是指向一块缓存空间AVBuffer,由缓冲区自身来维护引用计数和真正的媒体数据。以AVPacket为例,对于多个AVPacket共享同一个缓存空间的情况, FFmpeg引用计数的机制如下 :(AVFrame也是如此)
- 初始化时引用计数的指为0,只有真正分配AVBuffer的时候,引用计数的值才加至1。
- 当有新的Packet引用共享的缓存空间时, 就将引用计数+1。
- 当释放了引用共享空间的Packet时,就将引用计数-1;
- 引用计数减至0时,就释放掉引用的缓存空间AVBuffer。
API介绍
AVPacket:
AVPacket *av_packet_alloc();
为AVPacket申请空间,此时并未创建AVBuffer。void av_init_packet(AVPacket *pkt);
初始化pkt中的相关字段,例如将整型数据设为0,将指针为null等操作。int av_new_packet(AVPacket *pkt, int size);
创建数据包,申请一个size字节大小的AVBuffer,并让pkt的AVBufferRef指向它。此时才是真正的创建了AVBuffer。int av_packet_ref(AVPacket *dst, const AVPacket *src);
对给定数据包设置一个新的引用。其作用是将dst的AVBufferRef指向src的AVBuffer,即让dst也关联到src的AVBuffer。此时对应的AVBuffer的引用计数加一。void av_packet_unref(AVPacket *pkt);
擦除一个数据包。取消pkt和它对应AVBuffer的关联,并使其引用计数减一。如果AVBuffer的引用计数减为0了,则FFmpeg会释放掉这块AVBuffer的空间。void av_packet_move_ref(AVPacket *dst, AVPacket *src);
将src中的每个字段移动到dst,并重置(清空)src。此时src与AVBuffer的关联断掉,转移到dst上面,AVBuffer的引用计数不变。AVPacket *av_packet_clone(const AVPacket *src);
AVPacket克隆,相当于av_packet_alloc + av_packet_ref。创建一个和src一样的AVPacket,并作为返回值返回给上层。此时对应的AVBuffer的引用计数加一。void av_packet_free(AVPacket **pkt);
释放AVPacket,要和av_packet_alloc搭配使用,成对出现。
AVFrame:
AVFrame *av_frame_alloc();
为AVFrame 申请空间,作用与av_packet_alloc一样。int av_frame_ref(AVFrame *dst, const AVFrame *src);
对给定数据包设置一个新的引用。作用与av_packet_ref一样。void av_frame_unref(AVFrame *frame);
擦除一个数据包。作用与av_packet_unref一样。void av_frame_move_ref(AVFrame *dst, AVFrame *src);
将src中的每个字段移动到dst,并重置(清空)src。作用与av_packet_move_ref一样。int av_frame_get_buffer(AVFrame*frame,int align);
为媒体数据分配新的缓冲区,根据AVFrame分配内存。AVFrame *av_frame_clone(const AVFrame *src);
作用与av_packet_clone一样。void av_frame_free(AVFrame **frame);
释放AVFrame,要和av_frame_alloc搭配使用,成对出现。
注意事项
- AVPacket/AVFrame和AVBuffer是两回事,AVBuffer是真实的数据缓冲空间,AVPacket/AVFrame并不直接存储媒体数据,而是有能够访问到AVBuffer的引用字段。所以AVPacket/AVFrame和AVBuffer都需要为其分配空间,就好像指针需要4/8字节空间,而它指向的数据也需要分配空间。
- av_init_packet会将字段下的所有指针置为null,所以如果此时的AVPacket字段中还关联的AVBuffer数据而没有释放,在其指针置为null后就会失去关联,此时的AVBuffer就永远无法得到释放了,就会造成内存泄漏。所以av_init_packet函数不能滥用,很容易导致内存泄漏。
解复用
解复用是指将一个复合的音视频文件或流中的不同数据流分离出来。
分离流
- 用到的结构体与API
AVFormatContext *avformat_alloc_context();
申请一个AVFormatContext结构内存,并进行简单的初始化。此时AVFormatContext中还没有数据。其中AVFormatContext
是解复用器上下文结构体。void avformat_free_context(AVFormatContext *s);
释放 AVFormatContext 及其所有流。int avformat_open_input(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);
打开输入的媒体文件,并识别容器读取header。参数ps为解复用器上下文对象的地址;url表示输入文件的路径或者网络地址;fmt表示设置输入格式,为null则表示自动识别(一般都设为null);options表示选项,一般也设为null。void avformat_close_input(AVFormatContext **s);
关闭打开的AVFormatContext。释放它及其所有内容并将 *s 置为null。其函数中已经包含了avformat_free_context操作,所以调用了avformat_close_input之后就就不用再调用avformat_open_input了。int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
读取媒体文件的数据包以及流信息等,用以填充AVFormatContext
结构体信息,options选项一般设为null。int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, const struct AVCodec **decoder_ret, int flags);
查找指定流的下标,其返回值为对应的流所在format->streams数组下的下标。参数ic表示指定的解复用器上下文,type就表示要查找的流(AVMEDIA_TYPE_AUDIO表示音频流、AVMEDIA_TYPE_VIDEO表示视频流等等),wanted_stream_nb和related_stream一般设为-1表示自动选择,decoder_ret是输出型参数,返回所选流的解码器,可以为null;flags,暂时未定义(“flags; none are currently defined”)。int av_read_frame(AVFormatContext *s, AVPacket *pkt);
读取一帧音视频包,返回流的下一个帧。这个函数会自动读取下一帧数据。返回值为0表示成功,如果为AVERROR_EOF则表示读到末尾结束了。
- 解复用流程介绍
先用avformat_alloc_context
分配一个解复用器上下文AVFormatContext,接着用avformat_open_input
打开媒体文件。随后可以用avformat_find_stream_info
读取媒体到AVFormatContext中,进而分离流,或者可以直接用av_find_best_stream
分离流。
avformat_find_stream_info接口之所以不是必须的,这是因为avformat_open_input接口在调用时不只是打开了媒体文件,并且还会读取格式头部信息并初始化AVFormatContext。所以即使在不调用avformat_find_stream_info的情况下,AVFormatContext中还是会有媒体文件的元数据的,可以保证正常的分离流操作。
分离流之后,就开始在一个循环中不断调用av_read_frame
读取数据包并处理数据包,直到读完媒体文件。注意,虽然函数叫read frame,但读取的其实是packet。在此期间,根据packet->stream_index和av_find_best_stream的返回值匹配,区分处理音频数据和视频数据。
提取流
由于不同容器的封装格式不同,有些容器在分离流之后读取的packet中是裸流数据,即不包含头部信息,只有媒体流数据,音频和视频配置信息通常存储在元数据中。而有些容器在分离流之后读取的packet是包含头部信息的。所以对于不包含头部信息的媒体流数据,在提取流时就要为其加上头部信息再写入,而包含头部信息的就可以直接写入。
例如ts文件分离流之后读取的packet就是包含头部信息的,在提取的时候就可以直接写入,不用做其它处理。而mp4和flv等格式分离流之后读取的packet却是不包含头部信息的,其packet只有裸流数据,在这种情况下,就需要额外先为其写入头部信息,再写入packet(裸流数据)
如下是一些常见的格式:
- packet为裸流数据的格式:FLV、MP4、MKV、WebM
- packet为带有头部的媒体流数据的格式:TS、MPEG-2 PS、AVI、WMV / ASF
音频流 - aac
音频流以提取aac流为例,需要在写入packet->data之前,手动绘制一个7字节的adts头部数据,并将其写入。
视频流 - h264
而视频流的则比较麻烦了,以h264格式为例,因为情况比较复杂,简单来说就是在提取h264流数据时并不能简单的手动写入头部数据,而是需要让FFmepg中的过滤器代为处理,将数据转为标准的Annex B格式的数据。大致需要用到如下内容:
AVBitStreamFilter
过滤器的结构体。AVBSFContext
过滤器上下文的结构体,BSF即为BitStreamFilter的简写。const AVBitStreamFilter *av_bsf_get_by_name(const char *name);
根据名字查找指定的过滤器,不同的过滤器对应着不同的功能。- “h264_mp4toannexb”,一个过滤器的名字,其功能是将MP4格式转换成AnnexB格式。
int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);
为过滤器分配上下文,即将过滤器与过滤器上下文之间进行绑定。int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
复制编码器参数,以便过滤器正常运行。int av_bsf_init(AVBSFContext *ctx);
初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用)int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
将pkt发送给ctx对应的那个过滤器,过滤器会将处理好的packet放到对应的缓冲区中。int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
从对应的缓冲区中取出一个packet。
demo样例
解复用一个mp4文件,提取出mp4媒体文件中的aac和h264两个流的文件。
#include <iostream>
#include <fstream>
#include <string>
using namespace std;// FFmepg-7.0头文件引入
extern "C"
{
#include "libavutil/error.h"
#include "libavformat/avformat.h"
#include "libavcodec/bsf.h"
}// sampling_frequencies,用于获取sampling_frequency_index
const int sampling_frequencies[] = {96000, // 0x088200, // 0x164000, // 0x248000, // 0x344100, // 0x432000, // 0x524000, // 0x622050, // 0x716000, // 0x812000, // 0x911025, // 0xa8000 // 0xb// 0xc 0xd 0xe 0xf 是保留的
};
/*** 填充aac-ADTS协议头** @param adts_header_buf 自定义ADTS-header的缓冲区* @param data_length aac-body的长度(packet->size)* @param profile AAC规范类型* @param sample_rate 采样率* @param channels 声道数
*/
bool fill_ADTS_header(char* adts_header_buf, const int data_length,const int profile, const int sample_rate, const int channels)
{int sampling_frequency_index = 3; // 默认48000hzint adtsLen = data_length + 7; // data_length + adts_header_lenint frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]);for(int i = 0; i < frequencies_size; i++){// 找到了对应的采样率,填充adts-headerif(sampling_frequencies[i] == sample_rate){sampling_frequency_index = i;// syncword:0xfff - 12bitsadts_header_buf[0] = 0xff; // 高8bitsadts_header_buf[1] = 0xf0; // 低4bits// ID=0(MPEG-4) - 1bitadts_header_buf[1] |= (0 << 3);// Layer:0 - 2bitsadts_header_buf[1] |= (0 << 1);// protection_absent=1(no CRC) - 1bitadts_header_buf[1] |= 1;// profile:${profile} - 2bitsadts_header_buf[2] = (profile) << 6;// sampling frequency index=${sampling_frequency_index} - 4bitsadts_header_buf[2] |= (sampling_frequency_index & 0x0f)<<2;//private bit:0 - 1bitadts_header_buf[2] |= (0 << 1);//channel configuration:channels 高1bitadts_header_buf[2] |= (channels & 0x04)>>2;//channel configuration:channels 低2bitsadts_header_buf[3] = (channels & 0x03)<<6;//original:0 - 1bitadts_header_buf[3] |= (0 << 5);//home:0 - 1bitadts_header_buf[3] |= (0 << 4);//copyright id bit:0 - 1bitadts_header_buf[3] |= (0 << 3);//copyright id start:0 - 1bitadts_header_buf[3] |= (0 << 2);//frame length:value - 高2bitsadts_header_buf[3] |= ((adtsLen & 0x1800) >> 11);//frame length:value - 中间8bitsadts_header_buf[4] = (uint8_t)((adtsLen & 0x7f8) >> 3);//frame length:value - 低3bitsadts_header_buf[5] = (uint8_t)((adtsLen & 0x7) << 5);//buffer fullness:0x7ff - 高5bitsadts_header_buf[5] |= 0x1f;//buffer fullness:0x7ff - 低6bitsadts_header_buf[6] = 0xfc; //11111100// number_of_raw_data_blocks_in_frame:// 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。return true;}}// 没找到对应的采样率cerr << "unsupport samplerate: " << sample_rate << endl;return false;
}// usage: process <in_file> <out_audio> <out_video>
int main(int argc, char* argv[])
{// 打开文件ofstream out_audio(argv[2], ios_base::out | ios_base::binary);ofstream out_video(argv[3], ios_base::out | ios_base::binary);// 解复用器上下文AVFormatContext* fmt_ctx = nullptr;// 打开一个输入流并读取其headeravformat_open_input(&fmt_ctx, argv[1], nullptr, nullptr);// 获取媒体流信息(index)int audio_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);// 指定过滤器:h264_mp4toannexb过滤器的功能是将MP4格式转换成AnnexBconst AVBitStreamFilter* avbsf = av_bsf_get_by_name("h264_mp4toannexb");// 为过滤器分配上下文AVBSFContext* avbsf_ctx = nullptr;av_bsf_alloc(avbsf, &avbsf_ctx);// 复制编码器参数,以便过滤器正常运行(为过滤器填充音频流的编码器参数)avcodec_parameters_copy(avbsf_ctx->par_in, fmt_ctx->streams[video_index]->codecpar);// 初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用)av_bsf_init(avbsf_ctx);// packet allocAVPacket* packet = av_packet_alloc();av_init_packet(packet);// 提取流;while(av_read_frame(fmt_ctx, packet) != AVERROR_EOF){if(packet->stream_index == audio_index) // 音频流(暂定aac格式){// 手动添加aac-adts header// header 信息int profile = fmt_ctx->streams[audio_index]->codecpar->profile;int sample_rate = fmt_ctx->streams[audio_index]->codecpar->sample_rate;int channels = fmt_ctx->streams[audio_index]->codecpar->ch_layout.nb_channels;// 填充adts-headerchar buf[7] = {0}; // adts-header的大小就为7字节fill_ADTS_header(buf, packet->size, profile, sample_rate, channels);// 写入aac-adts_headerout_audio.write(buf, 7);// 写入aac-bodyout_audio.write((char*)packet->data, packet->size);}else if(packet->stream_index == video_index) //视频流(暂定h264格式){// sendav_bsf_send_packet(avbsf_ctx, packet);// receive // 一个输入数据包可能被过滤器拆分成多个输出数据包,所以这里要用循环while(av_bsf_receive_packet(avbsf_ctx, packet) == 0){out_video.write((char*)packet->data, packet->size); // 写入文件av_packet_unref(packet); // 释放packet,防止内存泄漏}}// 及时清理buf,防止内存泄漏av_packet_unref(packet);}// clear and exitout_video.close();out_audio.close();avformat_close_input(&fmt_ctx);av_bsf_free(&avbsf_ctx);av_packet_free(&packet);return 0;
}
解码
所谓解码就是指将压缩的音视频数据恢复为可播放的原始数据格式的过程。
用到的结构体与API
AVCodec
解码器的结构体。AVCodecParserContext
解析器上下文的结构体。const AVCodec *avcodec_find_decoder(enum AVCodecID id);
根据指定的id查找匹配的解码器。AVCodecParserContext *av_parser_init(int codec_id);
初始化id对应的AVCodecParserContext。AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
为AVCodecContext分配内存。int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
打开解码器(将解码器和解码器上下文进行关联)int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx, uint8_t **poutbuf, int *poutbuf_size, const uint8_t *buf, int buf_size, int64_t pts, int64_t dts, int64_t pos);
解析⼀个Packet。从buf中读取一个数据包到poutbuf中,并设置poutbuf_size,返回值为读取的字节数。int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
将AVPacket压缩数据给解码器,解码器会自动解码之后放到对应的缓冲区中。int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
获取到解码后的AVFrame数据(从对应的解码器缓冲区中取走一个frame)int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
获取每个样本sample中的字节数。
解码流程
对于解码操作而言,音频解码和视频解码的操作流大致一样,只不过在最后保存frame数据时要根据不同的格式采取不同的方式。
解码流程如下
首先创建环境:先用
avcodec_find_decoder
查找解码器,接着用av_parser_init
初始化裸流的解析器,用avcodec_alloc_context3
分配解码器上下文,用avcodec_open2
将解码器和解码器上下文进行关联。然后循环处理:先用
av_parser_parse2
解析一个数据包,接着用avcodec_send_packet
将packet发送给解码器,然后用avcodec_receive_frame
接收编码后的frame,最后写入解析帧,生成PCM数据。
demo样例
对一个媒体文件中的音频流进行解码,音频流为aac格式
#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
using namespace std;// FFmepg-7.0头文件引入
extern "C"
{
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
}// 解码器ID
const AVCodecID codec_id = AV_CODEC_ID_AAC;
// 数据包缓冲区大小
const int in_buf_size = 25600;
// 缓冲区阈值
const int threshold_size = 1024;// 解码操作
void decode(AVCodecContext *codec_ctx, AVPacket *packet, AVFrame *frame, ofstream &out_file)
{// 将带有压缩数据的数据包发送到解码器avcodec_send_packet(codec_ctx, packet);// 读取所有输出帧(在文件中,一般可能有任意数量的输出帧)while (avcodec_receive_frame(codec_ctx, frame) == 0){// 获取每个样本的字节数int data_size = av_get_bytes_per_sample(codec_ctx->sample_fmt);// 写入文件for (int i = 0; i < frame->nb_samples; i++){// if(av_sample_fmt_is_planar()) // 判断是否为平面格式// 交错的方式写入for (int j = 0; j < frame->ch_layout.nb_channels; j++){out_file.write((char *)frame->data[j] + data_size * i, data_size);}}}
}// 播放范例: ffplay -ar 48000*2 out.pcm
// Usage: <input file> <output file>
int main(int argc, char *argv[])
{// 解码器const AVCodec *codec = avcodec_find_decoder(codec_id);// 裸流的解析器上下文AVCodecParserContext *parser_ctx = av_parser_init(codec->id);// 解码器上下文AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);// 打开解码器(将解码器和解码器上下文进行关联)avcodec_open2(codec_ctx, codec, nullptr);// 打开io文件ifstream in_file(argv[1], ios_base::in | ios_base::binary);ofstream out_file(argv[2], ios_base::out | ios_base::binary);// 获取输入文件的长度in_file.seekg(0, ios_base::end);int in_file_len = in_file.tellg();in_file.seekg(0);// 从文件中读取一次uint8_t *in_buf = new uint8_t[in_buf_size + AV_INPUT_BUFFER_PADDING_SIZE]{0};in_file.read((char *)in_buf, in_buf_size);uint8_t *data = in_buf;int data_size = in_file.tellg();// 解析+解码AVPacket *packet = av_packet_alloc();AVFrame *frame = av_frame_alloc();while (in_file.tellg() != in_file_len || data_size > 0){// 解析packetint parse_size = av_parser_parse2(parser_ctx, codec_ctx, &packet->data, &packet->size,data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);// 更新数据信息data += parse_size;data_size -= parse_size;// 进行解码decode(codec_ctx, packet, frame, out_file);// 边界检查if (data_size < threshold_size && in_file.tellg() != in_file_len){memmove(in_buf, data, data_size);int read_count = min(in_buf_size - data_size, in_file_len - (int)in_file.tellg());if (!in_file.read((char *)in_buf + data_size, read_count)){cerr << "file read error! " << "[" << __FILE__ << ":" << __LINE__ << "]" << endl;}data_size += read_count;data = in_buf;}}// over: clear and exitin_file.close();out_file.close();av_parser_close(parser_ctx);avcodec_free_context(&codec_ctx);av_packet_free(&packet);av_frame_free(&frame);return 0;
}
相关文章:
FFmpeg实战 - 解复用解码
文章目录 前置知识音视频基础概念解复用、解码的流程分析FFMPEG有8个常用库 常见音视频格式的介绍aac格式介绍h264格式介绍flv格式介绍mp4格式介绍 FFmpeg解码解封装实战数据包和数据帧(AVPacket/AVFrame)AVPacket/AVFrame的引用计数问题API介绍注意事项…...
Jmeter混合压测(2407)
一 压测需求: 电商作为服务端,至少需要满足并发量,QPS:100/s,TPS:20/s。例如场景: 电商交易中,商品图片请求量最多,电商服务端需要满足并发请求查询图片信息。各家可能会并发请求同一家电商商品、订单等内容。 二 压…...
Prometheus各类监控及监控指标和告警规则
目录 linux docker监控 linux 系统进程监控 linux 系统os监控 windows 系统os监控 配置文件&告警规则 Prometheus配置文件 node_alert.rules docker_container.rules mysql_alert.rules vmware.rules Alertmanager告警规则 consoul注册服务 Dashboard JSON…...
G120 EPos配置方案及应用场景
EPos功能就是基本定位器功能,它可计算出轴的运行特性,使轴以时间最佳的方式移动到目标位置。EPos功能主要包括:设定值 直接给定(MDI)功能、 选择程序段功能、回参考点功能、点动功能、运行到固定挡块功能。 EPos功能通过处理给定的加速度、速度和位置值生成运行特性曲线,…...
定制化爬虫管理:为企业量身打造的数据抓取方案
在数据驱动的时代,企业如何高效、安全地获取互联网上的宝贵信息?定制化爬虫管理服务应运而生,成为解锁专属数据宝藏的金钥匙。本文将深入探讨定制化爬虫管理如何为企业量身打造数据抓取方案,揭秘其在海量信息中精准捕获价值数据的…...
Javascript面试基础6【每日更新10】
Gulp gulp是前端开发过程中一种基于流的代码构建工具,是自动化项目的构建利器;它不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成 Gulp的核心概念:流 流,简单来说就是建立在面向对象基础上的一种抽象的…...
CTF Web信息搜集 25000字详解
目录 前言信息收集常见信息分类域名信息whois备案CDN子域名解析记录 旁站C段服务器信息端口服务器类型数据库类型waf防火墙 网站信息备份文件备份文件常见的后缀名备份文件常见的文件名gedit备份文件vim备份文件收集方法 敏感目录CMS类型(指纹识别)探针泄…...
MSPM0G3507之电赛小车
一、前言 本文没什么技术分享,纯聊天。以下内容均为笔者的浅薄理解,有不对的地方还请多多包涵。 二、相关配置 主控单元:MSPM0G3507SPTR(48角) 编译环境:Keil5.33、5.39(推荐)都可 …...
linux运维一天一个shell命令之vmstat详解
概念 vmstat 是 Linux 系统中一个非常有用的工具,主要用于报告系统的虚拟内存、进程、CPU 活动和 IO 性能等信息。以下是对 vmstat 工具的详细解释: 基本语法 vmstat [options] [delay [count]]delay:更新的时间间隔(以秒为单…...
前端开发调试工具推荐分类整理
具体前往:前端调试工具分类整理汇总...
http协议与nginx
动态页面与静态页面的差别: (1)URL不同 静态⻚⾯链接⾥没有“?” 动态⻚⾯链接⾥包含“?” (2)后缀不同 (开发语⾔不同) 静态⻚⾯⼀般以 .html .htm .xml 为后缀 动态⻚⾯⼀般以 .php .jsp .py等为后…...
一款国外开发的高质量WordPress下载站模板主题
5play下载站是由国外站长开发的一款WordPress主题,主题简约大方,为v1.8版本, 该主题模板中包含了上千个应用,登录后台以后只需要简单的三个步骤就可以轻松发布apk文章, 我们只需要在WordPress后台中导入该主题就可以…...
Laravel为什么会成为最优雅的PHP框架
Laravel之所以成为最优雅的PHP框架之一,是因为它提供了一系列的优点,包括简洁的语法、强大的功能集、高度模块化和可扩展性、优雅的ORM、内置认证系统、丰富的社区支持和测试友好等。这些优点使得Laravel在PHP框架中脱颖而出,成为了很多开发者的首选框架。 官网:https://l…...
孤儿进程的例子
先让父进程死亡,子进程的父进程会被操作系统管理 先使用gcc编译代码, 执行代码后用 ps -p <进程号> -f 查看进程 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h>int main() {pid_t pid;// 创建…...
CSS前端面试题——怎么用CSS实现一个宽高自适应的正方形?
方法一:使用 padding 方案 这种方法通过设置元素的 padding 属性来实现宽高比例相等的正方形。假设我们希望正方形的边长为相对于父容器的百分比值,比如50% .square {width: 50%; /* 可以是任意宽度,这里假设为父元素宽度的50% */padding-t…...
谷粒商城实战笔记-56~57-商品服务-API-三级分类-修改-拖拽功能完成
文章目录 一,56-商品服务-API-三级分类-修改-拖拽功能完成二,57-商品服务-API-三级分类-修改-批量拖拽效果1,增加按钮2,多次拖拽一次保存完整代码 在构建商品服务API中的三级分类修改功能时,拖拽排序是一个直观且高效的…...
Shader入门精要总结(二)矩阵
1. 矩阵乘法 一个rn的矩阵A和一个nc的矩阵B相乘,它们的结果AB将会是一个rc大小的矩阵,不满足此规则不能相乘 矩阵乘法满足一些性质 矩阵乘法不满足交换律 即AB≠BA矩阵乘法满足结合律 (AB)CA(BC) 2. 特殊矩阵 方块矩阵 指行和列数目相等的矩阵&#…...
基于CentOS Stream 9平台安装MySQL Community Server 9.0.1 Innovation
1. 安装之前 1.1 查看系统版本 cat /etc/redhat-releaseCentOS Stream release 9 1.2 查看cpu架构 lscpu架构: x86_64 CPU 运行模式: 32-bit, 64-bit 2. 官网下载 https://dev.mysql.com/downloads/mysql/ 要多看看 官方9.0文档:https://d…...
正则采集器之五——商品匹配规则
需求设计 实现分析 系统通过访问URL得到html代码,通过正则表达式匹配html,通过反向引用来得到商品的标题、图片、价格、原价、id,这部分逻辑在java中实现。 匹配商品的正则做成可视化编辑,因为不同网站的结构不同,同…...
一键切换阿里yum源(包括其他系统repo镜像查找方法)
一键切换阿里yum源 示例命令其他系统repo镜像GitHub文档 示例命令 # 备份旧源 mv CentOS-Base.repo CentOS-Base.repo.bak # 添加新源(阿里镜像源) wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo其他系统repo镜像 这里的示例是用…...
Fiddler学习笔记
目录 前言 简介 原理 界面 前言 测试可以使用fiddler工具,通过抓包的方式修改前端参数和模拟后端返回,快速定位缺陷。 简介 Fiddler是HTTP协议调试代理工具,可以记录并检查所有客户端和服务器之间的HTTP和HTTPS请求,允许监视…...
【Vue3】watch 监视多种类型数据
【Vue3】watch 监视多种类型数据 背景简介开发环境开发步骤及源码 背景 随着年龄的增长,很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来,技术出身的人总是很难放下一些执念,遂将这些知识整理成文,以纪念曾经努力学习奋斗…...
【C++入门】虚函数与多态
文章目录 前言虚函数是什么?如何使用虚函数? 纯虚函数是什么?虚函数与普通函数的区别虚表虚表是什么?含有虚表的类内存结构图如何找到虚表的地址?示例代码代码解释 多态是什么?如何使用多态?为什…...
wpf中轮询显示图片
本文的需求是,在一个文件夹中,放一堆图片的集合,然后在wpf程序中,按照定时的方式,循序显示照片。 全部代码 1.声明一个PictureInfo类 namespace WpfApp1 {public class PictureInfo{public string? FileName { get; …...
CSA笔记9-磁盘管理(2)
分区挂载 挂载:将该文件系统中的内容与指定的目录关联起来,使得你可以通过该目录来访问文件系统中的文件和目录。 mount 命令用来挂载文件系统 #挂载/dev/sda1和/dev/sda2 [rootlocalhost ~]# mkdir test{1..2} [rootlocalhost ~]# ll test1 te…...
Python入门第三课
# 入门第三课 # 关键字 if and or in not in ! car g print(car g) print(car dd) if car ! hh:print("wlcome to here ") age 33 print(age 33) print(age 44) age1 44 if age >0 and age1 > 0:print("nihao") if age >0 or age1 > …...
java计算器,输入公式和对应变量的值
目标:最近想写个东西,本质就是一个计算器,我们可以输入公式(例如:ab),然后把公式的值(a:10,b:20)也输入进去。最后得到结果。核心:这个想法核心部分就是给一个…...
加密货币赋能跨境电商:PayPal供应链金融服务如何引领行业新趋势
跨境电商行业近年来呈现出爆发式增长,随着全球化贸易壁垒的降低和数字经济的快速发展,越来越多的商家和消费者跨越国界进行交易。根据eMarketer的数据,全球跨境电商交易额在2023年已超过4万亿美元,并预计在未来几年内仍将保持两位…...
redis面试(二)List链表数据
list 列表 我们总是说List为列表,其实在真正的数据结构来说,redis是自己基于c语言来实现的双向链表数据结构,主要的逻辑就是每个节点都可以指向下一个节点,这个结构就属于链表数组结构。 每个节点中的属性如下: type…...
SpringDataJPA(三):多表操作,复杂查询
一、Specifications动态查询 有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在Spring Data JPA中可以通过JpaSpecificationExecutor接口查询。相比JPQL,其优势是类型安全,更加的面向对象。 import …...
嵌入式硬件面试题集萃:从基础到进阶
基础问题 问题: 解释什么是微控制器,以及它与微处理器的区别。 答案: 微控制器是具有集成内存和输入/输出外设的微型计算机。与通用微处理器相比,微控制器通常用于控制特定应用,而不是执行通用计算任务。 问题: 什么是数字逻辑门,…...
easyui-datebox 只显示月份选择,默认开启月份,隐藏日期选择框
如果你使用 easyui-datebox 并希望隐藏日期选择框,只显示月份选择,可以通过一些自定义代码来实现。虽然 EasyUI 没有直接提供这种功能,但可以通过自定义 formatter 和 parser 方法,以及修改 onShowPanel 事件来实现这个功能。 以下…...
【数据结构】队列(链表实现 + 力扣 + 详解 + 数组实现循环队列 )
Hi~!这里是奋斗的明志,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 🌱🌱个人主页:奋斗的明志 🌱🌱所属专栏:数据结构 📚本系列文章为个人学…...
02 Go语言操作MySQL基础教程_20240729 课程笔记
概述 如果您没有Golang的基础,应该学习如下前置课程。 Golang零基础入门Golang面向对象编程Go Web 基础Go语言开发REST API接口_20240728 基础不好的同学每节课的代码最好配合视频进行阅读和学习,如果基础比较扎实,则阅读本教程巩固一下相…...
相交链表 - 力扣(LeetCode)C语言
160. 相交链表 - 力扣(LeetCode) (点击前面链接即可查看题目) 一、题目 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 图示两个链表在节点 c1 开始…...
【Python】基础学习技能提升代码样例3:JSON文本处理
对json的处理,无非是编码和解码两部分 编码:将python数据结构转换为json字符串解码: 将json字符串转换为python数据结构 另外,还有.json文件的读写 一、编码 json.dumps(obj, *, skipkeysFalse, ensure_asciiTrue, check_circularTrue, a…...
最新Yiso智云搜索引擎系统源码/开源PHP源码/修复版
源码简介: 最新Yiso智云搜索引擎系统源码/开源PHP源码/修复版。Yiso 是一个性能非常好的搜索引擎,不仅免费开源,还能当作收录网址的平台来用呢!只需要输入关键词,就能轻松找到相关的搜索结果内容。 1、Yiso 用的是自…...
Anconda 快速常用命令简洁版
目的:简单清楚的使用基本的conda 命令 可能需求 查看项目中的虚拟环境及依赖是否满足需求操作新环境来满足项目或者论文的实现 Anconda 常用命令 conda 查看基础命令1. 进入Anaconda 环境2. 查看版本3.查看有哪些虚拟环境4.激活虚拟环境5. 进入虚拟环境查看6. 退出…...
Android 系统启动动画
一、接着我们把 bootanimation.zip 动画文件 预制到 /system/media/ 目录下: 二、目录/system/media/bootanimation.zip PRODUCT_COPY_FILES \$(LOCAL_PATH)/bootanimation.zip:/system/media/bootanimation.zipPRODUCT_ARTIFACT_PATH_REQUIREMENT_WHITELIST \ /…...
解决antd打开modal时页面自动跳到顶部问题
问题原因:antd的样式中有一行,如下样式代码,这行代码导致了在本来有滚动条的页面底部触发modal弹出时,会自动滚动到页面顶部。 html {overflow-y: scroll; } 解决办法:删除这行代码、或者将html的overflow-y属性改成…...
什么是等保测评2.0,等保测评如何定级
在信息化时代,网络安全已成为国家安全的重要组成部分。为了应对日益复杂的网络安全形势,我国推出了网络安全等级保护制度,其中等保测评是评估信息系统安全防护能力的关键环节。本文将深入探讨等保2.0的测评流程和定级标准,以揭示其…...
【嵌入式英语教程--6】C语言中的数组与指针
C语言中的数组与指针 英文原文 Arrays and pointers are fundamental concepts in the C programming language. An array is a collection of elements of the same data type stored in contiguous memory locations. Arrays can be used to store and manipulate sequence…...
RocketMQ 中的同步发送
在现代分布式系统中,消息队列是实现异步通信和解耦的重要组件。Apache RocketMQ 是一款高性能、高吞吐量的分布式消息中间件,广泛应用于电商、金融等领域。本文将详细介绍 RocketMQ 中的同步发送,包括其原理、应用场景、代码示例及注意事项。…...
c语言指针2
文章目录 一、void * 指针二、const关键字1.const修饰变量2.const修饰指针变量2. 1 const放在*的右边2. 2 const放在*的左边2. 3 总结 三、指针的运算3. 1指针的加减运算3. 2 指针 - 指针3. 3 指针的关系运算 四、野指针4. 1 什么叫野指针?4. 1 野指针的成因4.1.1 指…...
十七、openCV教程 图像轮廓
一、图像轮廓 图像轮廓是具有相同颜色或灰度的连续点的曲线.轮廓在形状分析和物体的检测和识别中很有用。 轮廓的作用:.用于图形分析、物体的识别和检测 注意点: 为了检测的准确性,需要先对图像进行二值化或Canny操作。 画轮廓时会修改输入的图像,如…...
基于视觉的语义匹配见多了,那基于雷达的呢?
论文题目: LiDAR-based HD Map Localization using Semantic Generalized ICP with Road Marking Detection 论文作者: Yansong Gong, Xinglian Zhang, Jingyi Feng, Xiao He and Dan Zhang 作者单位:北京驭势科技有限公司 导读ÿ…...
01、爬虫学习入门
爬虫:通过编写程序,来获取获取互联网上的资源 需求:用程序模拟浏览器,输入一个网址,从该网址获取到资源或内容 一、入门程序 #使用urlopen来进行爬取 from urllib.request import urlopen url "http://www.ba…...
我与C语言二周目邂逅vlog——6.文件操作
1. 为什么使⽤⽂件? 如果没有⽂件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失 了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进⾏持久…...
Hugo 部署与自动更新(Git)
文章目录 Nginx部署Hugonginx.confhugo.conf Hugo自动更新Hugo自动更新流程添加访问令牌添加web hookrust实现自动更新接口 Nginx部署Hugo nginx.conf user nginx; worker_processes auto;error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;even…...
HTTP代理揭秘:这些场景你都用对了吗?
HTTP代理是网络中常见的一种工具,可以帮助我们提升网络安全性和隐私保护,优化网络访问速度。本文将详细介绍什么是HTTP代理及其适用的场景。 HTTP代理是介于客户端(如浏览器)和服务器之间的中间服务器。它接收客户端的HTTP请求&a…...