最近工作需要用QT做一个网络摄像头测试,简单记录:
服务端,主机配置为Ubuntu,通过端口12345采集传输MJPEG格式图片
windows客户端,QT Creator通过ip地址连接访问

提前准备
服务端需要安装QT5
bash
sudo apt-get install qt5-default
g++
qmake
客户端需要安装Qt Creator
服务端代码
cameraserver.cpp
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
cpp
#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
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 image
QImage 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 frame
if (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 frame
QByteArray frameData = socket->read(frameSize);
readingFrame = false;
// Decode JPEG to QImage
currentFrame = 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
cpp
#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_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
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
bash
<?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应该会遇到一些阻力,不过可以期待一下