1. 从零开始:Qt+FFmpeg解码H265视频流
第一次接触视频流处理时,我被各种专业术语搞得晕头转向。直到把Qt和FFmpeg这对黄金组合用起来,才发现解码H265视频并没有想象中复杂。这里分享一个真实案例:某小区需要实时显示高空抛物监控画面,并在视频上叠加抛物轨迹分析结果。通过Qt的界面能力加上FFmpeg的解码能力,我们只用300行核心代码就实现了这个功能。
FFmpeg处理视频流就像工厂的流水线:先拆包装(解析数据包),再加工(解码帧数据),最后包装成新产品(转成Qt能显示的格式)。对于H265这种高效编码格式,关键要找到正确的解码器。在代码中你会看到这样的初始化流程:
// 查找HEVC解码器(H265的别名) m_pCodec = avcodec_find_decoder(AV_CODEC_ID_HEVC); if (!m_pCodec) { qDebug() << "未找到H265解码器"; return false; }2. 解码流程深度解析
2.1 数据包处理的艺术
网络摄像头传来的数据就像一堆散装的乐高积木,我们需要先识别出H265数据块。这里用到AVPacket这个容器:
while (nDataSize > 0) { int nLength = av_parser_parse2( m_pCodecParserCtx, m_avCodecCtx, &m_avPacket->data, &m_avPacket->size, pData, nDataSize, AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE); // 移动数据指针 nDataSize -= nLength; data.remove(0, nLength); if (m_avPacket->size == 0) continue; // 识别帧类型(I帧/P帧/B帧) switch(m_pCodecParserCtx->pict_type) { case AV_PICTURE_TYPE_I: qDebug() << "关键帧到来"; break; case AV_PICTURE_TYPE_P: qDebug() << "预测帧到来"; break; } }2.2 现代解码API的正确姿势
很多老教程还在用avcodec_decode_video2,但在FFmpeg4.0+中应该使用新型的双调用模式:
// 发送数据包到解码器 int ret = avcodec_send_packet(m_avCodecCtx, m_avPacket); if (ret < 0) { qDebug() << "发送数据包失败"; continue; } // 循环获取解码后的帧 while (ret >= 0) { ret = avcodec_receive_frame(m_avCodecCtx, m_avFrameInput); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { qDebug() << "解码错误"; break; } // 这里获取到完整的视频帧m_avFrameInput processDecodedFrame(m_avFrameInput); }3. 图像转换与智能分析叠加
3.1 YUV到RGB的魔法转换
解码出来的YUV420P格式就像彩色铅笔的线稿,需要转换成Qt能直接绘制的RGB画布:
// 初始化转换上下文 m_pSwsContext = sws_getContext( m_nVideoWidth, m_nVideoHeight, m_avCodecCtx->pix_fmt, m_nVideoWidth, m_nVideoHeight, AV_PIX_FMT_RGB32, SWS_FAST_BILINEAR, NULL, NULL, NULL); // 执行转换 sws_scale(m_pSwsContext, (const uint8_t* const*)m_avFrameInput->data, m_avFrameInput->linesize, 0, m_nVideoHeight, m_avFramePicture->data, m_avFramePicture->linesize);3.2 智能分析结果叠加实战
高空抛物检测系统会给出物体坐标轨迹,我们需要用OpenCV绘制到图像上:
// Qt图像转OpenCV Mat QImage qtImage(m_avFramePicture->data[0], m_nVideoWidth, m_nVideoHeight, QImage::Format_RGB32); cv::Mat matImage = cv::Mat( qtImage.height(), qtImage.width(), CV_8UC4, (void*)qtImage.constBits(), qtImage.bytesPerLine()); // 绘制抛物轨迹(假设pTemp包含坐标数据) for(auto& point : pTemp->trajectory) { cv::circle(matImage, cv::Point(point.x, point.y), 5, cv::Scalar(0,0,255), -1); } // 转回Qt图像显示 QImage resultImage( (const uchar*)matImage.data, matImage.cols, matImage.rows, matImage.step, QImage::Format_RGB32); emit frameReady(resultImage); // 发送信号更新UI4. 性能优化与坑点指南
4.1 内存管理要点
FFmpeg的内存管理就像借书证制度,有借必须有还。常见的内存泄漏点:
// 正确释放方式示例 av_packet_unref(m_avPacket); // 释放数据包 av_frame_unref(m_avFrameInput); // 释放帧 // 初始化时记得分配内存 m_avPacket = av_packet_alloc(); m_avFrameInput = av_frame_alloc();4.2 多线程处理方案
视频解码是CPU密集型任务,推荐采用生产者-消费者模式:
- 网络线程:接收原始数据放入队列
- 解码线程:从队列取数据解码
- UI线程:通过信号槽获取最终图像
// 示例队列操作 m_pDataMutex->lock(); m_qDataArray.enqueue(data); // 生产者放入数据 m_pDataMutex->unlock(); // 在解码线程中 m_pDataMutex->lock(); if (!m_qDataArray.isEmpty()) { QByteArray data = m_qDataArray.dequeue(); // 处理数据... } m_pDataMutex->unlock();4.3 硬件加速方案
对于4K等高分辨率视频,可以考虑硬件解码:
// 尝试使用CUDA加速 AVBufferRef* hw_ctx; av_hwdevice_ctx_create(&hw_ctx, AV_HWDEVICE_TYPE_CUDA, NULL, NULL, 0); m_avCodecCtx->hw_device_ctx = av_buffer_ref(hw_ctx);记得检查FFmpeg编译时是否包含了对应硬件加速支持。