
使用的FFMpeng库,库连接:https://download.csdn.net/download/yuchunhai321/92903428
使用的QT工程,编译器MSVC2015,编译成功,运行时需要将库里bin文件下的文件拷到可执行的文件目录下
工程源码:
pro文件中增加
INCLUDEPATH += $$PWD/FFMpegH264/lib/ffmpeg4.0.1/include
LIBS += -L$$PWD/FFMpegH264/lib/ffmpeg4.0.1/lib/win32/ -lavcodec -lavdevice -lavfilter -lavformat -lavutil -lpostproc -lswresample -lswscale
#ifndef VIDEO_RECORDER_H
#define VIDEO_RECORDER_H
#include <QObject>
#include <QImage>
#include <QThread>
#include <QMutex>
#include <QQueue>
#include <QWaitCondition>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
#include <libavdevice/avdevice.h>
}
class VideoRecorder : public QObject
{
Q_OBJECT
public:
explicit VideoRecorder(QObject *parent = nullptr);
~VideoRecorder();
// 设置编码参数(可选,有默认值)
void setVideoParameters(int width, int height, int fps = 25, int bitrate = 2000000);
// 开始录制:传入输出文件名
bool startRecording(const QString& filename);
// 停止录制
void stopRecording();
// 添加一帧图片数据
void addFrame(const QImage& image);
void addFrame(const QByteArray& imageData);
signals:
// 录制状态信号
void recordingStarted();
void recordingStopped(bool success);
void errorOccurred(const QString& error);
// 进度信号(可选)
void frameEncoded(int frameCount);
private:
// FFmpeg 相关成员
AVFormatContext* m_formatCtx = nullptr;
AVCodecContext* m_codecCtx = nullptr;
AVStream* m_stream = nullptr;
SwsContext* m_swsCtx = nullptr;
AVFrame* m_frame = nullptr;
// 视频参数
int m_width = 1920;
int m_height = 1080;
int m_fps = 25;
int m_bitrate = 2000000; // 2 Mbps
AVPixelFormat m_pixelFormat = AV_PIX_FMT_YUV420P;
// 编码状态
bool m_isRecording = false;
int64_t m_ptsCounter = 0;
int m_videoStreamIndex = -1;
// 线程安全的数据队列
QQueue<QImage> m_frameQueue;
QMutex m_mutex;
QWaitCondition m_condition;
bool m_stopRequested = false;
// 工作线程
QThread* m_workerThread = nullptr;
// 内部函数
bool initializeFFmpeg(const QString& filename);
void cleanupFFmpeg();
bool encodeFrame(const QImage& image);
void flushEncoder();
bool convertImageToFrame(const QImage& image, AVFrame* frame);
private slots:
void processFrames(); // 工作线程的主循环
};
class VideoSaver : public QObject
{
Q_OBJECT
public:
explicit VideoSaver(QObject *parent = nullptr);
~VideoSaver();
// 打开视频文件准备保存
bool open(const QString &filename, int width, int height, double fps = 25.0);
// 写入一帧QImage图像
bool writeFrame(const QImage &image);
// 关闭文件,写入尾部信息
void close();
private:
bool initCodec(int width, int height, double fps);
bool convertQImageToAVFrame(const QImage &image, AVFrame *frame);
AVFormatContext *m_formatCtx;//全局上下文 用于打开视频文件
AVCodecContext *m_codecCtx;//启动编解码器的上下文
AVStream *m_stream;//媒体流
AVFrame *m_frame;//解码后的原始数据
AVPacket *m_packet;//编码后的压缩数据
SwsContext *m_swsCtx;//图像尺寸/格式转换器
int m_frameIndex;
bool m_isOpen;
};
class VideoPlayer : public QObject
{
Q_OBJECT
public:
explicit VideoPlayer(QObject *parent = nullptr);
~VideoPlayer();
// 打开视频文件
bool open(const QString &filename);
// 获取下一帧(返回QImage,调用者负责显示)
bool getNextFrame(QImage &image);
// 关闭并释放资源
void close();
// 获取视频信息
int getWidth() const { return m_width; }
int getHeight() const { return m_height; }
double getFps() const { return m_fps; }
int getTotalFrames() const { return m_totalFrames; }
bool isOpen() const { return m_isOpen; }
// 重置到开头
void reset();
private:
bool initDecoder();
bool initSwsContext();
bool decodeNextFrame();
void cleanup();
bool readNextPacket();
private:
AVFormatContext *m_formatCtx;
AVCodecContext *m_codecCtx;
AVCodec *m_codec;
AVFrame *m_frame;
AVFrame *m_frameRGB;
AVPacket *m_packet;
struct SwsContext *m_swsCtx;
int m_streamIndex;
int m_width;
int m_height;
double m_fps;
int m_totalFrames;
bool m_isOpen;
bool m_endOfFile;
uint8_t *m_rgbBuffer;
QImage m_currentImage;
};
#endif // VIDEO_RECORDER_H
#include "videorecorder.h"
#include <QDebug>
#include <QDir>
VideoRecorder::VideoRecorder(QObject *parent) : QObject(parent)
{
// 初始化 FFmpeg 库
avformat_network_init();
// 重要:先移除父对象,然后才能移动线程
this->setParent(nullptr);
// 创建工作线程
m_workerThread = new QThread(this); // 这个可以保留父对象
this->moveToThread(m_workerThread);
// 连接信号和槽
connect(m_workerThread, &QThread::started, this, &VideoRecorder::processFrames);
connect(m_workerThread, &QThread::finished, m_workerThread, &QThread::deleteLater);
m_workerThread->start();
}
VideoRecorder::~VideoRecorder()
{
stopRecording();
if (m_workerThread) {
m_workerThread->quit();
// 注意:这里不能直接调用 wait(),因为当前对象可能在主线程
// 而 m_workerThread 是子对象,Qt 会自动处理
// 但为了安全,可以等待一段时间
if (!m_workerThread->wait(3000)) {
qDebug() << "Thread didn't stop gracefully, terminating...";
m_workerThread->terminate();
m_workerThread->wait();
}
}
cleanupFFmpeg();
avformat_network_deinit();
}
void VideoRecorder::setVideoParameters(int width, int height, int fps, int bitrate)
{
m_width = width;
m_height = height;
m_fps = fps;
m_bitrate = bitrate;
}
bool VideoRecorder::startRecording(const QString& filename)
{
QMutexLocker locker(&m_mutex);
if (m_isRecording) {
emit errorOccurred("Already recording");
return false;
}
if (!initializeFFmpeg(filename)) {
emit errorOccurred("Failed to initialize FFmpeg");
return false;
}
m_isRecording = true;
m_stopRequested = false;
m_ptsCounter = 0;
emit recordingStarted();
qDebug() << "Recording started:" << filename;
return true;
}
void VideoRecorder::stopRecording()
{
if (!m_isRecording) {
return;
}
// 标记停止请求
{
QMutexLocker locker(&m_mutex);
m_stopRequested = true;
m_condition.wakeAll();
}
}
void VideoRecorder::addFrame(const QImage& image)
{
if (!m_isRecording) {
return;
}
// 检查队列大小,避免内存爆炸
{
QMutexLocker locker(&m_mutex);
if (m_frameQueue.size() > 300) { // 最多缓存300帧
return;
}
}
// 将图片转换为标准格式
QImage standardized = image;
if (standardized.format() != QImage::Format_RGB32 &&
standardized.format() != QImage::Format_ARGB32) {
standardized = standardized.convertToFormat(QImage::Format_RGB32);
}
// 缩放图片到目标尺寸
if (standardized.width() != m_width || standardized.height() != m_height) {
standardized = standardized.scaled(m_width, m_height,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
QMutexLocker locker(&m_mutex);
m_frameQueue.enqueue(standardized);
m_condition.wakeOne(); // 唤醒工作线程
}
void VideoRecorder::addFrame(const QByteArray &imageData)
{
// 将 QByteArray 转换为 QImage
QImage image;
if (!image.loadFromData(imageData)) {
qDebug() << "Error: Failed to load image from QByteArray";
return ;
}
// 调用原有的 addFrame 函数
return addFrame(image);
}
bool VideoRecorder::initializeFFmpeg(const QString& filename)
{
int ret = 0;
// 确保先清理之前的资源
cleanupFFmpeg();
// 1. 创建输出上下文
ret = avformat_alloc_output_context2(&m_formatCtx, nullptr, "mp4",
filename.toStdString().c_str());
if (ret < 0 || !m_formatCtx) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Failed to create output context:" << errbuf;
return false;
}
// 2. 查找编码器 (H.264)
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
qDebug() << "H.264 encoder not found, trying MPEG-4...";
codec = avcodec_find_encoder(AV_CODEC_ID_MPEG4);
if (!codec) {
qDebug() << "No suitable encoder found";
return false;
}
}
// 3. 创建视频流
m_stream = avformat_new_stream(m_formatCtx, codec);
if (!m_stream) {
qDebug() << "Failed to create video stream";
return false;
}
m_videoStreamIndex = m_stream->index;
// 4. 创建编码器上下文
m_codecCtx = avcodec_alloc_context3(codec);
if (!m_codecCtx) {
qDebug() << "Failed to allocate codec context";
return false;
}
// 5. 设置编码参数
m_codecCtx->codec_id = codec->id;
m_codecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
m_codecCtx->width = m_width;
m_codecCtx->height = m_height;
m_codecCtx->time_base = AVRational{1, m_fps};
m_codecCtx->framerate = AVRational{m_fps, 1};
m_codecCtx->pix_fmt = m_pixelFormat;
m_codecCtx->bit_rate = m_bitrate;
m_codecCtx->gop_size = m_fps * 2;
// 设置编码质量(对 MPEG-4 有效)
if (codec->id == AV_CODEC_ID_MPEG4) {
m_codecCtx->qmin = 10;
m_codecCtx->qmax = 31;
}
// 6. 设置全局头(MP4 需要)
if (m_formatCtx->oformat->flags & AVFMT_GLOBALHEADER) {
m_codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 7. 打开编码器
// 添加编码器选项,减少内部缓冲
AVDictionary* opts = nullptr;
av_dict_set(&opts, "tune", "zerolatency", 0); // 零延迟模式
av_dict_set(&opts, "preset", "ultrafast", 0); // 最快编码
av_dict_set(&opts, "rc_lookahead", "0", 0); // 无前瞻缓冲
ret = avcodec_open2(m_codecCtx, codec, &opts);
if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Failed to open codec:" << errbuf;
return false;
}
// 8. 将编码器参数复制到流
ret = avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx);
if (ret < 0) {
qDebug() << "Failed to copy codec parameters";
return false;
}
// 9. 创建 AVFrame
m_frame = av_frame_alloc();
if (!m_frame) {
qDebug() << "Failed to allocate frame";
return false;
}
m_frame->format = m_codecCtx->pix_fmt;
m_frame->width = m_width;
m_frame->height = m_height;
ret = av_frame_get_buffer(m_frame, 32);
if (ret < 0) {
qDebug() << "Failed to allocate frame buffer";
return false;
}
// 10. 创建图像转换上下文 (RGB -> YUV)
m_swsCtx = sws_getContext(m_width, m_height, AV_PIX_FMT_RGB32,
m_width, m_height, m_pixelFormat,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!m_swsCtx) {
qDebug() << "Failed to create sws context";
return false;
}
// 11. 打开输出文件
ret = avio_open(&m_formatCtx->pb, filename.toStdString().c_str(), AVIO_FLAG_WRITE);
if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Failed to open output file:" << errbuf;
return false;
}
// 12. 写入文件头
ret = avformat_write_header(m_formatCtx, nullptr);
if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Failed to write header:" << errbuf;
return false;
}
return true;
}
void VideoRecorder::cleanupFFmpeg()
{
if (m_swsCtx) {
sws_freeContext(m_swsCtx);
m_swsCtx = nullptr;
}
if (m_frame) {
av_frame_free(&m_frame);
m_frame = nullptr;
}
if (m_codecCtx) {
avcodec_free_context(&m_codecCtx);
m_codecCtx = nullptr;
}
if (m_formatCtx) {
if (m_formatCtx->pb) {
avio_closep(&m_formatCtx->pb);
}
avformat_free_context(m_formatCtx);
m_formatCtx = nullptr;
}
}
bool VideoRecorder::encodeFrame(const QImage& image)
{
if (!m_isRecording || !m_codecCtx) {
return false;
}
// 1. 转换图片数据到 AVFrame
if (!convertImageToFrame(image, m_frame)) {
return false;
}
// 2. 设置 PTS
m_frame->pts = m_ptsCounter++;
// 3. 发送帧到编码器
int ret = avcodec_send_frame(m_codecCtx, m_frame);
if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Error sending frame to encoder:" << errbuf;
return false;
}
// 4. 接收编码后的数据包
AVPacket* pkt = av_packet_alloc();
bool success = true;
while (true) {
ret = avcodec_receive_packet(m_codecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Error receiving packet:" << errbuf;
success = false;
break;
}
// 5. 转换时间戳
pkt->stream_index = m_videoStreamIndex;
av_packet_rescale_ts(pkt, m_codecCtx->time_base,
m_stream->time_base);
// 6. 写入文件
ret = av_interleaved_write_frame(m_formatCtx, pkt);
if (ret < 0) {
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "Error writing packet:" << errbuf;
success = false;
}
av_packet_unref(pkt);
}
av_packet_free(&pkt);
if (success) {
emit frameEncoded(m_ptsCounter);
}
return success;
}
bool VideoRecorder::convertImageToFrame(const QImage& image, AVFrame* frame)
{
// 确保 frame 可写
if (av_frame_make_writable(frame) < 0) {
return false;
}
// 准备源数据
uint8_t* srcData[1] = { const_cast<uint8_t*>(image.bits()) };
int srcLinesize[1] = { static_cast<int>(image.bytesPerLine()) };
// 执行转换
sws_scale(m_swsCtx, srcData, srcLinesize, 0, m_height,
frame->data, frame->linesize);
return true;
}
void VideoRecorder::flushEncoder()
{
if (!m_codecCtx) {
return;
}
// 发送 NULL 帧刷新编码器
int ret = avcodec_send_frame(m_codecCtx, nullptr);
if (ret < 0) {
return;
}
// 接收所有剩余的编码数据包
AVPacket* pkt = av_packet_alloc();
while (true) {
ret = avcodec_receive_packet(m_codecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
break;
}
pkt->stream_index = m_videoStreamIndex;
av_packet_rescale_ts(pkt, m_codecCtx->time_base,
m_stream->time_base);
av_interleaved_write_frame(m_formatCtx, pkt);
av_packet_unref(pkt);
}
av_packet_free(&pkt);
// 写入文件尾部
if (m_formatCtx) {
av_write_trailer(m_formatCtx);
}
}
void VideoRecorder::processFrames()
{
while (true) {
QImage frame;
{
QMutexLocker locker(&m_mutex);
// 等待条件:有帧要处理,或者被要求停止
while (m_frameQueue.isEmpty() && !m_stopRequested) {
m_condition.wait(&m_mutex);
}
// 检查退出条件
if (m_stopRequested && m_frameQueue.isEmpty()) {
break;
}
// 取出一帧
if (!m_frameQueue.isEmpty()) {
frame = m_frameQueue.dequeue();
}
}
// 编码帧
if (m_isRecording && !frame.isNull()) {
if (!encodeFrame(frame)) {
qDebug() << "Failed to encode frame";
}
}
}
// 清理:刷新编码器并关闭文件
if (m_isRecording) {
qDebug() << "Flushing encoder...";
flushEncoder();
cleanupFFmpeg();
m_isRecording = false;
emit recordingStopped(true);
qDebug() << "Recording finished, total frames:" << m_ptsCounter;
}
}
#include <QDebug>
VideoSaver::VideoSaver(QObject *parent)
: QObject(parent)
, m_formatCtx(nullptr)
, m_codecCtx(nullptr)
, m_stream(nullptr)
, m_frame(nullptr)
, m_packet(nullptr)
, m_swsCtx(nullptr)
, m_frameIndex(0)
, m_isOpen(false)
{
// 初始化FFmpeg网络模块(某些格式需要)
avformat_network_init();
}
VideoSaver::~VideoSaver()
{
close();
}
bool VideoSaver::open(const QString &filename, int width, int height, double fps)
{
if (m_isOpen) {
qWarning() << "Video saver is already open";
return false;
}
// 1. 分配输出上下文
QString filePath = filename;
// 输出上下文指针的指针 指定输出格式 输出格式的名称字符串 输出文件的路径或 URL
int ret = avformat_alloc_output_context2(&m_formatCtx, nullptr, nullptr,
filePath.toUtf8().constData());
if (ret < 0 || !m_formatCtx) {
qWarning() << "Failed to allocate output context";
return false;
}
// 2. 初始化编码器和流
if (!initCodec(width, height, fps)) {
qWarning() << "Failed to init codec";
return false;
}
// 3. 打开输出文件
if (!(m_formatCtx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&m_formatCtx->pb, filePath.toUtf8().constData(), AVIO_FLAG_WRITE);
if (ret < 0) {
qWarning() << "Failed to open output file";
return false;
}
}
// 4. 写入文件头
ret = avformat_write_header(m_formatCtx, nullptr);
if (ret < 0) {
qWarning() << "Failed to write header";
return false;
}
m_isOpen = true;
qDebug() << "Video saver opened:" << filename << "size:" << width << "x" << height << "fps:" << fps;
return true;
}
bool VideoSaver::initCodec(int width, int height, double fps)
{
// 查找H.264编码器(或MPEG4等其他编码器)
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
// 如果H.264不可用,回退到MPEG4
codec = avcodec_find_encoder(AV_CODEC_ID_MPEG4);
if (!codec) {
qWarning() << "Codec not found";
return false;
}
}
// 创建视频流
m_stream = avformat_new_stream(m_formatCtx, codec);
if (!m_stream) {
qWarning() << "Failed to create stream";
return false;
}
m_stream->id = m_formatCtx->nb_streams - 1;
// 分配编码器上下文
m_codecCtx = avcodec_alloc_context3(codec);
if (!m_codecCtx) {
qWarning() << "Failed to allocate codec context";
return false;
}
// 设置编码参数
m_codecCtx->codec_id = codec->id;
m_codecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
m_codecCtx->width = width;
m_codecCtx->height = height;
m_codecCtx->time_base = AVRational{1, static_cast<int>(fps)};
m_codecCtx->framerate = AVRational{static_cast<int>(fps), 1};
m_codecCtx->pix_fmt = AV_PIX_FMT_YUV420P; // H.264常用格式
m_codecCtx->gop_size = 12; // 关键帧间隔
m_codecCtx->max_b_frames = 0; // 不使用B帧,降低延迟
// 设置码率(简单估算)
m_codecCtx->bit_rate = width * height * fps * 0.1; // 粗略估算
if (m_codecCtx->bit_rate < 400000) m_codecCtx->bit_rate = 400000;
if (m_codecCtx->bit_rate > 5000000) m_codecCtx->bit_rate = 5000000;
// 某些格式需要全局头
if (m_formatCtx->oformat->flags & AVFMT_GLOBALHEADER) {
m_codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 打开编码器
int ret = avcodec_open2(m_codecCtx, codec, nullptr);
if (ret < 0) {
qWarning() << "Failed to open codec";
return false;
}
// 将编码器参数复制到流
ret = avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx);
if (ret < 0) {
qWarning() << "Failed to copy parameters to stream";
return false;
}
m_stream->time_base = m_codecCtx->time_base;
// 分配帧内存
m_frame = av_frame_alloc();
if (!m_frame) {
qWarning() << "Failed to allocate frame";
return false;
}
m_frame->format = m_codecCtx->pix_fmt;
m_frame->width = width;
m_frame->height = height;
ret = av_frame_get_buffer(m_frame, 0);
if (ret < 0) {
qWarning() << "Failed to allocate frame buffer";
return false;
}
// 分配数据包
m_packet = av_packet_alloc();
if (!m_packet) {
qWarning() << "Failed to allocate packet";
return false;
}
// 创建图像转换上下文(QImage RGB → YUV420P)
m_swsCtx = sws_getContext(width, height, AV_PIX_FMT_RGB32,
width, height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!m_swsCtx) {
qWarning() << "Failed to create sws context";
return false;
}
return true;
}
bool VideoSaver::convertQImageToAVFrame(const QImage &image, AVFrame *frame)
{
// 确保图像尺寸匹配
if (image.width() != frame->width || image.height() != frame->height) {
qWarning() << "Image size mismatch";
return false;
}
// 确保帧数据可写
if (av_frame_make_writable(frame) < 0) {
qWarning() << "Frame not writable";
return false;
}
// 准备源数据(QImage的数据指针)
// 注意:QImage::bits()返回的指针需要确保QImage在整个转换过程中有效
QImage img = image.convertToFormat(QImage::Format_RGB32);
const uint8_t *inData[1] = { img.bits() };
int inLinesize[1] = { img.bytesPerLine() };
// 执行格式转换:RGB32 → YUV420P
sws_scale(m_swsCtx, inData, inLinesize, 0, frame->height,
frame->data, frame->linesize);
return true;
}
bool VideoSaver::writeFrame(const QImage &image)
{
if (!m_isOpen) {
qWarning() << "Video saver not open";
return false;
}
// 1. 将QImage转换为AVFrame
if (!convertQImageToAVFrame(image, m_frame)) {
return false;
}
// 设置帧的时间戳(PTS)
m_frame->pts = m_frameIndex++;
// 2. 发送帧到编码器
int ret = avcodec_send_frame(m_codecCtx, m_frame);
if (ret < 0) {
qWarning() << "Error sending frame to encoder";
return false;
}
// 3. 接收编码后的数据包
while (ret >= 0) {
ret = avcodec_receive_packet(m_codecCtx, m_packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
qWarning() << "Error receiving packet from encoder";
return false;
}
// 4. 调整时间戳并写入文件
av_packet_rescale_ts(m_packet, m_codecCtx->time_base, m_stream->time_base);
m_packet->stream_index = m_stream->index;
ret = av_interleaved_write_frame(m_formatCtx, m_packet);
if (ret < 0) {
qWarning() << "Error writing frame";
return false;
}
av_packet_unref(m_packet);
}
return true;
}
void VideoSaver::close()
{
if (!m_isOpen) return;
// 1. 刷新编码器缓冲区
avcodec_send_frame(m_codecCtx, nullptr);
int ret;
while (true) {
ret = avcodec_receive_packet(m_codecCtx, m_packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret >= 0) {
av_packet_rescale_ts(m_packet, m_codecCtx->time_base, m_stream->time_base);
m_packet->stream_index = m_stream->index;
av_interleaved_write_frame(m_formatCtx, m_packet);
av_packet_unref(m_packet);
}
}
// 2. 写入文件尾
av_write_trailer(m_formatCtx);
// 3. 清理资源
if (m_swsCtx) sws_freeContext(m_swsCtx);
if (m_packet) av_packet_free(&m_packet);
if (m_frame) av_frame_free(&m_frame);
if (m_codecCtx) avcodec_free_context(&m_codecCtx);
if (m_formatCtx) {
if (m_formatCtx->pb) avio_closep(&m_formatCtx->pb);
avformat_free_context(m_formatCtx);
}
m_swsCtx = nullptr;
m_packet = nullptr;
m_frame = nullptr;
m_codecCtx = nullptr;
m_formatCtx = nullptr;
m_isOpen = false;
m_frameIndex = 0;
qDebug() << "Video saver closed";
}
#include <QEventLoop>
#include <QTimer>
VideoPlayer::VideoPlayer(QObject *parent)
: QObject(parent)
, m_formatCtx(nullptr)
, m_codecCtx(nullptr)
, m_codec(nullptr)
, m_frame(nullptr)
, m_frameRGB(nullptr)
, m_packet(nullptr)
, m_swsCtx(nullptr)
, m_streamIndex(-1)
, m_width(0)
, m_height(0)
, m_fps(0.0)
, m_totalFrames(0)
, m_isOpen(false)
, m_endOfFile(false)
, m_rgbBuffer(nullptr)
{
avformat_network_init();
}
VideoPlayer::~VideoPlayer()
{
close();
}
bool VideoPlayer::open(const QString &filename)
{
if (m_isOpen) {
qWarning() << "VideoPlayer already open";
return false;
}
// 1. 打开输入文件
m_formatCtx = avformat_alloc_context();
if (avformat_open_input(&m_formatCtx, filename.toStdString().c_str(), nullptr, nullptr) != 0) {
qDebug() << "Failed to open input file:" << filename;
return false;
}
// 2. 获取流信息
if (avformat_find_stream_info(m_formatCtx, nullptr) < 0) {
qDebug() << "Failed to find stream info";
return false;
}
// 3. 查找视频流索引
for (unsigned int i = 0; i < m_formatCtx->nb_streams; i++) {
if (m_formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_streamIndex = i;
break;
}
}
if (m_streamIndex == -1) {
qDebug() << "No video stream found";
return false;
}
// 4. 获取视频参数
AVCodecParameters *codecpar = m_formatCtx->streams[m_streamIndex]->codecpar;
m_width = codecpar->width;
m_height = codecpar->height;
// 计算帧率
AVRational timeBase = m_formatCtx->streams[m_streamIndex]->time_base;
m_fps = av_q2d(av_guess_frame_rate(m_formatCtx, m_formatCtx->streams[m_streamIndex], nullptr));
if (m_fps <= 0) {
m_fps = 30.0;
}
// 获取总帧数
if (m_formatCtx->streams[m_streamIndex]->nb_frames > 0) {
m_totalFrames = m_formatCtx->streams[m_streamIndex]->nb_frames;
} else {
// 估算总帧数
double duration = m_formatCtx->duration / (double)AV_TIME_BASE;
m_totalFrames = static_cast<int>(duration * m_fps);
}
// 5. 初始化解码器
if (!initDecoder()) {
return false;
}
// 6. 初始化SWS转换上下文
if (!initSwsContext()) {
return false;
}
m_isOpen = true;
m_endOfFile = false;
qDebug() << "Video opened:" << filename
<< "Size:" << m_width << "x" << m_height
<< "FPS:" << m_fps
<< "Total frames:" << m_totalFrames;
return true;
}
bool VideoPlayer::initDecoder()
{
AVCodecParameters *codecpar = m_formatCtx->streams[m_streamIndex]->codecpar;
// 查找解码器
m_codec = avcodec_find_decoder(codecpar->codec_id);
if (!m_codec) {
qDebug() << "Decoder not found";
return false;
}
// 分配解码器上下文
m_codecCtx = avcodec_alloc_context3(m_codec);
if (!m_codecCtx) {
qDebug() << "Failed to allocate codec context";
return false;
}
// 复制参数到解码器上下文
if (avcodec_parameters_to_context(m_codecCtx, codecpar) < 0) {
qDebug() << "Failed to copy parameters to context";
return false;
}
// 打开解码器
if (avcodec_open2(m_codecCtx, m_codec, nullptr) < 0) {
qDebug() << "Failed to open decoder";
return false;
}
// 分配帧和数据包
m_frame = av_frame_alloc();
m_frameRGB = av_frame_alloc();
m_packet = av_packet_alloc();
if (!m_frame || !m_frameRGB || !m_packet) {
qDebug() << "Failed to allocate frame or packet";
return false;
}
return true;
}
bool VideoPlayer::initSwsContext()
{
// 分配RGB缓冲区
m_rgbBuffer = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_RGB32,
m_width, m_height, 1));
if (!m_rgbBuffer) {
qDebug() << "Failed to allocate RGB buffer";
return false;
}
av_image_fill_arrays(m_frameRGB->data, m_frameRGB->linesize, m_rgbBuffer,
AV_PIX_FMT_RGB32, m_width, m_height, 1);
// 创建SWS上下文
m_swsCtx = sws_getContext(m_width, m_height, m_codecCtx->pix_fmt,
m_width, m_height, AV_PIX_FMT_RGB32,
SWS_BICUBIC, nullptr, nullptr, nullptr);
if (!m_swsCtx) {
qDebug() << "Failed to create sws context";
return false;
}
return true;
}
bool VideoPlayer::readNextPacket()
{
while (true) {
int ret = av_read_frame(m_formatCtx, m_packet);
if (ret < 0) {
if (ret == AVERROR_EOF) {
m_endOfFile = true;
return false;
}
continue;
}
if (m_packet->stream_index == m_streamIndex) {
return true;
}
av_packet_unref(m_packet);
}
}
bool VideoPlayer::decodeNextFrame()
{
if (m_endOfFile) {
return false;
}
while (true) {
// 如果没有包了,读取下一个包
if (m_packet->size <= 0) {
if (!readNextPacket()) {
return false;
}
}
// 发送包到解码器
int ret = avcodec_send_packet(m_codecCtx, m_packet);
av_packet_unref(m_packet);
if (ret < 0) {
if (ret == AVERROR_EOF) {
m_endOfFile = true;
return false;
}
continue;
}
// 接收解码后的帧
ret = avcodec_receive_frame(m_codecCtx, m_frame);
if (ret == AVERROR(EAGAIN)) {
continue;
} else if (ret == AVERROR_EOF) {
m_endOfFile = true;
return false;
} else if (ret < 0) {
continue;
}
// 转换格式到RGB32
sws_scale(m_swsCtx, m_frame->data, m_frame->linesize, 0, m_height,
m_frameRGB->data, m_frameRGB->linesize);
// 创建QImage(拷贝数据,确保独立)
m_currentImage = QImage(m_frameRGB->data[0], m_width, m_height,
QImage::Format_RGB32).copy();
av_frame_unref(m_frame);
return true;
}
}
bool VideoPlayer::getNextFrame(QImage &image)
{
if (!m_isOpen) {
qWarning() << "VideoPlayer not open";
return false;
}
if (m_endOfFile) {
return false;
}
if (decodeNextFrame()) {
image = m_currentImage;
return true;
}
return false;
}
void VideoPlayer::reset()
{
if (!m_isOpen) return;
// 关闭并重新打开解码器
avcodec_flush_buffers(m_codecCtx);
// 重新定位到文件开头
avformat_seek_file(m_formatCtx, -1, 0, 0, 0, AVSEEK_FLAG_BACKWARD);
m_endOfFile = false;
// 清空包
if (m_packet) {
av_packet_unref(m_packet);
}
}
void VideoPlayer::close()
{
if (!m_isOpen) return;
cleanup();
m_isOpen = false;
m_endOfFile = false;
qDebug() << "VideoPlayer closed";
}
void VideoPlayer::cleanup()
{
if (m_swsCtx) {
sws_freeContext(m_swsCtx);
m_swsCtx = nullptr;
}
if (m_rgbBuffer) {
av_free(m_rgbBuffer);
m_rgbBuffer = nullptr;
}
if (m_frameRGB) {
av_frame_free(&m_frameRGB);
m_frameRGB = nullptr;
}
if (m_frame) {
av_frame_free(&m_frame);
m_frame = nullptr;
}
if (m_packet) {
av_packet_free(&m_packet);
m_packet = nullptr;
}
if (m_codecCtx) {
avcodec_free_context(&m_codecCtx);
m_codecCtx = nullptr;
}
if (m_formatCtx) {
avformat_close_input(&m_formatCtx);
m_formatCtx = nullptr;
}
}
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTimer>
#include <QFile>
#include "videorecorder.h"
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
void onTimeout_10s(); //只录制10s
void onTimeout_noRestrictions(); //不限制时间
void onRecordingStopped(bool success);
void on_pushButton_2_clicked();
void on_pushButton_3_clicked();
private:
void videoRecorder();
Ui::Widget *ui;
VideoRecorder* m_recorder = nullptr;
VideoSaver m_videoSaver;
QTimer* m_timer = nullptr;
int m_frameCount = 0;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QTime>
#include <QRandomGenerator>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
qDebug() << "FFmpeg configuration:" << avcodec_configuration();
qDebug() << "FFmpeg version:" << avcodec_version();
// 初始化随机数生成器
QRandomGenerator::global()->generate();
// 创建录制器
m_timer = new QTimer(this);
}
Widget::~Widget()
{
delete ui;
if (m_recorder) {
m_recorder->stopRecording();
m_recorder->deleteLater();
}
if (m_timer) {
m_timer->stop();
delete m_timer;
}
}
void Widget::on_pushButton_clicked()
{
// 如果已经在录制,先停止之前的
if (m_recorder) {
m_recorder->stopRecording();
if (m_timer) {
m_timer->stop();
}
m_recorder->deleteLater();
m_recorder = nullptr;
}
videoRecorder();
}
void Widget::onTimeout_10s()
{
if (!m_recorder) {
return;
}
// 创建测试图片(模拟摄像头数据)
QImage image(1920, 1080, QImage::Format_RGB32);
// 创建渐变效果,更容易看出视频是否正常
for (int y = 0; y < image.height(); ++y) {
QRgb* line = reinterpret_cast<QRgb*>(image.scanLine(y));
int r = (y + m_frameCount) % 255;
int g = (m_frameCount * 2) % 255;
int b = (y * 2 + m_frameCount) % 255;
for (int x = 0; x < image.width(); ++x) {
line[x] = qRgb(r, g, b);
}
}
//测试 addFrame 输入
QByteArray imageData(reinterpret_cast<const char*>(image.bits()),
image.sizeInBytes());
// 添加帧到录制器
m_recorder->addFrame(image);
m_frameCount++;
// 显示进度(每30帧显示一次)
if (m_frameCount % 30 == 0) {
qDebug() << "Added frame:" << m_frameCount;
}
// 录制300帧后自动停止(约10秒,30fps)
if (m_frameCount >= 300) {
qDebug() << "Auto stop after 300 frames";
m_recorder->stopRecording();
if (m_timer) {
m_timer->stop();
}
}
}
void Widget::onTimeout_noRestrictions()
{
// 创建测试图片(模拟摄像头数据)
QImage image(1920, 1080, QImage::Format_RGB32);
// 创建渐变效果,更容易看出视频是否正常
for (int y = 0; y < image.height(); ++y) {
QRgb* line = reinterpret_cast<QRgb*>(image.scanLine(y));
int r = (y + m_frameCount) % 255;
int g = (m_frameCount * 2) % 255;
int b = (y * 2 + m_frameCount) % 255;
for (int x = 0; x < image.width(); ++x) {
line[x] = qRgb(r, g, b);
}
}
//测试 addFrame 输入
QByteArray imageData(reinterpret_cast<const char*>(image.bits()),
image.sizeInBytes());
// 添加帧到录制器
m_videoSaver.writeFrame(image);
m_frameCount++;
// 显示进度(每30帧显示一次)
if (m_frameCount % 30 == 0) {
qDebug() << "Added frame:" << m_frameCount;
}
}
void Widget::onRecordingStopped(bool success)
{
qDebug() << "Recording stopped signal received, success:" << success;
if (m_timer) {
m_timer->stop();
}
// 延迟删除录制器
if (m_recorder) {
m_recorder->deleteLater();
m_recorder = nullptr;
}
}
void Widget::videoRecorder()
{
qDebug() << "Starting video recorder...";
// 创建录制器
m_recorder = new VideoRecorder(this);
// 设置视频参数
m_recorder->setVideoParameters(1920, 1080, 30, 2000000);
// 连接信号槽
connect(m_recorder, &VideoRecorder::recordingStarted, this, []() {
qDebug() << "Recording started!";
});
connect(m_recorder, &VideoRecorder::recordingStopped, this, &Widget::onRecordingStopped);
connect(m_recorder, &VideoRecorder::errorOccurred, this, [](const QString& error) {
qDebug() << "Error:" << error;
});
connect(m_recorder, &VideoRecorder::frameEncoded, this, [](int count) {
// 每30帧输出一次日志,减少刷屏
static int lastLogCount = 0;
if (count - lastLogCount >= 30) {
qDebug() << "Encoded frame count:" << count;
lastLogCount = count;
}
});
// 开始录制
if (!m_recorder->startRecording("output.mp4")) {
qDebug() << "Failed to start recording";
m_recorder->deleteLater();
m_recorder = nullptr;
return;
}
// 开始模拟数据源
m_frameCount = 0;
connect(m_timer, &QTimer::timeout, this, &Widget::onTimeout_10s);
m_timer->start(33); // 约30fps (1000/30 ≈ 33ms)
qDebug() << "Timer started, will generate frames every 33ms";
}
void Widget::on_pushButton_2_clicked()
{
static int m_clicked = 0;
if(((m_clicked++) % 2) == 0){
qDebug()<<"start";
ui->pushButton_2->setText(QString::fromLocal8Bit("停止录制-不限时"));
// 如果已经在录制,先停止之前的
m_videoSaver.close();
// 开始录制
if (!m_videoSaver.open("output.mp4", 1920, 1080)) {
m_videoSaver.close();
return;
}
// 开始模拟数据源
m_frameCount = 0;
connect(m_timer, &QTimer::timeout, this, &Widget::onTimeout_noRestrictions);
m_timer->start(33); // 约30fps (1000/30 ≈ 33ms)
qDebug() << "Timer started, will generate frames every 33ms";
}else{
qDebug()<<"stop";
ui->pushButton_2->setText(QString::fromLocal8Bit("开始录制-不限时"));
if (m_timer) {
m_timer->stop();
}
m_videoSaver.close();
}
}
void delay_ms(int ms)
{
QTime stopTime;
stopTime.start();
while (stopTime.elapsed() < ms) {
QCoreApplication::processEvents();
}
}
//void Widget::on_pushButton_3_clicked()
//{
// int ret;
// int streamIndex = -1;
// AVPacket *pkt = nullptr;
// AVCodec *codec = nullptr;
// AVFrame *frame = nullptr;
// AVFrame *frameRGB = nullptr;
// AVCodecContext *codecCtx = nullptr;
// AVFormatContext *formatCtx = nullptr;
// struct SwsContext *swsCtx = nullptr;
// QString playUrl = "output.mp4";
// // 1. 打开输入文件
// formatCtx = avformat_alloc_context();
// if (avformat_open_input(&formatCtx, playUrl.toStdString().c_str(), nullptr, nullptr) != 0) {
// qDebug() << "Failed to open input file.";
// return;
// }
// // 2. 获取流信息
// if (avformat_find_stream_info(formatCtx, nullptr) < 0) {
// qDebug() << "Failed to find stream info.";
// goto cleanup;
// }
// // 3. 查找视频流索引
// for (unsigned int i = 0; i < formatCtx->nb_streams; i++) {
// if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// streamIndex = i;
// break;
// }
// }
// if (streamIndex == -1) {
// qDebug() << "No video stream found.";
// goto cleanup;
// }
// // 4. 初始化解码器
// codecCtx = avcodec_alloc_context3(nullptr);
// avcodec_parameters_to_context(codecCtx, formatCtx->streams[streamIndex]->codecpar);
// codec = avcodec_find_decoder(codecCtx->codec_id);
// if (!codec || avcodec_open2(codecCtx, codec, nullptr) < 0) {
// qDebug() << "Failed to open decoder.";
// goto cleanup;
// }
// // 5. 准备帧和格式转换
// pkt = av_packet_alloc();
// frame = av_frame_alloc();
// frameRGB = av_frame_alloc();
// uint8_t *buffer = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_RGB32,
// codecCtx->width, codecCtx->height, 1));
// av_image_fill_arrays(frameRGB->data, frameRGB->linesize, buffer,
// AV_PIX_FMT_RGB32, codecCtx->width, codecCtx->height, 1);
// swsCtx = sws_getContext(codecCtx->width, codecCtx->height, codecCtx->pix_fmt,
// codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB32,
// SWS_BICUBIC, nullptr, nullptr, nullptr);
// // 6. 解码循环
// while (av_read_frame(formatCtx, pkt) >= 0) {
// if (pkt->stream_index == streamIndex) {
// ret = avcodec_send_packet(codecCtx, pkt);
// if (ret < 0) {
// av_packet_unref(pkt);
// continue;
// }
// while (ret >= 0) {
// ret = avcodec_receive_frame(codecCtx, frame);
// if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
// break;
// if (ret < 0)
// break;
// sws_scale(swsCtx, frame->data, frame->linesize, 0, codecCtx->height,
// frameRGB->data, frameRGB->linesize);
// QImage img(frameRGB->data[0], codecCtx->width, codecCtx->height, QImage::Format_RGB32);
// ui->label->setPixmap(QPixmap::fromImage(img));
// delay_ms(30);
// }
// }
// av_packet_unref(pkt);
// }
// qDebug() << "Play finished!";
// ui->label->setText(QString::fromLocal8Bit("视频播放完毕"));
//cleanup:
// // 7. 释放资源
// if (swsCtx) sws_freeContext(swsCtx);
// if (frameRGB) av_frame_free(&frameRGB);
// if (frame) av_frame_free(&frame);
// if (pkt) av_packet_free(&pkt);
// if (codecCtx) avcodec_free_context(&codecCtx);
// if (formatCtx) avformat_close_input(&formatCtx);
//}
void Widget::on_pushButton_3_clicked()
{
// 创建VideoPlayer实例
VideoPlayer *player = new VideoPlayer(this);
// 打开视频文件
QString playUrl = "output.mp4";
if (!player->open(playUrl)) {
qDebug() << "Failed to open video file";
delete player;
return;
}
// 逐帧获取并显示
QImage frame;
int frameCount = 0;
while (player->getNextFrame(frame)) {
ui->label->setPixmap(QPixmap::fromImage(frame));
frameCount++;
// 延迟控制播放速度
QEventLoop loop;
QTimer::singleShot(30, &loop, &QEventLoop::quit);
loop.exec();
}
qDebug() << "Play finished. Total frames:" << frameCount;
ui->label->setText(QString::fromLocal8Bit("视频播放完毕"));
// 关闭并释放资源
player->close();
delete player;
}

2226

被折叠的 条评论
为什么被折叠?



