设计思路:
- 使用ffmpeg库对视频帧进行解码,将解码后的rgb数据存储在内存块中。
- 在客户端使用Qt框架创建一个QWidget对象,并重写其paintEvent()函数,在该函数中使用QPainter绘制rgb图像。
- 创建一个线程用于接收服务端发送的视频帧,并将解码后的rgb数据传递给QWidget对象进行绘制。
- 使用Qt提供的信号和槽机制实现多线程间同步。
代码实现:
首先需要安装ffmpeg库并配置Qt项目的.pro文件,具体步骤可以参考Qt官网文档。
定义一个VideoDec类,用于视频帧解码和rgb数据存储:
class VideoDec : public QObject
{
Q_OBJECT
public:
VideoDec(QObject *parent = nullptr);
~VideoDec();
bool open(const QString &fileName); // 打开视频文件
void close(); // 关闭视频文件
bool decode(); // 解码一帧视频
uint8_t* getRgbData(); // 获取rgb数据指针
int getWidth(); // 获取视频宽度
int getHeight(); // 获取视频高度
private:
AVFormatContext *m_formatCtx;
AVCodecContext *m_codecCtx;
AVFrame *m_frame;
uint8_t *m_rgbData;
struct SwsContext *m_swsCtx;
int m_videoStreamIndex;
int m_width;
int m_height;
};
VideoDec::VideoDec(QObject *parent)
: QObject(parent)
, m_formatCtx(nullptr)
, m_codecCtx(nullptr)
, m_frame(nullptr)
, m_rgbData(nullptr)
, m_swsCtx(nullptr)
, m_videoStreamIndex(-1)
, m_width(0)
, m_height(0)
{
}
VideoDec::~VideoDec()
{
close();
}
bool VideoDec::open(const QString &fileName)
{
// 打开视频文件
if (avformat_open_input(&m_formatCtx, fileName.toStdString().c_str(), nullptr, nullptr) < 0) {
qDebug() << "Failed to open file";
return false;
}
// 查找流信息
if (avformat_find_stream_info(m_formatCtx, nullptr) < 0) {
qDebug() << "Failed to find stream info";
avformat_close_input(&m_formatCtx);
return false;
}
// 查找视频流索引
for (int i = 0; i < m_formatCtx->nb_streams; i++) {
if (m_formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_videoStreamIndex = i;
break;
}
}
// 没有找到视频流
if (m_videoStreamIndex == -1) {
qDebug() << "Failed to find video stream";
avformat_close_input(&m_formatCtx);
return false;
}
// 创建解码器并打开
AVCodec *codec = avcodec_find_decoder(m_formatCtx->streams[m_videoStreamIndex]->codecpar->codec_id);
if (!codec) {
qDebug() << "Failed to find codec";
avformat_close_input(&m_formatCtx);
return false;
}
m_codecCtx = avcodec_alloc_context3(codec);
if (avcodec_parameters_to_context(m_codecCtx, m_formatCtx->streams[m_videoStreamIndex]->codecpar) < 0) {
qDebug() << "Failed to set codec parameters";
avformat_close_input(&m_formatCtx);
return false;
}
if (avcodec_open2(m_codecCtx, codec, nullptr) < 0) {
qDebug() << "Failed to open codec";
avcodec_free_context(&m_codecCtx);
avformat_close_input(&m_formatCtx);
return false;
}
// 创建一个空帧用于存放解码后的数据
m_frame = av_frame_alloc();
if (!m_frame) {
qDebug() << "Failed to allocate frame";
avcodec_free_context(&m_codecCtx);
avformat_close_input(&m_formatCtx);
return false;
}
// 计算rgb数据内存大小并分配内存
m_width = m_codecCtx->width;
m_height = m_codecCtx->height;
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, m_width, m_height, 1);
m_rgbData = static_cast<uint8_t*>(av_malloc(numBytes * sizeof(uint8_t)));
if (!m_rgbData) {
qDebug() << "Failed to allocate rgb data";
av_frame_free(&m_frame);
avcodec_free_context(&m_codecCtx);
avformat_close_input(&m_formatCtx);
return false;
}
// 创建图像转换上下文
m_swsCtx = sws_getContext(m_width, m_height, m_codecCtx->pix_fmt,
m_width, m_height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!m_swsCtx) {
qDebug() << "Failed to create sws context";
av_free(m_rgbData);
av_frame_free(&m_frame);
avcodec_free_context(&m_codecCtx);
avformat_close_input(&m_formatCtx);
return false;
}
qDebug() << "Video width:" << m_width << "height:" << m_height;
return true;
}
void VideoDec::close()
{
if (m_swsCtx) {
sws_freeContext(m_swsCtx);
m_swsCtx = nullptr;
}
if (m_rgbData) {
av_free(m_rgbData);
m_rgbData = 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) {
avformat_close_input(&m_formatCtx);
m_formatCtx = nullptr;
}
}
bool VideoDec::decode()
{
AVPacket packet;
int ret = av_read_frame(m_formatCtx, &packet);
if (ret < 0) {
qDebug() << "Failed to read frame";
return false;
}
if (packet.stream_index != m_videoStreamIndex) {
av_packet_unref(&packet);
return false;
}
ret = avcodec_send_packet(m_codecCtx, &packet);
if (ret < 0) {
qDebug() << "Failed to send packet for decoding";
av_packet_unref(&packet);
return false;
}
ret = avcodec_receive_frame(m_codecCtx, m_frame);
if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
qDebug() << "Failed to receive frame";
av_packet_unref(&packet);
return false;
}
if (ret >= 0) {
sws_scale(m_swsCtx, m_frame->data, m_frame->linesize, 0, m_height,
&m_rgbData, nullptr);
av_packet_unref(&packet);
return true;
}
av_packet_unref(&packet);
return false;
}
uint8_t* VideoDec::getRgbData()
{
return m_rgbData;
}
int VideoDec::getWidth()
{
return m_width;
}
int VideoDec::getHeight()
{
return m_height;
}
在客户端中创建一个VideoWidget类,用于绘制rgb图像:
class VideoWidget : public QWidget
{
Q_OBJECT
public:
VideoWidget(QWidget *parent = nullptr);
~VideoWidget();
protected:
void paintEvent(QPaintEvent *event) override;
private:
QImage m_image;
};
VideoWidget::VideoWidget(QWidget *parent)
: QWidget(parent)
{
}
VideoWidget::~VideoWidget()
{
}
void VideoWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.drawImage(0, 0, m_image);
}
在主窗口中创建一个VideoPlayer类,用于视频播放和rgb数据传递:
class VideoPlayer : public QObject
{
Q_OBJECT
public:
VideoPlayer(QObject *parent = nullptr);
bool open(const QString &fileName);
void close();
bool play(); // 开始播放视频
void stop(); // 停止播放视频
signals:
void imageReady(); // rgb数据已准备好的信号
private slots:
void processFrame(); // 处理一帧视频帧的槽函数
private:
QThread m_thread;
VideoDec m_videoDec; // 视频解码器
bool m_stopFlag;
};
VideoPlayer::VideoPlayer(QObject *parent)
: QObject(parent)
, m_stopFlag(true)
{
m_videoDec.moveToThread(&m_thread);
connect(&m_thread, &QThread::started, &m_videoDec, [&]() {
while (!m_stopFlag && m_videoDec.decode()) {
emit imageReady();
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
});
m_thread.start();
}
bool VideoPlayer::open(const QString &fileName)
{
return m_videoDec.open(fileName);
}
void VideoPlayer::close()
{
m_videoDec.close();
}
bool VideoPlayer::play()
{
if (m_stopFlag) {
m_stopFlag = false;
QMetaObject::invokeMethod(&m_videoDec, &VideoDec::decode);
return true;
}
return false;
}
void VideoPlayer::stop()
{
m_stopFlag = true;
m_thread.quit();
m_thread.wait();
}
void VideoPlayer::processFrame()
{
QImage image(m_videoDec.getRgbData(), m_videoDec.getWidth(), m_videoDec.getHeight(),
QImage::Format_RGB888);
static_cast<VideoWidget*>(parent())->setPixmap(QPixmap::fromImage(image));
}
使用时,在主窗口的构造函数中创建一个VideoWidget对象和一个VideoPlayer对象,将VideoWidget对象添加到布局中,并连接VideoPlayer对象的imageReady()信号和VideoWidget对象的update()槽函数:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_videoWidget(new VideoWidget)
, m_videoPlayer(new VideoPlayer)
{
ui->setupUi(this);
QGridLayout *layout = new QGridLayout(ui->centralWidget);
layout->addWidget(m_videoWidget, 0, 0, 1, 1);
connect(m_videoPlayer, &VideoPlayer::imageReady, m_videoWidget, &VideoWidget::update);
}
MainWindow::~MainWindow()
{
delete ui;
}
需要注意的是,由于rgb数据在解码器线程中生成,而QWidget的paintEvent()函数在主线程中执行,因此需要在两个线程之间进行同步。这里使用Qt提供的信号和槽机制实现多线程间同步:当rgb数据准备好后,VideoPlayer对象发送imageReady()信号,VideoWidget对象接收到该信号后调用update()函数触发QWidget的重新绘制操作。由于update()函数在主线程中被调用,因此可以保证多线程间的同步。