最近工作需要用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應該會遇到一些阻力,不過可以期待一下