1 解封装流程
1.1 什么解封装
封装的逆向操作:封装是把音频流、视频流、字幕流等不同成分按一定规则组合成视频文件(如 MP4、FLV ) ,复用器负责此过程。解封装则相反,是用解复用器(针对 MP4、FLV 等格式有对应解复用器)将视频文件再按规则拆分回音频流、视频流、字幕流等各个成分 。
流索引标识:解封装后,为区分各流,会给音频流、视频流等分配索引,如图中 audio_index = 1 表示音频流索引为 1 ,video_index = 0 表示视频流索引为 0 。程序后续可依这些索引分别处理对应流,比如解码视频流、播放音频流等 。
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)
1.2 实验
第一部分: :ffmpeg -i in_filename # 打开流
cpp
const char *default_filename = "believe.mp4";
char *in_filename = NULL;
if(argv[1] == NULL)
{
in_filename = default_filename;
}
else
{
in_filename = argv[1];
}
AVFormatContext *ifmt_ctx = NULL//初始化上下文
int ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL);//头部信息
if (ret < 0) //如果打开媒体文件失败,打印失败原因
{
char buf[1024] = { 0 };
av_strerror(ret, buf, sizeof(buf) - 1);
printf("open %s failed:%s\n", in_filename, buf);
goto failed;
}
ret = avformat_find_stream_info(ifmt_ctx, NULL);// 流的信息
if (ret < 0) //如果打开媒体文件失败,打印失败原因
{
char buf[1024] = { 0 };
av_strerror(ret, buf, sizeof(buf) - 1);
printf("avformat_find_stream_info %s failed:%s\n", in_filename, buf);
goto failed;
}
av_dump_format(ifmt_ctx, 0, in_filename, 0);
printf_s("\n==== av_dump_format in_filename:%s ===\n", in_filename);
av_dump_format(ifmt_ctx, 0, in_filename, 0);
printf_s("\n==== av_dump_format finish =======\n\n");
// url: 调用avformat_open_input读取到的媒体文件的路径/名字
printf("media name:%s\n", ifmt_ctx->url);
// nb_streams: nb_streams媒体流数量
printf("stream number:%d\n", ifmt_ctx->nb_streams);
// bit_rate: 媒体文件的码率,单位为bps
printf("media average ratio:%lldkbps\n",(int64_t)(ifmt_ctx->bit_rate/1024));
// 时间
printf("total duration: %02d:%02d:%02d\n", hour, minute, second);
printf("\n");
第二部分:分析流的信息 : 也是ffmpeg -i in_filename# 打开流
cpp
ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (ret >= 0) {
audioindex = ret;
AVStream *in_stream = ifmt_ctx->streams[audioindex];
printf("----- Audio info:\n");
// index: 每个流成分在ffmpeg解复用分析后都有唯一的index作为标识
printf("index:%d\n", in_stream->index);
// sample_rate: 音频编解码器的采样率,单位为Hz
printf("samplerate:%dHz\n", in_stream->codecpar->sample_rate);
// codecpar->format: 音频采样格式
if (AV_SAMPLE_FMT_FLTP == in_stream->codecpar->format) {
printf("sampleformat:AV_SAMPLE_FMT_FLTP\n");
} else if (AV_SAMPLE_FMT_S16P == in_stream->codecpar->format) {
printf("sampleformat:AV_SAMPLE_FMT_S16P\n");
}
// channels: 音频信道数目
printf("channel number:%d\n", in_stream->codecpar->channels);
// codec_id: 音频压缩编码格式
if (AV_CODEC_ID_AAC == in_stream->codecpar->codec_id) {
printf("audio codec:AAC\n");
} else if (AV_CODEC_ID_MP3 == in_stream->codecpar->codec_id) {
printf("audio codec:MP3\n");
} else {
printf("audio codec_id:%d\n", in_stream->codecpar->codec_id);
}
// 音频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的
if (in_stream->duration != AV_NOPTS_VALUE) {
int duration_audio = (in_stream->duration) * av_q2d(in_stream->time_base);
//将音频总时长转换为时分秒的格式打印到控制台上
printf("audio duration: %02d:%02d:%02d\n",
duration_audio / 3600, (duration_audio % 3600) / 60, (duration_audio % 60));
} else {
printf("audio duration unknown");
}
}
----- Audio info:
index:1
samplerate:48000Hz
sampleformat:AV_SAMPLE_FMT_FLTP
channel number:2
audio codec:AAC
audio duration: 00:03:42
分别对应 音频流索引 采样率 采样精度 声道 编码器 时间
cpp
ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (ret >= 0) {
videoindex = ret;
AVStream *in_stream = ifmt_ctx->streams[videoindex];
printf("----- Video info:\n");
printf("index:%d\n", in_stream->index);
// avg_frame_rate: 视频帧率,单位为fps,表示每秒出现多少帧
printf("fps:%lffps\n", av_q2d(in_stream->avg_frame_rate));
if (AV_CODEC_ID_MPEG4 == in_stream->codecpar->codec_id) //视频压缩编码格式
{
printf("video codec:MPEG4\n");
} else if (AV_CODEC_ID_H264 == in_stream->codecpar->codec_id) //视频压缩编码格式
{
printf("video codec:H264\n");
} else {
printf("video codec_id:%d\n", in_stream->codecpar->codec_id);
}
// 视频帧宽度和帧高度
printf("width:%d height:%d\n", in_stream->codecpar->width,
in_stream->codecpar->height);
//视频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的
if (in_stream->duration != AV_NOPTS_VALUE) {
int duration_video = (in_stream->duration) * av_q2d(in_stream->time_base);
printf("video duration: %02d:%02d:%02d\n",
duration_video / 3600,
(duration_video % 3600) / 60,
(duration_video % 60)); //将视频总时长转换为时分秒的格式打印到控制台上
} else {
printf("video duration unknown");
}
printf("\n");
}
----- Video info:
index:0
fps:14.464607fps
video codec:H264
width:1920 height:1080
video duration: 00:03:42
视频流索引 帧率 编码器 分辨率 时长
第三部分:ffmpeg -i input.mp4 -vn -c:a aac output.aac
cpp
printf("audio profile:%d, FF_PROFILE_AAC_LOW:%d\n",
ifmt_ctx->streams[audio_index]->codecpar->profile,
FF_PROFILE_AAC_LOW);
if(ifmt_ctx->streams[audio_index]->codecpar->codec_id != AV_CODEC_ID_AAC)
{
printf("the media file no contain AAC stream, it's codec_id is %d\n",
ifmt_ctx->streams[audio_index]->codecpar->codec_id);
goto failed;
}
// 读取媒体文件,并把aac数据帧写入到本地文件
while(av_read_frame(ifmt_ctx, &pkt) >=0 )
{
if(pkt.stream_index == audio_index)
{
char adts_header_buf[7] = {0};
adts_header(adts_header_buf, pkt.size,
ifmt_ctx->streams[audio_index]->codecpar->profile,
ifmt_ctx->streams[audio_index]->codecpar->sample_rate,
ifmt_ctx->streams[audio_index]->codecpar->channels);
fwrite(adts_header_buf, 1, 7, aac_fd); // 写adts header , ts流不适用,ts流分离出来的packet带了adts header
len = fwrite( pkt.data, 1, pkt.size, aac_fd); // 写adts data
if(len != pkt.size)
{
av_log(NULL, AV_LOG_DEBUG, "warning, length of writed data isn't equal pkt.size(%d, %d)\n",
len,
pkt.size);
}
}
av_packet_unref(&pkt);
}
写入aac的head和数据包
第四部分 :提取视频流 ffmpeg -i nput.mp4 -an -c:v libx264 output.h264
cpp
pkt = av_packet_alloc();
av_init_packet(pkt);
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);
file_end = 0;
while (0 == file_end)
{
if((ret = av_read_frame(ifmt_ctx, pkt)) < 0)
{
// 没有更多包可读
file_end = 1;
printf("read file end: ret:%d\n", ret);
}
if(ret == 0 && pkt->stream_index == videoindex)
{
#if 1
int input_size = pkt->size;
int out_pkt_count = 0;
if (av_bsf_send_packet(bsf_ctx, pkt) != 0) // bitstreamfilter内部去维护内存空间
{
av_packet_unref(pkt); // 你不用了就把资源释放掉
continue; // 继续送
}
av_packet_unref(pkt); // 释放资源
while(av_bsf_receive_packet(bsf_ctx, pkt) == 0)
{
out_pkt_count++;
// printf("fwrite size:%d\n", pkt->size);
size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
if(size != pkt->size)
{
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
}
if(out_pkt_count >= 2)
{
printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
input_size, out_pkt_count);
}
#else // TS流可以直接写入
size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
if(size != pkt->size)
{
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
#endif
使用了一个过滤器 为 AVBitStreamFilter 要为h264一个annexb模式服务
2 解码
上面我们获取了 两个流 分别为aac的音频流和h264的视频流,现在我们要对这两个流解码
2.1 函数
cpp
const AVCodec *codec;
AVCodecContext *codec_ctx= NULL;
AVCodecParserContext *parser = NULL
// 查找解码器
codec = avcodec_find_decoder(audio_codec_id); // AV_CODEC_ID_AAC
if (!codec) {
fprintf(stderr, "Codec not found\n");
exit(1);
}
// 获取裸流的解析器 AVCodecParserContext(数据) + AVCodecParser(方法)
parser = av_parser_init(codec->id);
if (!parser) {
fprintf(stderr, "Parser not found\n");
exit(1);
}
// 分配codec上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
fprintf(stderr, "Could not allocate audio codec context\n");
exit(1);
}
- codec:编解码器
- AVCodecContext :编解码器上下文
基本概念
编解码器上下文(Codec Context)本质上是一个数据结构,它用来存储编解码器在工作过程中所需要的各种参数、状态信息以及中间数据。简单来说,它就是编解码器的 "工作环境",编解码器依据上下文中的这些信息来完成编码或者解码的操作。
作用参数配置:编解码器上下文可以对编解码器的各种参数进行配置。以视频编解码器为例,这些参数可能包括视频的分辨率、帧率、比特率、像素格式等;对于音频编解码器,可能涉及采样率、声道数、采样格式等。通过对这些参数的合理配置,能够让编解码器根据具体需求进行工作。
状态管理:它会记录编解码器在运行过程中的状态信息。例如,在解码过程中,可能会记录当前解码到的帧号、是否遇到错误等信息;在编码过程中,可能会记录编码进度、已经编码的帧数等。
数据传递:编解码器上下文还承担着在不同模块之间传递数据的任务。比如,在解码时,输入的编码数据会通过上下文传递给解码器;解码完成后,解码后的原始数据也会通过上下文传递给后续的处理模块。
-
AVCodecParserContext:⽤于解析输⼊的数据流并把它分成⼀帧⼀帧的压缩编码数据。⽐较形象的说法就是把⻓⻓的⼀段连续的数据"切割"成⼀段段的数据。
-
解码
- avcodec_send_packet():
函数int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
作用:⽀持将裸流数据包送给解码器 - avcodec_receive_frame
函数 :int avcodec_receive_frame ( AVCodecContext * avctx, AVFrame * frame )
作⽤ :从解码器返回已解码的输出数据。
返回值 :AVERROR(EAGAIN): 该状态下没有帧输出,需要使⽤avcodec_send_packet发送新的packet到解码器
AVERROR_EOF: 解码器已经被完全刷新,不再有输出帧
- avcodec_send_packet():
cpp
static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,
FILE *outfile)
{
int i, ch;
int ret, data_size;
/* send the packet with the compressed data to the decoder */
ret = avcodec_send_packet(dec_ctx, pkt);
if(ret == AVERROR(EAGAIN))
{
fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
}
else if (ret < 0)
{
fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
av_get_err(ret), pkt->size);
// exit(1);
return;
}
/* read all the output frames (infile general there may be any number of them */
while (ret >= 0)
{
// 对于frame, avcodec_receive_frame内部每次都先调用
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0)
{
fprintf(stderr, "Error during decoding\n");
exit(1);
}
data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
if (data_size < 0)
{
/* This should not occur, checking just for paranoia */
fprintf(stderr, "Failed to calculate data size\n");
exit(1);
}
static int s_print_format = 0;
if(s_print_format == 0)
{
s_print_format = 1;
print_sample_format(frame);
}
/**
P表示Planar(平面),其数据格式排列方式为 :
LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每个LLLLLLRRRRRR为一个音频帧)
而不带P的数据格式(即交错排列)排列方式为:
LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每个LR为一个音频样本)
播放范例: ffplay -ar 48000 -ac 2 -f f32le believe.pcm
*/
for (i = 0; i < frame->nb_samples; i++)
{
for (ch = 0; ch < dec_ctx->channels; ch++) // 交错的方式写入, 大部分float的格式输出
fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile);
}
}
是解一个包中的帧的样本点
cpp
static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,
FILE *outfile)
{
int ret;
/* send the packet with the compressed data to the decoder */
ret = avcodec_send_packet(dec_ctx, pkt);//发包
if(ret == AVERROR(EAGAIN))//错误处理
{
fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
}
else if (ret < 0)
{
fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
av_get_err(ret), pkt->size);
return;
}
/* read all the output frames (infile general there may be any number of them */
while (ret >= 0)
{
// 对于frame, avcodec_receive_frame内部每次都先调用
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0)
{
fprintf(stderr, "Error during decoding\n");
exit(1);
}
static int s_print_format = 0;
if(s_print_format == 0)
{
s_print_format = 1;
print_video_format(frame);
}
// 一般H264默认为 AV_PIX_FMT_YUV420P, 具体怎么强制转为 AV_PIX_FMT_YUV420P 在音视频合成输出的时候讲解
// frame->linesize[1] 对齐的问题
// 正确写法 linesize[]代表每行的字节数量,所以每行的偏移是linesize[]
for(int j=0; j<frame->height; j++)
fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, outfile);
for(int j=0; j<frame->height/2; j++)
fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, outfile);
for(int j=0; j<frame->height/2; j++)
fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, outfile);
// 错误写法 用source.200kbps.766x322_10s.h264测试时可以看出该种方法是错误的
// 写入y分量
// fwrite(frame->data[0], 1, frame->width * frame->height, outfile);//Y
// // 写入u分量
// fwrite(frame->data[1], 1, (frame->width) *(frame->height)/4,outfile);//U:宽高均是Y的一半
// // 写入v分量
// fwrite(frame->data[2], 1, (frame->width) *(frame->height)/4,outfile);//V:宽高均是Y的一半
}
}
是解一个包中的 帧中的yuv
3 自定义io
cpp
AVIOContext *avio_alloc_context(
unsigned char *buffer,
int buffer_size,
int write_flag,
void *opaque,
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
int64_t (*seek)(void *opaque, int64_t offset, int whence)
);
-
unsigned char *buffer
指向预先分配好的缓冲区的指针,该缓冲区用于存储从数据源读取的数据(读取模式)或要写入数据源的数据(写入模式)。在调用此函数之前,你需要使用 av_malloc 等内存分配函数为其分配足够的内存空间。
-
int buffer_size
缓冲区的大小,以字节为单位。该值指定了 buffer 所指向的内存区域的大小。
-
int write_flag
一个布尔值,用于指示 AVIOContext 的操作模式。
- 0 表示只读模式,即 AVIOContext 仅用于从数据源读取数据。
- 非零值(通常为 1)表示可写模式,即 AVIOContext 用于向数据源写入数据。
-
void *opaque
一个通用指针,可传递任意用户数据,通常是与数据源相关的上下文信息,如文件指针、网络套接字等。这个指针会被传递给后续的 read_packet、write_packet 和 seek 回调函数,以便在这些函数中使用。
-
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size)
一个指向读取回调函数的指针,用于从数据源读取数据。当 AVIOContext 需要从数据源读取数据时,会调用这个函数。
函数参数:
void *opaque:即前面传入的 opaque 指针,可用于获取数据源的上下文信息。
uint8_t *buf:指向用于存储读取数据的缓冲区。
int buf_size:缓冲区的大小,即最多可读取的字节数。
函数返回值:成功读取的字节数,如果返回值小于 0,则表示发生了错误。
-
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size)
一个指向写入回调函数的指针,用于向数据源写入数据。当 AVIOContext 需要向数据源写入数据时,会调用这个函数。
函数参数与 read_packet 类似,只是 buf 中的数据是要写入数据源的数据。
函数返回值:成功写入的字节数,如果返回值小于 0,则表示发生了错误。
-
int64_t (*seek)(void *opaque, int64_t offset, int whence)
一个指向定位回调函数的指针,用于在数据源中移动读写位置。当 AVIOContext 需要在数据源中定位到特定位置时,会调用这个函数。
函数参数:
void *opaque:同样是前面传入的 opaque 指针。
int64_t offset:相对于 whence 指定位置的偏移量。
int whence:定位的起始位置,取值与标准 C 库中的 fseek 函数的 whence 参数类似,常见取值有 SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。
函数返回值:新的读写位置,如果返回值小于 0,则表示发生了错误。
cpp
uint8_t *io_buffer = av_malloc(BUF_SIZE);
AVIOContext *avio_ctx = avio_alloc_context(io_buffer, BUF_SIZE, 0, (void *)in_file, \
read_packet, NULL, NULL);
AVFormatContext *format_ctx = avformat_alloc_context();
format_ctx->pb = avio_ctx;
当调用这个的时候
ret = av_read_frame(format_ctx, packet); //使用自定义io