Linux和Windows基于V4L2和TCP的QT监控

最近工作需要用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应该会遇到一些阻力,不过可以期待一下

相关推荐
~黄夫人~4 分钟前
Ubuntu系统快速上手命令(详细)
linux·运维·笔记·ubuntu·postgresql
老歌老听老掉牙5 分钟前
使用 OpenCASCADE 提取布尔运算后平面图形的外轮廓
c++·平面·opencascade
发光的沙子10 分钟前
FPGA----petalinux的Ubuntu文件系统移植
linux·运维·ubuntu
lili-felicity16 分钟前
解决VMware Workstation Pro 17中Ubuntu 24.04无法复制粘贴
linux·运维·ubuntu
Lzc77416 分钟前
Linux网络的应用层自定义协议
linux·应用层自定义协议与序列化
闻缺陷则喜何志丹21 分钟前
【动态规划】数位DP的原理、模板(封装类)
c++·算法·动态规划·原理·模板·数位dp
胖咕噜的稞达鸭39 分钟前
二叉树搜索树插入,查找,删除,Key/Value二叉搜索树场景应用+源码实现
c语言·数据结构·c++·算法·gitee
虚伪的空想家1 小时前
HUAWEI A800I A2 aarch64架构服务器鲲鹏920开启虚拟化功能
linux·运维·服务器·显卡·npu·huawei·鲲鹏920
进击的大海贼1 小时前
QT-C++ 自定义加工统计通用模块
开发语言·c++·qt
笨蛋少年派1 小时前
将 MapReduce 程序打成 JAR 包并在 Linux 虚拟机的 Hadoop 集群上运行
linux·jar·mapreduce