我想要使用Qt搭建一个上位机用来接收esp32cam的视频流,,使用的是QNetworkReply等。我把程序全部都放在主线程里的时候运行很正确,但我想把传输视频流的那部分放在另一个线程里就出错了,没有视频显示
videothread1.h
#ifndef VIDEOTHREAD1_H
#define VIDEOTHREAD1_H
#include
// 用于访问esp32cam的webserver,并返回视频流到上位机上
#include
#include
#include
#include
#include
#include
#include
class VideoThread : public QThread
{
Q_OBJECT
public:
explicit VideoThread(QThread *parent = nullptr);
// 通过覆写QThread类的run函数来开启线程
virtual void run() override;
// 分析处理已经经过了一次处理的,完整的视频帧
void ProcessFrameData(const QByteArray& frameData);
signals:
void VideoPixmapSignal(QPixmap* pixmap);
// 摄像头断开发出信号
void LinkWrongSignal();
private slots:
void ReplyRcevSlot();
private:
// 创建两个QByteArray,一个暂存所有的视频数据,一个接收上一个已经处理了的视频数据帧
QByteArray buffer;
QByteArray lastFrameBuffer;
};
#endif // VIDEOTHREAD1_H
videothread1.cpp
#include "videothread1.h"
// esp32cam 使用ap模式默认ip为192.168.4.1
//QString VideoThread::ip = "192.168.4.1";
bool isOpen = false;
bool isPaused = false;
VideoThread::VideoThread(QThread *parent)
:QThread(parent)
{
}
void VideoThread::run()
{
qDebug() << "线程开启";
// 用于请求视频流数据
QNetworkAccessManager* manager = new QNetworkAccessManager(this);
QNetworkRequest request;
// esp32cam视频流使用的是81端口
request.setUrl(QUrl("http://192.168.4.1:81/stream"));
request.setRawHeader("Connection", "Keep-Alive");
request.setRawHeader("User-Agent", "1601");
QNetworkReply* client = manager->get(request);
// 如果发生网络错误执行该槽函数
connect(client, &QNetworkReply::finished, [=]()
{
qDebug() << "NetworkError" << client->error();
emit LinkWrongSignal();
isOpen = false;
});
connect(client, &QNetworkReply::readyRead, this, &VideoThread::ReplyRcevSlot);
}
// 接收视频流数据
void VideoThread::ReplyRcevSlot()
{
qDebug() << "线程开启11111111111111111111";
// 检查当前帧是否有"Content-Type",如果不包含则直接存入上一帧数据lastFrameBuffer
// 如果包含则将前半部分存入lastFrameBuffer,后半部分存入下一帧
QNetworkReply* client = (QNetworkReply*)sender();
if(client->error() == QNetworkReply::NoError)
{
buffer = client->readAll();
// 判断是否存在"Content-Type",并返回它的索引,如果不存在则返回-1
int index = buffer.indexOf("Content-Type");
if(index != -1)
{
if(index > 0)
{
// 处理上一帧未处理完的数据
QByteArray preBuffer = buffer.mid(0,index);
ProcessFrameData(lastFrameBuffer + preBuffer);
}
lastFrameBuffer = buffer.mid(index);
}
// 如果不存在Content-Type
else
{
lastFrameBuffer += buffer;
}
buffer.clear();
}
}
void VideoThread::ProcessFrameData(const QByteArray& frameData)
{
// 处理后的每一帧视频数据frameData 都以
// "Content-Type: image/jpeg\r\nContent-Length: 5925\r\nX-Timestamp: 14.657244\r\n\r\n"开头
// 以"\r\n--123456789000000000000987654321\r\n"结尾
// qDebug() << frameData.left(100);
// qDebug() << frameData.right(100);
// qDebug() << "--------------------------------------";
// Content-Length: 5925表示一帧图像数据有多少个byte,我们需要把这个读取出来作为length
qDebug() << "线程开启2222222222222222222222222222";
int lengthIndex = frameData.indexOf("Content-Length: ");
if(lengthIndex >= 0)
{
// 获取Content-Length: 后面的数字作为length
int endIndex = frameData.indexOf("\r\n", lengthIndex);
int length = frameData.mid(lengthIndex + 16, endIndex - (lengthIndex + 16 - 1)).toInt();
QPixmap* pixmap = new QPixmap;
int startIndex = frameData.indexOf("\r\n\r\n");
auto loadStatus = pixmap->loadFromData(frameData.mid(startIndex + 4, length));
if(!loadStatus)
{
qDebug() << "video load failed";
return;
}
if(!isPaused)
emit VideoPixmapSignal(pixmap);
}
}
widget.cpp
#include "widget.h"
#include "ui_widget.h"
extern bool isOpen;
extern bool isPaused;
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
tcpClient = new QTcpSocket;
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_startBtn_clicked()
{
// 获取当前连接的wifi名称
GetCurrentConnectedWiFi();
// softAPBroadcastIP是一个网络术语,
// 通常用于描述在软件访问点(soft AP)模式下,无线网络接口设备(如路由器或无线网卡)使用的广播IP地址。
// 在无线网络中,广播IP地址通常用于向网络中的所有设备发送广播消息。
// 在soft AP模式下,网络接口设备充当一个虚拟的访问点,允许其他设备连接到它并共享网络连接。
// 这个虚拟访问点使用软件生成的IP地址,通常被称为“softAP IP地址”。
// 而softAPBroadcastIP则是用于在该访问点下发送广播消息的IP地址,它通常被设置为子网掩码中的广播地址。
// 因此,softAPBroadcastIP是soft AP模式下无线网络设备使用的广播IP地址,用于向该访问点下连接的所有设备发送广播消息。
if(connectedSsid == "192.168.4.255") // 192.168.4.255是连接上esp32cam ap模式的热点后返回的ip,
// 即softAPBroadcastIp
{
if(!isOpen)
{
// 用于连接tcp服务器
tcpClient->connectToHost("192.168.4.1", 22333); // 使用8000端口作为tcp通讯
connect(tcpClient, &QTcpSocket::connected, this, &Widget::TcpRecvSlot);
connect(tcpClient, &QTcpSocket::readyRead, [this]()
{
qDebug() << tcpClient->readAll();
});
Start();
isOpen = true;
}
else
{
QMessageBox::information(this,"警告","请勿重复推送");
}
}
else
QMessageBox::warning(this, "警告", "未连接" + connectedSsid);
}
void Widget::on_stopBtn_clicked() // 用于暂停或者继续播放视频流
{
if(isOpen)
{
if(isPaused)
{
ui->stopBtn->setText("暂停推送");
isPaused = false;
}
else
{
ui->stopBtn->setText("继续推送");
isPaused = true;
}
}
else
{
QMessageBox::information(this,"警告","请先推送视频流");
}
}
void Widget::TcpRecvSlot()
{
QMessageBox::information(this, "提示", "Connected to ESP32-CAM TCP server!");
}
void Widget::Start()
{
VideoThread* videoThread = new VideoThread;
videoThread->start();
connect(videoThread, &VideoThread::VideoPixmapSignal, this, &Widget::RecvVideoSlot);
connect(videoThread, &VideoThread::LinkWrongSignal, [this]()
{
QMessageBox::information(this, "警告", "摄像头断开");
});
}
void Widget::RecvVideoSlot(QPixmap* pixmap)
{
ui->labelDisplay->setPixmap(*pixmap);
}
void Widget::GetCurrentConnectedWiFi()
{
QString ssid;
// 使用QNetworkInterface来获取wifi的名称
foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces())
{
// Skip loopback and inactive interfaces
if (interface.flags().testFlag(QNetworkInterface::IsLoopBack) ||
!(interface.flags().testFlag(QNetworkInterface::IsUp) &&
interface.flags().testFlag(QNetworkInterface::IsRunning)))
continue;
// Find the first non-internal IPv4 address
foreach(QNetworkAddressEntry entry, interface.addressEntries()) {
if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol ||
entry.ip().isLoopback() ||
entry.ip().isMulticast() ||
entry.ip().isNull())
continue;
ssid = entry.broadcast().toString();
break;
}
if (!ssid.isEmpty())
break;
}
if (!ssid.isEmpty())
{
qDebug() << "Current network name: " << ssid;
connectedSsid = ssid;
}
else
{
qDebug() << "No active network connection found";
// QMessageBox::information(this,"警告","未连接WiFi");
}
}
void Widget::on_qjBtn_clicked()
{
tcpClient->write("qj\r");
// 在Qt的QTcpSocket类中,flush()函数的作用是将未发送的数据立即写入套接字。
// 它通常在数据发送之后调用,以确保数据被立即发送,而不是等待缓冲区满或等待操作系统自动发送数据。
// 当向套接字写入数据时,数据将首先被写入缓冲区。
// 当缓冲区达到一定大小时,数据将被自动发送到目标套接字。
// 但是,如果您希望立即发送数据,而不等待缓冲区满或等待操作系统自动发送数据,则可以调用flush()函数。
tcpClient->flush();
}
void Widget::on_htBtn_clicked()
{
tcpClient->write("ht\r");
tcpClient->flush();
}
void Widget::on_zzBtn_clicked()
{
tcpClient->write("zz\r");
tcpClient->flush();
}
void Widget::on_yzBtn_clicked()
{
tcpClient->write("yz\r");
tcpClient->flush();
}
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include
// 用于取得连接的wifi名称,用于判断是否连接上摄像头
#include
// 用于上位机与esp32cam的通讯,上位机作为TCP客户端,esp32cam使用ap模式做TCP服务器
#include
// 视频流线程用于传输摄像头视频,主线程用于与esp32cam进行tcp通讯
#include "videothread1.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
// 开始推送流
void Start();
// 获取wifi名称
void GetCurrentConnectedWiFi();
private slots:
void on_startBtn_clicked();
void on_stopBtn_clicked();
void TcpRecvSlot();
void on_qjBtn_clicked();
void on_htBtn_clicked();
void on_zzBtn_clicked();
void on_yzBtn_clicked();
// 将视频传输线程里返回的图像显示到ui上
void RecvVideoSlot(QPixmap* pixmap);
private:
Ui::Widget *ui;
// 连接的wifi的名称
QString connectedSsid;
// tcp客户端
QTcpSocket* tcpClient;
};
#endif // WIDGET_H
基于Monster 组和GPT的调写:
基于最新版ChatGPT4的回答,望采纳!!!有其他问题也可以询问我哦💕(最新版更智能,功能更加强大):
根据您提供的代码,您想要使用Qt创建一个上位机应用程序,以接收esp32cam的视频流。当您将所有程序放在主线程中运行时,一切都正常。但是,当您尝试将视频流传输部分放入另一个线程中时,会出现错误,导致无法显示视频。
在此情况下,您可能需要检查以下几点:
确保线程的创建和使用是正确的。您可以查看一些关于Qt线程使用的教程或文档,以确保您正确地使用了线程。
检查信号与槽之间的连接是否正确。您可以查看Qt关于信号与槽的教程或文档,了解如何正确地连接它们。
在多线程编程中,同步和数据共享可能会导致问题。请确保您正确地处理了多线程同步和数据共享。在Qt中,您可以使用QMutex、QSemaphore等类来帮助实现线程同步。
请确保从视频线程发送的信号和主线程接收的槽函数之间的数据类型匹配。例如,如果您在信号中发送的是QPixmap指针,那么在槽函数中也应该接收QPixmap指针。
在多线程环境中,Qt对象的生命周期管理尤为重要。当从一个线程向另一个线程发送Qt对象时,您需要确保该对象在线程之间传递时不会被删除。您可以使用QSharedPointer来管理Qt对象的生命周期,确保在需要时它们不会被意外删除。
以上是一些建议,您可以参考这些内容来查找并修复您的程序中的问题。希望对您有所帮助!