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应该会遇到一些阻力,不过可以期待一下

相关推荐
飞凌嵌入式5 分钟前
飞凌嵌入式亮相第九届瑞芯微开发者大会:AIoT模型创新重做产品
人工智能·嵌入式硬件·嵌入式·飞凌嵌入式
刚入坑的新人编程27 分钟前
暑期算法训练.8
数据结构·c++·算法·面试·哈希算法
花下的晚风34 分钟前
如何搭建Linux环境下的flink本地集群
linux·flink
TalkU浩克1 小时前
C++中使用Essentia实现STFT/ISTFT
开发语言·c++·音频·istft·stft·essentia
RainbowSea1 小时前
用户中心项目部署上线03
linux·服务器·spring boot
Gss7771 小时前
Vim 编辑器全模式操作指南
linux·编辑器·vim
小比卡丘1 小时前
【C++进阶】第7课—红黑树
java·开发语言·c++
sagima_sdu2 小时前
银河麒麟安装软件商店方法
linux·运维·服务器
不断努力的根号七2 小时前
qt框架,使用webEngine如何调试前端
开发语言·前端·qt
Gene_20224 小时前
Ubuntu 22.04 使用 Issac Gym 进行人形强化学习训练
linux·运维·ubuntu