FFmpeg 自定义IO和格式转换

文章目录

自定义IO

通常解封装时,当调用avformat_open_input和avformat_find_stream_info时,FFmpeg内部会自动读取文件内容来查找信息。除此之外,我们也可以自定义IO,我们只要提供一个自定义的读/写函数提供给avformat_open_input函数作为回调函数,这样的话,当调用前面这两个函数时,FFmpeg内部通过回调函数来提供数据。

头文件包含新增

#include <libavformat/avio.h> // 输入输出

#include <libavutil/file.h> // 文件操作

cpp 复制代码
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> // 音频重采样头文件

cpp 复制代码
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> // 图像工具

cpp 复制代码
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() << "视频图片转换完成";
}
相关推荐
daidaidaiyu1 天前
FFmpeg 关键的结构体
c++·ffmpeg
扶尔魔ocy2 天前
【QT window】ffmpeg实现录音功能之无损格式--PCM
ffmpeg·pcm
止礼2 天前
FFmpeg8.0.1 源代码的深入分析
ffmpeg
小曾同学.com2 天前
音视频中的“透传”与“DTS音频”
ffmpeg·音视频·透传·dts
vivo互联网技术2 天前
数字人动画云端渲染方案
前端·ffmpeg·puppeteer·web3d
止礼2 天前
FFmpeg8.0.1 编解码流程
ffmpeg
qs70162 天前
c直接调用FFmpeg命令无法执行问题
c语言·开发语言·ffmpeg
止礼2 天前
FFmpeg8.0.1 Mac环境 CMake本地调试配置
macos·ffmpeg
简鹿视频2 天前
视频转mp4格式具体作步骤
ffmpeg·php·音视频·实时音视频