Linux和Windows基于V4L2和TCP的QT监控
最近工作需要用QT做一个网络摄像头测试,简单记录:
服务端,主机配置为Ubuntu,通过端口12345采集传输MJPEG格式图片
windows客户端,QT Creator通过ip地址连接访问
提前准备
服务端需要安装QT5
sudo apt-get install qt5-default
g++
qmake
客户端需要安装Qt Creator
服务端代码
cameraserver.cpp
#include "cameraserver.h"CameraServer::CameraServer(QObject *parent) : QObject(parent), camera_fd(-1), buffer(nullptr), buffer_length(0)
{server = new QTcpServer(this);connect(server, &QTcpServer::newConnection, this, &CameraServer::newConnection);if (!server->listen(QHostAddress::Any, 12345)) {qDebug() << "Server could not start!";} else {qDebug() << "Server started on port 12345";}// 打开摄像头camera_fd = open("/dev/video0", O_RDWR | O_NONBLOCK, 0);if (camera_fd == -1) {qDebug() << "Failed to open camera:" << strerror(errno);return;}// 设置摄像头格式struct v4l2_format format;memset(&format, 0, sizeof(format));format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;format.fmt.pix.width = 640;format.fmt.pix.height = 480;format.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;format.fmt.pix.field = V4L2_FIELD_NONE;if (ioctl(camera_fd, VIDIOC_S_FMT, &format) == -1) {qDebug() << "Failed to set camera format:" << strerror(errno);close(camera_fd);camera_fd = -1;return;}// 请求缓冲区struct v4l2_requestbuffers req;memset(&req, 0, sizeof(req));req.count = 1;req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;req.memory = V4L2_MEMORY_MMAP;if (ioctl(camera_fd, VIDIOC_REQBUFS, &req) == -1) {qDebug() << "Failed to request buffers:" << strerror(errno);close(camera_fd);camera_fd = -1;return;}// 映射缓冲区struct v4l2_buffer buf;memset(&buf, 0, sizeof(buf));buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = 0;if (ioctl(camera_fd, VIDIOC_QUERYBUF, &buf) == -1) {qDebug() << "Failed to query buffer:" << strerror(errno);close(camera_fd);camera_fd = -1;return;}buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, camera_fd, buf.m.offset);if (buffer == MAP_FAILED) {qDebug() << "Failed to mmap buffer:" << strerror(errno);close(camera_fd);camera_fd = -1;return;}buffer_length = buf.length;// 将缓冲区加入队列if (ioctl(camera_fd, VIDIOC_QBUF, &buf) == -1) {qDebug() << "Failed to queue buffer:" << strerror(errno);close(camera_fd);camera_fd = -1;return;}// 开启视频流enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;if (ioctl(camera_fd, VIDIOC_STREAMON, &type) == -1) {qDebug() << "Failed to start streaming:" << strerror(errno);close(camera_fd);camera_fd = -1;return;}// 设置定时器timer = new QTimer(this);connect(timer, &QTimer::timeout, this, &CameraServer::captureFrame);timer->start(33); // ~30 FPS
}CameraServer::~CameraServer()
{if (camera_fd != -1) {// 停止视频流enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(camera_fd, VIDIOC_STREAMOFF, &type);if (buffer != MAP_FAILED && buffer != nullptr) {munmap(buffer, buffer_length);}close(camera_fd);}
}void CameraServer::newConnection()
{QTcpSocket *socket = server->nextPendingConnection();qDebug() << "Client connected:" << socket->peerAddress().toString();clients.append(socket);connect(socket, &QTcpSocket::disconnected, this, [this, socket]() {qDebug() << "Client disconnected";clients.removeOne(socket);socket->deleteLater();});
}void CameraServer::captureFrame()
{if (camera_fd == -1 || clients.isEmpty()) return;struct v4l2_buffer buf;memset(&buf, 0, sizeof(buf));buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;// 出队缓冲区if (ioctl(camera_fd, VIDIOC_DQBUF, &buf) == -1) {if (errno != EAGAIN) {qDebug() << "Failed to dequeue buffer:" << strerror(errno);}return;}// 发送帧数据for (QTcpSocket *client : clients) {if (client->state() == QAbstractSocket::ConnectedState) {// 发送帧大小quint32 size = buf.bytesused;client->write(reinterpret_cast<const char*>(&size), sizeof(size));// 发送帧数据client->write(reinterpret_cast<const char*>(buffer), size);}}// 重新入队缓冲区if (ioctl(camera_fd, VIDIOC_QBUF, &buf) == -1) {qDebug() << "Failed to queue buffer:" << strerror(errno);}
}
cameraserver.h
#ifndef CAMERASERVER_H
#define CAMERASERVER_H#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTimer>
#include <QList>
#include <QDebug>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <cstring>
#include <cerrno>class CameraServer : public QObject
{Q_OBJECT
public:explicit CameraServer(QObject *parent = nullptr);~CameraServer();private slots:void newConnection();void captureFrame();private:QTcpServer *server;QList<QTcpSocket*> clients;int camera_fd;QTimer *timer;void *buffer;size_t buffer_length;
};#endif // CAMERASERVER_H
缓冲区增加声明
客户端QT代码
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QBuffer>
#include <QMessageBox>
#include <QHostAddress> // 添加头文件MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setWindowTitle("Camera Stream Client");socket = new QTcpSocket(this);connect(socket, &QTcpSocket::connected, this, &MainWindow::on_socketConnected);connect(socket, &QTcpSocket::disconnected, this, &MainWindow::on_socketDisconnected);// 修复1: 使用兼容Qt 5.14的错误处理方式connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error),this, &MainWindow::on_socketError);connect(socket, &QTcpSocket::readyRead, this, &MainWindow::on_socketReadyRead);// Set placeholder imageQImage placeholder(640, 480, QImage::Format_RGB888);placeholder.fill(Qt::darkGray);ui->imageLabel->setPixmap(QPixmap::fromImage(placeholder));ui->imageLabel->setScaledContents(true);ui->ipLineEdit->setText("192.168.1.100"); // Default IP
}MainWindow::~MainWindow() {delete ui;
}void MainWindow::on_connectButton_clicked() {if (socket->state() == QAbstractSocket::ConnectedState) {socket->disconnectFromHost();ui->connectButton->setText("Connect");ui->statusLabel->setText("Disconnected");return;}QString ip = ui->ipLineEdit->text();if (ip.isEmpty()) {QMessageBox::warning(this, "Error", "Please enter server IP address");return;}ui->connectButton->setText("Connecting...");ui->connectButton->setEnabled(false);socket->connectToHost(ip, 12345);
}void MainWindow::on_socketConnected() {ui->connectButton->setText("Disconnect");ui->connectButton->setEnabled(true);// 修复2: 使用正确的QHostAddress方法ui->statusLabel->setText("Connected to " + socket->peerAddress().toString());
}void MainWindow::on_socketDisconnected() {ui->connectButton->setText("Connect");ui->statusLabel->setText("Disconnected");
}// 修改错误处理函数的签名
void MainWindow::on_socketError(QAbstractSocket::SocketError error) {Q_UNUSED(error);ui->connectButton->setText("Connect");ui->connectButton->setEnabled(true);ui->statusLabel->setText("Error: " + socket->errorString());
}void MainWindow::on_socketReadyRead() {while (socket->bytesAvailable() > 0) {if (!readingFrame) {// Start reading a new frameif (socket->bytesAvailable() < static_cast<qint64>(sizeof(quint32))) {return; // Wait for more data}socket->read(reinterpret_cast<char*>(&frameSize), sizeof(quint32));readingFrame = true;}if (socket->bytesAvailable() < frameSize) {return; // Wait for the complete frame}// Read the complete frameQByteArray frameData = socket->read(frameSize);readingFrame = false;// Decode JPEG to QImagecurrentFrame = QImage::fromData(frameData, "JPEG");if (!currentFrame.isNull()) {ui->imageLabel->setPixmap(QPixmap::fromImage(currentFrame));ui->statusLabel->setText(QString("Received frame: %1x%2").arg(currentFrame.width()).arg(currentFrame.height()));}}
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include <QTcpSocket>
#include <QImage>
#include <QLabel>
#include <QHostAddress> // 添加头文件QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void on_connectButton_clicked();void on_socketConnected();void on_socketDisconnected();void on_socketError(QAbstractSocket::SocketError error); // 保持原签名void on_socketReadyRead();private:Ui::MainWindow *ui;QTcpSocket *socket;QImage currentFrame;bool readingFrame = false;quint32 frameSize = 0;
};
#endif // MAINWINDOW_H
mainwindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"><class>MainWindow</class><widget class="QMainWindow" name="MainWindow"><property name="geometry"><rect><x>0</x><y>0</y><width>800</width><height>600</height></rect></property><property name="windowTitle"><string>Camera Stream Client</string></property><widget class="QWidget" name="centralwidget"><layout class="QVBoxLayout" name="verticalLayout"><item><widget class="QFrame" name="frame"><property name="frameShape"><enum>QFrame::StyledPanel</enum></property><property name="frameShadow"><enum>QFrame::Raised</enum></property><layout class="QHBoxLayout" name="horizontalLayout"><item><widget class="QLabel" name="label"><property name="text"><string>Server IP:</string></property></widget></item><item><widget class="QLineEdit" name="ipLineEdit"/></item><item><widget class="QPushButton" name="connectButton"><property name="text"><string>Connect</string></property></widget></item></layout></widget></item><item><widget class="QLabel" name="imageLabel"><property name="minimumSize"><size><width>640</width><height>480</height></size></property><property name="frameShape"><enum>QFrame::Box</enum></property><property name="text"><string>No Image</string></property><property name="alignment"><set>Qt::AlignCenter</set></property></widget></item><item><widget class="QLabel" name="statusLabel"><property name="text"><string>Disconnected</string></property></widget></item></layout></widget></widget><resources/><connections/>
</ui>
整体框架基于V4L2
演示效果
后续打算在RK3576上面跑一下,虽说arch64架构的Ubuntu,移植QT应该会遇到一些阻力,不过可以期待一下