FFmpeg 自定义IO和格式转换
文章目录
- 自定义IO
- 音频重采样
- 图像格式转换
自定义IO
通常解封装时,当调用avformat_open_input和avformat_find_stream_info时,FFmpeg内部会自动读取文件内容来查找信息。除此之外,我们也可以自定义IO,我们只要提供一个自定义的读/写函数提供给avformat_open_input函数作为回调函数,这样的话,当调用前面这两个函数时,FFmpeg内部通过回调函数来提供数据。
头文件包含新增
#include <libavformat/avio.h> // 输入输出
#include <libavutil/file.h> // 文件操作
struct BufferData
{
uint8_t *buffer; // 缓冲区
size_t buffer_size; // 缓冲区大小
};
int read_packet(void *opaque, uint8_t *buf, int buf_size) // 读取数据包
{
BufferData *buffer_data = (BufferData *)opaque; // 获取缓冲区数据结构
if (buffer_data->buffer_size > 0)
{
int size = std::min(buf_size, (int)buffer_data->buffer_size); // 获取要读取的字节数
memcpy(buf, buffer_data->buffer, size); // 读取数据
buffer_data->buffer += size; // 更新缓冲区指针
buffer_data->buffer_size -= size; // 更新缓冲区大小
return size;
}
return -1;
}
void Widget::customUI()
{
AVFormatContext *fmt_ctx = nullptr; // 分配一个格式上下文
AVIOContext *avio_ctx = nullptr; // 分配一个IO上下文
uint8_t *buffer = nullptr; // 分配一个缓冲区
uint8_t *avio_ctx_buffer = nullptr; // 分配一个IO上下文缓冲区
size_t buffer_size = 0, avio_ctx_buffer_size = 4096; // 缓冲区大小
const char *input_file = "../../source/audio.mp3"; // 输入文件名
int ret = av_file_map(input_file, &buffer, &buffer_size, 0, NULL); // 打开输入文件,将文件内容映射到缓冲区
if (ret < 0)
{
qDebug() << "打开输入文件失败";
return;
}
BufferData buffer_data; // 创建缓冲区数据结构
buffer_data.buffer = buffer;
buffer_data.buffer_size = buffer_size;
fmt_ctx = avformat_alloc_context(); // 分配一个格式上下文
if (!fmt_ctx)
{
qDebug() << "分配格式上下文失败";
return;
}
avio_ctx_buffer = (uint8_t *)av_malloc(avio_ctx_buffer_size); // 分配一个IO上下文缓冲区
avio_ctx = avio_alloc_context(avio_ctx_buffer, avio_ctx_buffer_size, 0, &buffer_data, read_packet, nullptr, nullptr); // 分配一个IO上下文
if (!avio_ctx)
{
qDebug() << "分配IO上下文失败";
return;
}
fmt_ctx->pb = avio_ctx; // 将IO上下文绑定到格式上下文
ret = avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr); // 打开输入文件
if (ret < 0)
{
qDebug() << "打开输入文件失败";
return;
}
avformat_find_stream_info(fmt_ctx, nullptr); // 查找流信息
av_dump_format(fmt_ctx, 0, input_file, 0); // 打印输入文件信息
avio_context_free(&avio_ctx); // 释放IO上下文
avformat_close_input(&fmt_ctx); // 关闭输入文件
avformat_free_context(fmt_ctx); // 释放格式上下文
av_file_unmap(buffer, buffer_size); // 释放缓冲区
qDebug() << "音频播放结束";
}
音频重采样
音频的播放是依赖于设备的,不同的设备它支持的音频格式不一样,要想在设备上播放就必须将音频格式转换成与设备对应的格式。音频在做编码的时候也是一样的,不同的音频编码器小不同的音频格式。
将一种音频格式转换成另一种音频格式,这个转换过程我们称之为重采样,重采样就是改变音频的采样率、采样格式、声道数参数。
一帧音频的数据量(字节)
=
c
h
a
n
n
e
l
数
×
n
b
_
s
a
m
p
l
e
s
样本数
×
采样格式
一帧音频的数据量(字节)=channel数\times nb\_samples样本数 \times 采样格式
一帧音频的数据量(字节)=channel数×nb_samples样本数×采样格式
AAC格式音频的样本数量一般是1024,MP3格式音频样本数量是1152。
一帧音频的播放时间 = 样本数量 ÷ 采样率 一帧音频的播放时间 = 样本数量 \div 采样率 一帧音频的播放时间=样本数量÷采样率
在做重采样时,重采样之前的播放时长和重采样后的播放时长是相等的。因此样本数量的计算,我们可以通过如下公式计算
源格式样本数
÷
源采样率
=
目标格式样本数量
÷
目标采样率
源格式样本数\div 源采样率 = 目标格式样本数量 \div 目标采样率
源格式样本数÷源采样率=目标格式样本数量÷目标采样率
如果出现小数,我们需要对其进行向上取整。
代码如下:
#include <libswresample/swresample.h> // 音频重采样头文件
void Widget::audioReSample()
{
const char *input_file = "../../source/audio.aac"; // 输入文件名
const char *output_file = "../../output/audioResample.pcm"; // 输出文件名
AVFormatContext *fmt_ctx = nullptr; // 分配一个格式上下文
int ret = avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr); // 打开输入文件
if (ret < 0)
{
qDebug() << "打开输入文件失败";
return;
}
avformat_find_stream_info(fmt_ctx, nullptr); // 查找流信息
int audio_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); // 查找音频流索引
if (audio_stream_index < 0)
{
qDebug() << "找不到音频流";
return;
}
const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[audio_stream_index]->codecpar->codec_id); // 查找解码器
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); // 分配一个解码器上下文
avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[audio_stream_index]->codecpar); // 将解码器参数复制到解码器上下文
ret = avcodec_open2(codec_ctx, codec, nullptr); // 打开解码器
if (ret < 0)
{
qDebug() << "打开解码器失败";
return;
}
SwrContext *swr_ctx = nullptr; // 创建重采样上下文
ret = swr_alloc_set_opts2(&swr_ctx, &codec_ctx->ch_layout, AV_SAMPLE_FMT_S16, 44100,
&codec_ctx->ch_layout, codec_ctx->sample_fmt, codec_ctx->sample_rate, 0, nullptr); // 设置重采样参数
if (ret < 0)
{
qDebug() << "设置重采样参数失败";
return;
}
ret = swr_init(swr_ctx); // 初始化重采样上下文
if (ret < 0)
{
qDebug() << "初始化重采样上下文失败";
return;
}
std::ofstream output_file_stream(output_file, std::ios::binary); // 打开输出文件
if (!output_file_stream.is_open())
{
qDebug() << "打开输出文件失败";
return;
}
AVPacket packet; // 创建一个包
AVFrame *frame = av_frame_alloc(); // 创建一个帧
uint8_t **out_buffer = nullptr; // 输出缓冲区
int out_buffer_size = 0; // 输出缓冲区大小
int64_t out_Samples = av_rescale_rnd(swr_get_delay(swr_ctx, codec_ctx->sample_rate) + codec_ctx->frame_size, 44100, codec_ctx->sample_rate, AV_ROUND_UP); // 计算输出样本数
av_samples_alloc_array_and_samples(&out_buffer, &out_buffer_size, 2, out_Samples, AV_SAMPLE_FMT_S16, 0); // 分配输出缓冲区
while (av_read_frame(fmt_ctx, &packet) >= 0) // 读取一个包
{
if (packet.stream_index == audio_stream_index) // 如果是音频包
{
ret = avcodec_send_packet(codec_ctx, &packet); // 发送一个包到解码器
if (ret < 0)
{
return;
}
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame); // 从解码器接收一个帧
if (ret < 0)
break;
swr_convert(swr_ctx, out_buffer, out_Samples, (const uint8_t **)frame->data, frame->nb_samples); // 重采样
int out_buffer_size = av_samples_get_buffer_size(nullptr, 2, frame->nb_samples, AV_SAMPLE_FMT_S16, 1);
if (out_buffer_size < 0)
return;
output_file_stream.write((char *)out_buffer[0], out_buffer_size); // 写入输出文件
av_frame_unref(frame); // 释放帧
}
}
av_packet_unref(&packet); // 释放包
}
ret = avcodec_send_packet(codec_ctx, nullptr); // 发送结束标志到解码器
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame); // 从解码器接收一个帧
if (ret < 0)
{
break;
}
swr_convert(swr_ctx, out_buffer, out_Samples, (const uint8_t **)frame->data, frame->nb_samples); // 重采样
int out_buffer_size = av_samples_get_buffer_size(nullptr, 2, frame->nb_samples, AV_SAMPLE_FMT_S16, 1);
if (out_buffer_size < 0)
return;
output_file_stream.write((char *)out_buffer[0], out_buffer_size); // 写入输出文件
}
av_frame_free(&frame); // 释放帧
av_free(out_buffer[0]); // 释放输出缓冲区
output_file_stream.close(); // 关闭输出文件
avformat_close_input(&fmt_ctx); // 关闭输入文件
avcodec_free_context(&codec_ctx); // 释放解码器上下文
swr_free(&swr_ctx); // 释放重采样上下文
avformat_free_context(fmt_ctx); // 释放格式上下文
qDebug() << "音频转换完成";
}
图像格式转换
大多数的视频文件中保存的图像格式一般都是YUV格式,而这种格式的图像我们无法进行直接显示,需要先将其转成RGB格式才能够显示,如果我们还想要对图像进行放大或者缩小,那么FFmpeg的libswscale库给我们提供了这些功能。
头文件添加
#include <libswscale/swscale.h> // 视频重采样
#include <libavutil/imgutils.h> // 图像工具
void Widget::ImageReSample()
{
const char *input_file = "../../source/video-30fps.mp4"; // 输入文件路径
const char *output_file = "../../output/out.rgb"; // 输出文件路径
AVFormatContext *fmt_ctx = nullptr; // 格式上下文
int ret = avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr); // 打开输入文件
if (ret < 0)
{
qDebug() << "无法打开输入文件";
return;
}
ret = avformat_find_stream_info(fmt_ctx, nullptr); // 查找流信息
if (ret < 0)
{
qDebug() << "无法查找流信息";
return;
}
int video_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); // 查找视频流
if (video_stream_index < 0)
{
qDebug() << "无法找到视频流";
return;
}
const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_index]->codecpar->codec_id); // 查找解码器
if (!codec)
{
qDebug() << "无法找到解码器";
return;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); // 分配解码器上下文
if (!codec_ctx)
{
qDebug() << "无法分配解码器上下文";
return;
}
ret = avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar); // 将流参数复制到解码器上下文
if (ret < 0)
{
qDebug() << "无法复制流参数到解码器上下文";
return;
}
ret = avcodec_open2(codec_ctx, codec, nullptr); // 打开解码器
if (ret < 0)
{
qDebug() << "无法打开解码器";
return;
}
SwsContext *sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
640, 480, AV_PIX_FMT_RGB24, SWS_BICUBIC, nullptr, nullptr, nullptr); // 创建重采样上下文
if (!sws_ctx)
{
qDebug() << "无法创建重采样上下文";
return;
}
uint8_t *out_buffer[4]; // 输出缓冲区
int out_buffer_size; // 输出缓冲区大小
int lineSize[4]; // 行大小
out_buffer_size = av_image_alloc(out_buffer, lineSize, 640, 480, AV_PIX_FMT_RGB24, 1); // 分配输出缓冲区
if (out_buffer_size < 0)
{
qDebug() << "无法分配输出缓冲区";
return;
}
std::ofstream output(output_file, std::ios::binary); // 打开输出文件
if (!output.is_open())
{
qDebug() << "无法打开输出文件";
return;
}
AVPacket *packet = av_packet_alloc(); // 分配AVPacket
AVFrame *frame = av_frame_alloc(); // 分配AVFrame
while (av_read_frame(fmt_ctx, packet) >= 0) // 读取帧
{
if (packet->stream_index == video_stream_index) // 如果是视频帧
{
ret = avcodec_send_packet(codec_ctx, packet); // 发送数据包到解码器
if (ret < 0)
break;
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame); // 接收解码后的帧
if (ret < 0)
break;
sws_scale(sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0, frame->height, out_buffer, lineSize); // 重采样
output.write((char *)out_buffer[0], out_buffer_size); // 写入文件
av_frame_unref(frame); // 释放AVFrame
}
}
av_packet_unref(packet); // 释放AVPacket
}
ret = avcodec_send_packet(codec_ctx, nullptr); // 发送空包结束解码
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame); // 接收解码后的帧
if (ret < 0)
break;
sws_scale(sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0, frame->height, out_buffer, lineSize); // 重采样
output.write((char *)out_buffer[0], out_buffer_size); // 写入文件
av_frame_unref(frame); // 释放AVFrame
}
av_packet_free(&packet); // 释放AVPacket
av_frame_free(&frame); // 释放AVFrame
av_free(out_buffer); // 释放输出缓冲区
output.close(); // 关闭输出文件
avcodec_free_context(&codec_ctx); // 释放解码器上下文
avformat_close_input(&fmt_ctx); // 关闭输入文件
avformat_free_context(fmt_ctx); // 释放格式上下文
qDebug() << "视频图片转换完成";
}