ffmpeg录制视频功能

本文目录

1.环境配置

下载并安装FFmpeg库 在Windows上 从FFmpeg官方网站下载预编译的FFmpeg库: 解压下载的文件,并记下解压后的路径。
FFmpeg下载(windows版本)_libijkffmpeg.so 32位下载-CSDN博客
记住一定下载是32位,否则使用msvc32位时无法进行链接对应库的。
将下载后的 static 中动态库拿出来,将shared的include与lib拿出来,放在当前项目目录下。

#FFmpeg库的路径
INCLUDEPATH += /path/to/ffmpeg/include
LIBS += -L/path/to/ffmpeg/lib -lavcodec -lavformat -lavutil -lswscale
#如果你在Windows上使用预编译的FFmpeg库,可能还需要添加以下内容
LIBS += -lavdevice -lavfilter -lswresample -lpostproc
重要的一点:指定可执行程序的路径,这里就是我们动态库的目录下:
DESTDIR =$$PWD/bin    #指定可执行的生成路径--到库的位置

2.ffmpeg编解码的主要逻辑:

  1. 初始化FFmpeg库在使用FFmpeg之前,需要初始化FFmpeg库。
dart 复制代码
*extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
}
void initializeFFmpeg() {
    av_register_all();
    avcodec_register_all();
    avformat_network_init();
}*

首先,在使用ffmpeg的上下文,音视频流,编解码...之前,都需要先进性初始化,

在使用FFmpeg进行多媒体处理时,初始化库是非常重要的步骤。以下是对 av_register_all、avcodec_register_all 和 avformat_network_init 三个初始化函数的详细解释:

  1. av_register_all()

    含义

    av_register_all 函数用于注册所有的编解码器、文件格式和协议。它是FFmpeg库的初始化函数之一,确保在使用FFmpeg的任何功能之前,所有的编解码器和格式都已注册。

    2.av_register_all();

    作用:

    注册所有的编解码器(如H.264、AAC等)。

    注册所有的文件格式(如MP4、AVI等)。

    注册所有的协议(如HTTP、RTMP等)。

  2. avcodec_register_all()

    avcodec_register_all 函数用于注册所有的编解码器。虽然 av_register_all 已经包含了这个步骤,但在某些情况下,你可能只需要注册编解码器而不需要注册其他组件。

    avcodec_register_all();

    主要作用:注册所有的编解码器(如H.264、AAC等)。

  3. avformat_network_init()

    avformat_network_init 函数用于初始化网络组件。它在使用任何网络协议(如HTTP、RTMP等)之前调用,以确保网络功能已正确初始化。

    主要作用:初始化网络组件,确保网络协议(如HTTP、RTMP等)可以正常使用。

    一般我们在构造进行库的初始化,之后,我们回去在一个初始化接口中去进行ffmpeg的准备工作:

    第一个准备工作就是去设置编解码器:

  4. 设置编解码器 ---参数设置

dart 复制代码
// 设置选项
AVDictionary *options = nullptr;
av_dict_set(&options, "rtbufsize", "100M", 0); // 设置实时缓冲区大小
av_dict_set(&options, "framerate", "30", 0);  // 设置帧率为30 FPS
//在打开输入文件,打开编码器,写入输出文件时可以设置这些参数

通过 AVDictionary 设置选项来配置输入设备的参数,例如帧率,缓冲区大小,防止在写入时崩溃。

初始化

dart 复制代码
主要是对与FFmpeg的以下成员进行设置初始化:
AVFormatContext)格式上下文(用于管理输出文件的格式信息
 AVCodecContext:FFmpeg的编解码器上下文,用于管理视频编码器的参数和状态
AVStream :FFmpeg的视频流,用于表示输出文件中的视频流
SwsContext :FFmpeg的图像转换上下文,用于将捕获的图像转换为编码器需要的格式
 AVFrame :FFmpeg的视频帧,用于存储要编码的视频数据
int frame:帧计数器,用于跟踪已捕获的帧数

常见的资源设置有如下这些:

AVCodecContext

资源:AVCodecContext 是编码器或解码器的上下文,包含了编解码器的所有状态信息和参数。

释放函数:avcodec_free_context

  1. AVFrame

    资源:AVFrame 用于存储解码后的帧数据或编码前的帧数据。

    释放函数:av_frame_free

  2. SwsContext

    资源:SwsContext 是用于图像缩放和像素格式转换的上下文。

    释放函数:sws_freeContext

  3. AVFormatContext

    资源:AVFormatContext 是多媒体文件的上下文,包含了文件格式、流信息等。

    释放函数:avformat_free_context

  4. AVPacket

    资源:AVPacket 用于存储编码后的数据包。

    释放函数:av_packet_unref

  5. AVCodecParameters

    资源:AVCodecParameters 用于存储编解码器的参数。

    释放函数:avcodec_parameters_free

  6. AVIOContext

    资源:AVIOContext 是用于输入输出操作的上下文。

    释放函数:avio_context_free

  7. AVFilterGraph

    资源:AVFilterGraph 是用于音视频过滤操作的图。

    释放函数:avfilter_graph_free

  8. AVFilterContext

    资源:AVFilterContext 是用于音视频过滤操作的上下文。

    释放函数:avfilter_free

  9. AVDictionary

    资源:AVDictionary 是用于存储键值对的字典,常用于传递选项。

    释放函数:av_dict_free

    设置编解码器的主要步骤:

  10. 创建输出上下文

dart 复制代码
avformat_alloc_output_context2(&formatContext, nullptr, nullptr, filename);
if (!formatContext) {
    qDebug() << "无法创建输出上下文";
    return;
}

avformat_alloc_output_context2 函数根据指定的文件名创建一个输出格式上下文。如果成功,formatContext 将指向一个新的AVFormatContext 结构体。如果失败,formatContext 将为 nullptr。

  1. 查找编码器
dart 复制代码
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
    qDebug() << "找不到编码器";
    return;
}

avcodec_find_encoder 函数根据编码器ID(这里是AV_CODEC_ID_H264)查找对应的编码器。如果成功,codec 将指向一个 AVCodec 结构体。如果失败,codec 将为 nullptr。

  1. 创建视频流
dart 复制代码
videoStream = avformat_new_stream(formatContext, codec);
if (!videoStream) {
    qDebug() << "无法创建视频流";
    return;
}

avformat_new_stream 函数在 formatContext 中创建一个新的流,并将其与指定的编码器关联。如果成功,videoStream 将指向一个新的 AVStream 结构体。如果失败,videoStream 将为 nullptr。

  1. 分配编码器上下文
dart 复制代码
codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
    qDebug() << "无法分配视频编解码器上下文";
    return;
}
//设置编码器参数
codecContext->bit_rate = 400000;
codecContext->width = width;
codecContext->height = height;
codecContext->time_base = {1, 25};
codecContext->framerate = {25, 1};
codecContext->gop_size = 10;
codecContext->max_b_frames = 1;
codecContext->pix_fmt = AV_PIX_FMT_YUV420P;

avcodec_alloc_context3 函数为指定的编码器分配并初始化一个新的编码器上下文。如果成功,codecContext 将指向一个新的 AVCodecContext 结构体。如果失败,codecContext 将为 nullptr。

参数设置:

bit_rate:设置编码器的比特率。

width 和 height:设置视频的宽度和高度。

time_base:设置时间基,表示每秒25帧。

framerate:设置帧率。

gop_size:设置GOP(Group of Pictures)大小。

max_b_frames:设置最大B帧数。

pix_fmt:设置像素格式。

  1. 设置全局头部标志
dart 复制代码
if (formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
    codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}

如果输出格式需要全局头部(AVFMT_GLOBALHEADER),则在编码器上下文中设置 AV_CODEC_FLAG_GLOBAL_HEADER 标志。

注意:

在使用FFmpeg进行解码和编码操作时,都需要打开对应的编解码器。具体来说,你需要为解码器和编码器分别初始化和打开它们的上下文。(demo这里只涉及编码)

解码器和编码器的初始化和打开

解码器

查找解码器:使用 avcodec_find_decoder 函数查找解码器。

分配解码器上下文:使用 avcodec_alloc_context3 函数分配解码器上下文。

打开解码器:使用 avcodec_open2 函数打开解码器。

编码器

查找编码器:使用 avcodec_find_encoder 函数查找编码器。

分配编码器上下文:使用 avcodec_alloc_context3 函数分配编码器上下文。

设置编码器参数:设置编码器的必要参数,如比特率、宽度、高度、像素格式等。

打开编码器:使用 avcodec_open2 函数打开编码器。

两者使用基本上一样,但注意及时两个类型一样,但编码器与解码器使用的是独立的上下文,注意这一点!!

以下是对于编码器的编写流程:

dart 复制代码
int main() {
    // 初始化FFmpeg库
    avcodec_register_all();
    avformat_network_init();

    // 查找H.264编码器
    AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        std::cerr << "找不到H.264编码器" << std::endl;
        return -1;
    }

    // 分配编码器上下文
    AVCodecContext *codecContext = avcodec_alloc_context3(codec);
    if (!codecContext) {
        std::cerr << "无法分配编码器上下文" << std::endl;
        return -1;
    }

    // 设置编码器参数
    codecContext->bit_rate = 400000;
    codecContext->width = 640;
    codecContext->height = 480;
    codecContext->time_base = {1, 25};
    codecContext->framerate = {25, 1};
    codecContext->gop_size = 10;
    codecContext->max_b_frames = 1;
    codecContext->pix_fmt = AV_PIX_FMT_YUV420P;

    if (avcodec_open2(codecContext, codec, nullptr) < 0) {
        std::cerr << "无法打开编解码器" << std::endl;
        avcodec_free_context(&codecContext);
        return -1;
    }


    // 发送一个空帧到编码器,以便刷新编码器的内部缓冲区
    int ret = avcodec_send_frame(codecContext, nullptr);
    if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        std::cerr << "发送空帧到编码器时出错: " << errbuf << std::endl;
        avcodec_free_context(&codecContext);
        return -1;
    }

    // 接收所有剩余的编码数据包
    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = nullptr;
    pkt.size = 0;

    while (true) {
        ret = avcodec_receive_packet(codecContext, &pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            char errbuf[128];
            av_strerror(ret, errbuf, sizeof(errbuf));
            std::cerr << "接收编码后的数据包时出错: " << errbuf << std::endl;
            break;
        }

        // 处理编码后的数据包
        std::cout << "接收到一个编码数据包,大小: " << pkt.size << " 字节" << std::endl;

        // 释放数据包
        av_packet_unref(&pkt);
    }

    // 释放资源
    avcodec_free_context(&codecContext);

    return 0;
}
  1. 打开编码器
dart 复制代码
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
    qDebug() << "无法打开编解码器";
    return;
}

avcodec_open2 函数初始化编码器上下文并打开编码器。如果成功,返回0;如果失败,返回负值。

  1. 复制编码器参数
dart 复制代码
if (avcodec_parameters_from_context(videoStream->codecpar, codecContext) < 0) {
    qDebug() << "无法复制编解码器参数";
    return;
}

avcodec_parameters_from_context 函数将编码器上下文中的参数复制到视频流的参数结构体中。如果成功,返回0;如果失败,返回负值。

  1. 打开输出文件
dart 复制代码
if (!(formatContext->oformat->flags & AVFMT_NOFILE)) {
    if (avio_open(&formatContext->pb, filename, AVIO_FLAG_WRITE) < 0) {
        qDebug() << "无法打开输出文件";
        return;
    }
}

如果输出格式不需要文件(AVFMT_NOFILE),则跳过此步骤。否则,使用 avio_open 函数打开输出文件。如果成功,返回0;如果失败,返回负值。 修改输出文件路径为桌面路径,---修改输出上下文中的输出路径为桌面路径

dart 复制代码
if (avformat_write_header(formatContext, nullptr) < 0) {
    qDebug() << "打开输出文件时出错";
    return;
}

avformat_write_header 函数将文件头写入输出文件。如果成功,返回0;如果失败,返回负值。

  1. 分配视频帧
dart 复制代码
frame = av_frame_alloc();
if (!frame) {
    qDebug() << "无法分配视频帧";
    return;
}
frame->format = codecContext->pix_fmt;
frame->width = codecContext->width;
frame->height = codecContext->height;

分配并初始化一个视频帧。

av_frame_alloc 函数分配一个新的视频帧。如果成功,frame 将指向一个新的 AVFrame 结构体。如果失败,frame 将为 nullptr。然后设置帧的格式、宽度和高度。

  1. 分配视频帧数据
dart 复制代码
if (av_frame_get_buffer(frame, 32) < 0) {
    qDebug() << "无法分配视频帧数据";
    return;
}

av_frame_get_buffer 函数为视频帧分配数据缓冲区。如果成功,返回0;如果失败,返回负值。

  1. 初始化像素格式转换上下文
dart 复制代码
swsContext = sws_getContext(codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
                            codecContext->width, codecContext->height, codecContext->pix_fmt,
                            SWS_BICUBIC, nullptr, nullptr, nullptr);
if (!swsContext) {
    qDebug() << "无法初始化转换上下文";
    return;
}

sws_getContext 函数创建一个用于像素格式转换的上下文。它将源图像的格式(AV_PIX_FMT_RGB24)转换为目标图像的格式(codecContext->pix_fmt)。如果成功,swsContext 将指向一个新的 SwsContext 结构体。如果失败,swsContext 将为 nullptr。

参数为:输入图像的高度,宽度,像素格式,输出图像的宽度,高度,像素格式,(缩放算法,过滤器

输入格式:RGB24(每个像素由三个字节表示,分别表示红色、绿色和蓝色分量)。

输出格式:通常是 YUV420P(每个像素由三个分量表示,分别是亮度(Y)和两个色度(U 和 V)。Y 分量的分辨率与原图像相同,而 U 和 V 分量的分辨率是原图像的一半(水平和垂直方向上各减少一半))。

完整代码:

dart 复制代码
    avformat_alloc_output_context2(&formatContext, nullptr, nullptr, filename);
    if (!formatContext) {
        qDebug() << "无法创建输出上下文";
        return;
    }

    AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        qDebug() << "找不到编码器";
        return;
    }

    videoStream = avformat_new_stream(formatContext, codec);
    if (!videoStream) {
        qDebug() << "无法创建视频流";
        return;
    }

    codecContext = avcodec_alloc_context3(codec);
    if (!codecContext) {
        qDebug() << "无法分配视频编解码器上下文";
        return;
    }

    codecContext->bit_rate = 400000;
    codecContext->width = width;
    codecContext->height = height;
    codecContext->time_base = {1, 25};
    codecContext->framerate = {25, 1};
    codecContext->gop_size = 10;
    codecContext->max_b_frames = 1;
    codecContext->pix_fmt = AV_PIX_FMT_YUV420P;

    if (formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
        codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    if (avcodec_open2(codecContext, codec, nullptr) < 0) {
        qDebug() << "无法打开编解码器";
        return;
    }

    if (avcodec_parameters_from_context(videoStream->codecpar, codecContext) < 0) {
        qDebug() << "无法复制编解码器参数";
        return;
    }

    if (!(formatContext->oformat->flags & AVFMT_NOFILE)) {
        if (avio_open(&formatContext->pb, filename, AVIO_FLAG_WRITE) < 0) {
            qDebug() << "无法打开输出文件";
            return;
        }
    }

    if (avformat_write_header(formatContext, nullptr) < 0) {
        qDebug() << "打开输出文件时出错";
        return;
    }

    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "无法分配视频帧";
        return;
    }
    frame->format = codecContext->pix_fmt;
    frame->width = codecContext->width;
    frame->height = codecContext->height;

    if (av_frame_get_buffer(frame, 32) < 0) {
        qDebug() << "无法分配视频帧数据";
        return;
    }

    swsContext = sws_getContext(codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
                                codecContext->width, codecContext->height, codecContext->pix_fmt,
                                SWS_BICUBIC, nullptr, nullptr, nullptr);
    if (!swsContext) {
        qDebug() << "无法初始化转换上下文";
        return;
    }
}

3. 捕获屏幕帧与写入输出文件

这里主要是使用Qt的grab函数捕获屏幕帧,并进行像素格式转换。具体步骤如下:

  1. 捕获屏幕
dart 复制代码
QPixmap originalPixmap = QGuiApplication::primaryScreen()->grabWindow(0);
QImage image = originalPixmap.toImage().convertToFormat(QImage::Format_RGB888);

QGuiApplication::primaryScreen()->grabWindow(0):捕获整个屏幕(窗口ID为0表示整个屏幕)。

originalPixmap.toImage().convertToFormat(QImage::Format_RGB888):将捕获的图像转换为 QImage 格式,并将其像素格式转换为 RGB888。

  1. 准备像素格式转换

uint8_t *data[1] = { image.bits() };

int linesize[1] = { static_cast(image.bytesPerLine()) };

功能:准备源图像数据和行字节数,以便进行像素格式转换。

解释:

data[1]:指向源图像数据的指针数组。

linesize[1]:源图像每行的字节数。

  1. 像素格式转换
dart 复制代码
sws_scale(swsContext, data, linesize, 0, codecContext->height, frame->data, frame->linesize);

swsContext:像素格式转换上下文。data 和 linesize:源图像数据和行字节数。--根据格式转化上下文转换对应的输入到输出----

输入数据属性:swsContext,data为图像数据,RGB24,

输出数据属性:codecContext->height, frame->data, frame->linesize

0:源图像的起始行。

codecContext->height:源图像的行数。

frame->data 和 frame->linesize:目标图像数据和行字节数。

  1. 记录帧的个数

frame->pts = frameCounter++;

解释:frameCounter 是一个递增的计数器。

  1. 发送帧到编码器
dart 复制代码
int ret = avcodec_send_frame(codecContext, frame);
if (ret < 0) {
    char errbuf[128];
    av_strerror(ret, errbuf, sizeof(errbuf));
    qDebug() << "发送帧到编码器时出错: " << errbuf;
    return;
}

功能:将帧发送到编码器。

解释:

avcodec_send_frame:将帧发送到编码器。

ret:返回值,如果小于0表示出错。

av_strerror:获取详细的错误信息。

  1. 初始化数据包
dart 复制代码
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;

av_init_packet:初始化数据包。

pkt.data 和 pkt.size:设置数据包的数据指针和大小。

  1. 接收编码后的数据包
dart 复制代码
while (ret >= 0) {
    ret = avcodec_receive_packet(codecContext, &pkt);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        break;
    } else if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "接收编码后的数据包时出错: " << errbuf;
        break;
    }
 
    av_interleaved_write_frame(formatContext, &pkt);
    av_packet_unref(&pkt);
}

avcodec_receive_packet:从编码器接收编码后的数据包。

ret:返回值,如果等于 AVERROR(EAGAIN) 或 AVERROR_EOF 表示没有更多数据包可接收。

av_interleaved_write_frame:将数据包写入文件。

av_packet_unref:释放数据包。

注意在写入的时候,调整时间戳

dart 复制代码
while (avcodec_receive_packet(codec_context_, &pkt) == 0) {
        av_log(NULL, AV_LOG_DEBUG, "写入的数据的地址: %p, 大小: %d\n", static_cast<void*>(pkt.data), pkt.size);
        // 重新调整时间戳
        av_packet_rescale_ts(&pkt, codec_context_->time_base, format_context_->streams[0]->time_base);
        pkt.stream_index = 0;

        ret = av_interleaved_write_frame(format_context_, &pkt);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "写入数据包时出错\n");
            av_packet_unref(&pkt);
            return;
        }
        av_packet_unref(&pkt);
    }

以下是完整的代码片段:

dart 复制代码
void captureFrame() {
    QPixmap originalPixmap = QGuiApplication::primaryScreen()->grabWindow(0);
    QImage image = originalPixmap.toImage().convertToFormat(QImage::Format_RGB888);

    uint8_t *data[1] = { image.bits() };
    int linesize[1] = { static_cast<int>(image.bytesPerLine()) };
    sws_scale(swsContext, data, linesize, 0, codecContext->height, frame->data, frame->linesize);

    frame->pts = frameCounter++;

    int ret = avcodec_send_frame(codecContext, frame);
    if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "发送帧到编码器时出错: " << errbuf;
        return;
    }

    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = nullptr;
    pkt.size = 0;

    while (ret >= 0) {
        ret = avcodec_receive_packet(codecContext, &pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            char errbuf[128];
            av_strerror(ret, errbuf, sizeof(errbuf));
            qDebug() << "接收编码后的数据包时出错: " << errbuf;
            break;
        }

        av_interleaved_write_frame(formatContext, &pkt);
        av_packet_unref(&pkt);
    }
}

4. 释放资源 在录制结束时,释放所有分配的资源。

释放资源ffmpeg提供有对应的释放的方法 以下是如何释放每种资源的具体方法: 刷新缓冲区中的每一帧数据,确保每一帧都写入输出文件当中。

dart 复制代码
*avcodec_send_frame(codec_context_, nullptr); // 传递空帧以刷新编码器
    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = nullptr;
    pkt.size = 0;
    while (avcodec_receive_packet(codec_context_, &pkt) == 0) {
        av_log(NULL, AV_LOG_DEBUG, "Flushing packet with data address: %p, size: %d\n", static_cast<void*>(pkt.data), pkt.size);
        av_packet_rescale_ts(&pkt, codec_context_->time_base, format_context_->streams[0]->time_base);
        pkt.stream_index = 0;
        av_interleaved_write_frame(format_context_, &pkt);
        av_packet_unref(&pkt);
    }
1. 释放 AVCodecContext
if (codecContext) {
    avcodec_free_context(&codecContext);
}
2. 释放 AVFrame
if (frame) {
    av_frame_free(&frame);
}
3. 释放 SwsContext
if (swsContext) {
    sws_freeContext(swsContext);
}
4. 释放 AVFormatContext
if (formatContext) {
    avformat_close_input(&formatContext);
    avformat_free_context(formatContext);
}
5. 释放 AVPacket
AVPacket packet;
av_init_packet(&packet);
// 使用 packet
av_packet_unref(&packet);
6. 释放 AVCodecParameters
if (codecParameters) {
    avcodec_parameters_free(&codecParameters);
}
7. 释放 AVIOContext
if (avioContext) {
    avio_context_free(&avioContext);
}
8. 释放 AVFilterGraph
if (filterGraph) {
    avfilter_graph_free(&filterGraph);
}
9. 释放 AVFilterContext
if (filterContext) {
    avfilter_free(filterContext);
}
10. 释放 AVFrame 中的缓冲区
if (frame) {
    av_frame_unref(frame);
}*
以下是本功能的释放部分:

av_log(NULL, AV_LOG_DEBUG, "开始刷新编码器\n");

  

```dart
  avcodec_send_frame(codec_context_, nullptr); // 传递空帧以刷新编码器
    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = nullptr;
    pkt.size = 0;
    while (avcodec_receive_packet(codec_context_, &pkt) == 0) {
        av_log(NULL, AV_LOG_DEBUG, "Flushing packet with data address: %p, size: %d\n", static_cast<void*>(pkt.data), pkt.size);

        av_packet_rescale_ts(&pkt, codec_context_->time_base, format_context_->streams[0]->time_base);
        pkt.stream_index = 0;

        av_interleaved_write_frame(format_context_, &pkt);
        av_packet_unref(&pkt);
    }

    if (format_context_) {
        av_log(NULL, AV_LOG_DEBUG, "写入文件尾部\n");
        av_write_trailer(format_context_);
        if (!(format_context_->oformat->flags & AVFMT_NOFILE)) {
            av_log(NULL, AV_LOG_DEBUG, "关闭输出文件\n");
            avio_closep(&format_context_->pb);
        }
        avformat_free_context(format_context_);
        format_context_ = nullptr;
    }

    if (codec_context_) {
        av_log(NULL, AV_LOG_DEBUG, "释放编码器上下文\n");
        avcodec_free_context(&codec_context_);
        codec_context_ = nullptr;
    }

    if (frame_) {
        av_log(NULL, AV_LOG_DEBUG, "释放视频帧\n");
        av_frame_free(&frame_);
        frame_ = nullptr;
    }

    if (sws_context_) {
        av_log(NULL, AV_LOG_DEBUG, "释放SWS上下文\n");
        sws_freeContext(sws_context_);
        sws_context_ = nullptr;
    }
}

5.自定义I/O上下文

为什么需要同时用 AVFormatContext 和 AVIOContext

虽然 AVFormatContext 和 AVIOContext 都涉及到数据处理,但它们的职责不同,且在数据处理流程中互补:

AVFormatContext:管理文件的格式和流信息。它负责描述文件的结构,包括音频、视频、字幕等流的元数据。

AVIOContext:处理实际的数据读写操作。它负责将数据从源读取或写入目标。 在实际应用中,AVFormatContext 需要一个

AVIOContext 来执行实际的 I/O 操作。

例如,当你调用 avformat_write_header 或 av_write_frame 等函数时,AVFormatContext 会使用关联的 AVIOContext 来执行实际的写操作。

为了确保写入输出文件之后,文件是正常可以打开的,除了写入输出上下文,我们还需要去自定义一个I/O上下文来控制我们的输入:
在使用 FFmpeg 进行多媒体处理时,通常会涉及到数据的输入和输出操作。FFmpeg 提供了 AVIOContext 结构体,用于处理这些 I/O 操作。默认情况下,FFmpeg 会使用标准的文件 I/O 函数来读取和写入数据。然而,在某些情况下,你可能需要自定义这些 I/O 操作,例如:

特殊的文件格式:你可能需要处理一些特殊的文件格式或协议,这些格式或协议无法通过标准的文件 I/O 函数来处理。

内存缓冲区:你可能希望将数据写入内存缓冲区而不是文件,或者从内存缓冲区读取数据。

网络流:你可能需要处理网络流,而不是本地文件。

自定义逻辑:你可能需要在读写数据时添加一些自定义的逻辑,例如加密、解密、压缩或解压缩。

虽然我们已经将数据写入了输出上下文,但可能需要自定义 I/O 上下文来实现更复杂或特定的功能。以下是一些可能的原因:

  1. 更灵活的控制 通过自定义 AVIOContext,你可以完全控制数据的读写过程。例如,你可以在写入数据之前对其进行处理,或者在读取数据之后对其进行处理。
  2. 处理非标准数据源或目标 如果你的数据源或目标不是标准的文件,而是内存缓冲区、网络流或其他非标准的数据源或目标,自定义 AVIOContext 是必要的。
  3. 性能优化 在某些情况下,自定义 I/O 上下文可以带来性能上的优化。例如,你可以使用更高效的缓冲区管理策略,减少 I/O 操作的次数,从而提高性能。
  4. 兼容性 某些平台或环境可能不支持标准的文件 I/O 操作,通过自定义 AVIOContext,你可以实现跨平台的 I/O 操作。

一般我们可以使用hls流媒体传输协议,HLS是一种流媒体协议,通常用于视频流媒体。

//样例

dart 复制代码
int main() {
    // 注册所有的编解码器和格式
    av_register_all();

    // 输出文件名
    const char *output_filename = "output.m3u8";
    const char *ts_name = "segment_%03d.ts";

    // 分配输出媒体上下文
    AVFormatContext *oc = NULL;
    avformat_alloc_output_context2(&oc, NULL, "hls", output_filename);
    if (!oc) {
        fprintf(stderr, "Could not allocate output context\n");
        return -1;
    }

    // 打开输出文件
    if (!(oc->oformat->flags & AVFMT_NOFILE)) {
        int ret = avio_open(&oc->pb, output_filename, AVIO_FLAG_WRITE);
        if (ret < 0) {
            char errbuf[128];
            av_strerror(ret, errbuf, sizeof(errbuf));
            fprintf(stderr, "Could not open output file: %s\n", errbuf);
            return -1;
        }
    }

    // 设置HLS相关的选项
    AVDictionary *opt = NULL;
    av_dict_set(&opt, "hls_flags", "append_list", 0);
    av_dict_set(&opt, "hls_time", "10", 0);
    av_dict_set(&opt, "hls_list_size", "0", 0);
    av_dict_set(&opt, "hls_segment_filename", ts_name, 0);

    // 写文件头
    int ret = avformat_write_header(oc, &opt);
    av_dict_free(&opt);
    if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Error occurred when opening output file: %s\n", errbuf);
        return -1;
    }

    // 释放资源
    if (!(oc->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&oc->pb);
    }
    avformat_free_context(oc);

    return 0;
}

6.对于ACC编码器注意事项

在FFmpeg中,某些音频编解码器(例如AAC)具有固定的帧大小(样本数),这通常是由编解码器的标准和设计决定的。即使你尝试手动设置帧大小,编解码器可能会忽略你的设置并使用其默认值。

确认编解码器的帧大小

在打开编解码器之后,你可以检查 AVCodecContext 的 frame_size 字段,以确认实际使用的帧大小。

要确认编解码器是否支持某些特定的设置(例如可变帧大小),你可以查看编解码器的能力标志(capabilities)。FFmpeg 提供了一些能力标志来描述编解码器的特性和支持的功能。

检查编解码器的能力标志

AVCodec 结构体包含一个 capabilities 字段,它是一个位掩码,表示编解码器支持的功能。你可以使用这些标志来检查编解码器是否支持特定的功能。

常见的能力标志

AV_CODEC_CAP_VARIABLE_FRAME_SIZE:表示编解码器支持可变帧大小。

AV_CODEC_CAP_DELAY:表示编解码器可能会在接收到所有输入数据后才输出数据。

AV_CODEC_CAP_FRAME_THREADS:表示编解码器支持帧级多线程。

// 检查编解码器是否支持可变帧大小

if (codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) {

printf("编解码器支持可变帧大小\n");

} else {

printf("编解码器不支持可变帧大小\n");

}

在FFmpeg中,支持可变帧大小的音频编码器相对较少,但确实存在一些支持可变帧大小的编码器。一个典型的例子是Opus编码器。Opus是一种高效的音频编解码器,广泛用于实时通信和流媒体应用。

使用Opus编码器设置可变帧大小

样例:

int main() {

// 注册所有编解码器

avcodec_register_all();

// 查找Opus编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_OPUS);
if (!codec) {
    fprintf(stderr, "找不到Opus编码器\n");
    return -1;
}

// 分配编解码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
    fprintf(stderr, "分配编解码器上下文失败\n");
    return -1;
}

// 设置编码参数
codec_ctx->bit_rate = 64000;
codec_ctx->sample_rate = 48000;
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
codec_ctx->sample_fmt = codec->sample_fmts[0]; // 使用编码器支持的第一个样本格式

// 检查编解码器是否支持可变帧大小
if (codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) {
    printf("Opus编码器支持可变帧大小\n");
    codec_ctx->frame_size = 960; // 例如,设置为960个样本
} else {
    printf("Opus编码器不支持可变帧大小\n");
    codec_ctx->frame_size = 960; // 使用默认的帧大小
}

// 打开编码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
    fprintf(stderr, "打开编码器失败\n");
    avcodec_free_context(&codec_ctx);
    return -1;
}

// 检查实际的帧大小
printf("实际的帧大小:%d\n", codec_ctx->frame_size);

// 释放编解码器上下文
avcodec_free_context(&codec_ctx);

return 0;

}

经过测试,基本上我们是无法去设定编码器的样本数,这通常取决于编码器自己设定的样本数,因此在设计帧率以及样本数,最好按照编码器来。

ACC 1024 OPUS 960

相关推荐
EasyCVR1 小时前
私有化部署视频平台EasyCVR宇视设备视频平台如何构建视频联网平台及升级视频转码业务?
大数据·网络·音视频·h.265
天空中的野鸟1 小时前
Android音频采集
android·音视频
计算机毕设孵化场2 小时前
计算机毕设-基于springboot的高校网上缴费综合务系统视频的设计与实现(附源码+lw+ppt+开题报告)
java·spring boot·计算机外设·音视频·课程设计·高校网上缴费综合务系统视频·计算机毕设ppt
lxkj_20247 小时前
使用线程局部存储解决ffmpeg中多实例调用下自定义日志回调问题
ffmpeg
简鹿办公9 小时前
如何提取某站 MV 视频中的音乐为 MP3 音频
音视频·简鹿视频格式转换器·视频提取mp3音频
yufengxinpian9 小时前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
runing_an_min11 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
ruizhenggang11 小时前
ffmpeg本地编译不容易发现的问题 — Error:xxxxx not found!
ffmpeg
runing_an_min13 小时前
ffmpeg视频滤镜:提取缩略图-framestep
ffmpeg·音视频·framestep
小曲曲14 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频