c++ opencv+http推流摄像头数据

用大模型写的一个简单的http推流demo,用opencv读取摄像头数据作为数据源

#include <opencv2/opencv.hpp>
#include <boost/asio.hpp>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <atomic>
#include <chrono>





class UnifiedMJPEGStreamer {
public:
    // 全局变量用于参数存储
    std::string global_params = "";

protected:
    boost::asio::io_service io_service;
    
private:
    boost::asio::ip::tcp::acceptor acceptor;
    std::vector<std::shared_ptr<boost::asio::ip::tcp::socket>> stream_clients;
    std::queue<std::vector<uchar>> frame_queue;
    std::mutex queue_mutex;
    std::mutex client_mutex;
    std::condition_variable queue_cv;
    bool running;
    std::thread stream_thread;
    
    // 缓冲区管理
    static const size_t MAX_QUEUE_SIZE = 50; // 最大50帧缓冲
    
    // 帧率统计
    std::atomic<int> frame_count{0};
    std::chrono::steady_clock::time_point start_time;
    
public:
    UnifiedMJPEGStreamer(int port = 8080)
        : acceptor(io_service, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)),
          running(false) {}
    
    // 支持外部访问的构造函数
    UnifiedMJPEGStreamer(const std::string& host, int port = 8080)
        : acceptor(io_service, boost::asio::ip::tcp::endpoint(
            boost::asio::ip::address::from_string(host), port)),
          running(false) {}
    
    ~UnifiedMJPEGStreamer() {
        stop();
    }
    
    void start() {
        running = true;
        frame_count = 0;
        start_time = std::chrono::steady_clock::now();
        startAccept();
        stream_thread = std::thread([this]() {
            io_service.run();
        });
        
        std::cout << "Unified MJPEG streamer started on port 8080" << std::endl;
    }
    
    void stop() {
        running = false;
        io_service.stop();
        if (stream_thread.joinable()) {
            stream_thread.join();
        }
    }
    
    void pushFrame(const cv::Mat& frame) {
        std::vector<uchar> buffer;
        cv::imencode(".jpg", frame, buffer, {cv::IMWRITE_JPEG_QUALITY, 85});
        
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            // 限制缓冲区大小,超过50帧时删除旧帧
            while (frame_queue.size() >= MAX_QUEUE_SIZE) {
                frame_queue.pop();
            }
            frame_queue.push(buffer);
        }
        queue_cv.notify_all();
        
        // 更新帧率统计
        frame_count++;
    }
    
private:
    // URL解码函数
    std::string urlDecode(const std::string& str) {
        std::string result;
        for (size_t i = 0; i < str.length(); ++i) {
            if (str[i] == '%' && i + 2 < str.length()) {
                // 解码%xx格式的URL编码
                std::string hexStr = str.substr(i + 1, 2);
                char c = static_cast<char>(std::strtol(hexStr.c_str(), nullptr, 16));
                result += c;
                i += 2;
            } else if (str[i] == '+') {
                // +号解码为空格
                result += ' ';
            } else {
                result += str[i];
            }
        }
        return result;
    }
    void startAccept() {
        auto socket = std::make_shared<boost::asio::ip::tcp::socket>(io_service);
        acceptor.async_accept(*socket,
            [this, socket](const boost::system::error_code& error) {
                if (!error) {
                    handleRequest(socket);
                }
                startAccept();
            });
    }
    
    void handleRequest(std::shared_ptr<boost::asio::ip::tcp::socket> socket) {
        std::thread([this, socket]() {
            try {
                boost::asio::streambuf buffer;
                boost::asio::read_until(*socket, buffer, "\r\n\r\n");
                
                std::istream request(&buffer);
                std::string request_line;
                std::getline(request, request_line);
                
                // std::cout << "[DEBUG] Request: " << request_line << std::endl;
                
                if (request_line.find("GET /stream") != std::string::npos) {
                    // 处理MJPEG流请求
                    {
                        std::lock_guard<std::mutex> lock(client_mutex);
                        stream_clients.push_back(socket);
                    }
                    sendStreamHeader(socket);
                    startStreaming(socket);
                } else if (request_line.find("POST /setparams") != std::string::npos) {
                    // 处理参数设置请求
                    handleParamSet(socket, request);
                } else if (request_line.find("GET /fps") != std::string::npos) {
                    // 处理FPS查询请求
                    handleFPSRequest(socket);
                } else {
                    // 默认返回主页面
                    sendMainPage(socket);
                }
            } catch (...) {
                std::cout << "[DEBUG] Request handling error" << std::endl;
            }
        }).detach();
    }
    
    void sendMainPage(std::shared_ptr<boost::asio::ip::tcp::socket> socket) {
        std::string html = generateMainPage();
        
        std::string response = "HTTP/1.1 200 OK\r\n"
                             "Content-Type: text/html; charset=utf-8\r\n"
                             "Connection: close\r\n"
                             "Content-Length: " + std::to_string(html.length()) + "\r\n\r\n" + html;
        
        boost::asio::write(*socket, boost::asio::buffer(response));
    }
    
    void handleParamSet(std::shared_ptr<boost::asio::ip::tcp::socket> socket, std::istream& request) {
        std::string line;
        while (std::getline(request, line)) {
            if (line.find("params=") != std::string::npos) {
                size_t pos = line.find("params=") + 7;
                std::string encoded_params = line.substr(pos);
                global_params = urlDecode(encoded_params);
                std::cout << "[DEBUG] 收到参数: " << global_params << std::endl;
                break;
            }
        }
        
        // 返回主页面
        sendMainPage(socket);
    }
    
    double getFPS() {
        auto now = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time);
        double fps = 0.0;
        if (duration.count() > 0 && frame_count > 0) {
            fps = (frame_count * 1000.0) / duration.count();
        }
        return fps;
    }
    
    void handleFPSRequest(std::shared_ptr<boost::asio::ip::tcp::socket> socket) {
        double fps = getFPS();
        // std::cout << "[DEBUG] FPS请求 - frame_count: " << frame_count << ", duration: "
        //           << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start_time).count()
        //           << "ms, calculated_fps: " << fps << std::endl;
        
        // 先生成内容体
        std::ostringstream body_stream;
        body_stream << std::fixed << std::setprecision(1) << fps;
        std::string body = body_stream.str();
        
        // 构建完整响应
        std::ostringstream response;
        response << "HTTP/1.1 200 OK\r\n"
                << "Content-Type: text/plain; charset=utf-8\r\n"
                << "Connection: close\r\n"
                << "Content-Length: " << body.length() << "\r\n\r\n"
                << body;
        
        std::string response_str = response.str();
        boost::asio::write(*socket, boost::asio::buffer(response_str));
        
        // std::cout << "[DEBUG] FPS响应 - Content-Length: " << body.length()
        //           << ", Body: '" << body << "', Total response length: " << response_str.length() << std::endl;
    }
    
    std::string generateMainPage() {
        std::string html = "<!DOCTYPE html>"
"<html lang=\"zh-CN\">"
"<head>"
"    <meta charset=\"UTF-8\">"
"    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"    <title>MJPEG摄像头推流</title>"
"    <style>"
"        body {"
"            font-family: Arial, sans-serif;"
"            margin: 0;"
"            padding: 20px;"
"            background-color: #f0f0f0;"
"        }"
"        .container {"
"            max-width: 1000px;"
"            margin: 0 auto;"
"            background-color: white;"
"            padding: 20px;"
"            border-radius: 10px;"
"            box-shadow: 0 2px 10px rgba(0,0,0,0.1);"
"        }"
"        .input-row {"
"            display: flex;"
"            align-items: center;"
"            gap: 10px;"
"            margin-bottom: 20px;"
"        }"
"        .input-row input[type=\"text\"] {"
"            flex: 1;"
"            padding: 8px;"
"            border: 1px solid #ddd;"
"            border-radius: 4px;"
"            font-size: 14px;"
"        }"
"        .input-row button {"
"            padding: 8px 20px;"
"            background-color: #007bff;"
"            color: white;"
"            border: none;"
"            border-radius: 4px;"
"            cursor: pointer;"
"            font-size: 14px;"
"        }"
"        .input-row button:hover {"
"            background-color: #0056b3;"
"        }"
"        .params-display {"
"            background-color: #f8f9fa;"
"            padding: 15px;"
"            border-radius: 4px;"
"            margin-bottom: 20px;"
"            font-family: monospace;"
"            white-space: pre-wrap;"
"            min-height: 50px;"
"            border: 1px solid #ddd;"
"        }"
"        .video-container {"
"            text-align: center;"
"        }"
"        .stream-img {"
"            max-width: 100%;"
"            height: auto;"
"            border: 2px solid #ddd;"
"            border-radius: 8px;"
"        }"
"        .status {"
"            margin-top: 10px;"
"            padding: 10px;"
"            border-radius: 4px;"
"            font-size: 12px;"
"        }"
"        .success {"
"            background-color: #d4edda;"
"            color: #155724;"
"            border: 1px solid #c3e6cb;"
"        }"
"        .error {"
"            background-color: #f8d7da;"
"            color: #721c24;"
"            border: 1px solid #f5c6cb;"
"        }"
"        .fps-display {"
"            background-color: #e9ecef;"
"            padding: 10px;"
"            border-radius: 4px;"
"            margin-top: 20px;"
"            font-family: monospace;"
"            font-size: 14px;"
"            border: 1px solid #ddd;"
"        }"
"    </style>"
"</head>"
"<body>"
"    <div class=\"container\">"
"        <h1>MJPEG摄像头推流服务</h1>"
"        "
"        <div class=\"input-row\">"
"            <input type=\"text\" id=\"paramInput\" placeholder=\"请输入参数...\" value=\"" + global_params + "\">"
"            <button onclick=\"setParams()\">设置参数</button>"
"        </div>"
"        "
"        <div class=\"params-display\" id=\"paramsDisplay\">" + global_params + "</div>"
"        "
"        <div class=\"video-container\">"
"            <img id=\"stream\" class=\"stream-img\" src=\"/stream\" alt=\"MJPEG Stream\">"
"        </div>"
"        "
"        <div class=\"fps-display\" id=\"fpsDisplay\">帧率: 0.0 fps</div>"
"    </div>"
"    "
"    <script>"
"        function setParams() {"
"            const input = document.getElementById('paramInput');"
"            const params = input.value;"
"            "
"            fetch('/setparams', {"
"                method: 'POST',"
"                headers: {"
"                    'Content-Type': 'application/x-www-form-urlencoded',"
"                },"
"                body: 'params=' + encodeURIComponent(params)"
"            })"
"            .then(response => response.text())"
"            .then(data => {"
"                document.getElementById('paramsDisplay').textContent = params;"
"                showStatus('参数设置成功: ' + params, 'success');"
"            })"
"            .catch(error => {"
"                showStatus('参数设置失败: ' + error.message, 'error');"
"            });"
"        }"
"        "
"        function updateFPS() {"
"            fetch('/fps')"
"            .then(response => response.text())"
"            .then(fps => {"
"                document.getElementById('fpsDisplay').textContent = '帧率: ' + fps + ' fps';"
"            })"
"            .catch(error => {"
"                console.log('[DEBUG] 获取帧率失败:', error);"
"            });"
"        }"
"        "
"        function showStatus(message, type) {"
"            const status = document.getElementById('streamStatus');"
"            status.textContent = message;"
"            status.className = 'status ' + type;"
"        }"
"        "
"        const streamImg = document.getElementById('stream');"
"        streamImg.onload = function() {"
"            showStatus('视频流连接正常', 'success');"
"        };"
"        streamImg.onerror = function() {"
"            showStatus('视频流连接失败,请检查服务器', 'error');"
"        };"
"        "

"        console.log('[DEBUG] 设置定时器,每1秒执行一次');"
"        "
"        setInterval(function() {updateFPS();}, 1000);"
"        "
"        document.getElementById('paramInput').addEventListener('keypress', function(e) {"
"            if (e.key === 'Enter') {"
"                setParams();"
"            }"
"        });"
"    </script>"
"</body>"
"</html>";
        return html;
    }
    
    void sendStreamHeader(std::shared_ptr<boost::asio::ip::tcp::socket> socket) {
        std::string header =
            "HTTP/1.0 200 OK\r\n"
            "Cache-Control: no-cache\r\n"
            "Pragma: no-cache\r\n"
            "Connection: close\r\n"
            "Content-Type: multipart/x-mixed-replace;boundary=frame\r\n\r\n";
        
        boost::asio::async_write(*socket, boost::asio::buffer(header),
            [](const boost::system::error_code&, size_t) {});
    }
    
    void startStreaming(std::shared_ptr<boost::asio::ip::tcp::socket> socket) {
        std::thread([this, socket]() {
            while (running) {
                std::vector<uchar> frame;
                {
                    std::unique_lock<std::mutex> lock(queue_mutex);
                    queue_cv.wait(lock, [this]() {
                        return !frame_queue.empty() || !running;
                    });
                    
                    if (!running) break;
                    if (frame_queue.empty()) continue;
                    
                    frame = frame_queue.front();
                    frame_queue.pop();
                }
                
                std::ostringstream header;
                header << "--frame\r\n"
                       << "Content-Type: image/jpeg\r\n"
                       << "Content-Length: " << frame.size() << "\r\n\r\n";
                
                std::string header_str = header.str();
                std::vector<boost::asio::const_buffer> buffers = {
                    boost::asio::buffer(header_str),
                    boost::asio::buffer(frame)
                };
                
                try {
                    boost::asio::write(*socket, buffers);
                } catch (...) {
                    std::lock_guard<std::mutex> lock(client_mutex);
                    auto it = std::find(stream_clients.begin(), stream_clients.end(), socket);
                    if (it != stream_clients.end()) {
                        stream_clients.erase(it);
                    }
                    break;
                }
                
                // 控制帧率
                std::this_thread::sleep_for(std::chrono::milliseconds(33)); // ~30fps
            }
        }).detach();
    }
};

int main() {
    std::cout << "[DEBUG] 启动MJPEG推流测试程序..." << std::endl;
    
    // 初始化摄像头 - 尝试不同的后端
    cv::VideoCapture cap;
    
    // 首先尝试V4L2后端
    std::cout << "[DEBUG] 尝试使用V4L2后端打开摄像头..." << std::endl;
    cap.open(1, cv::CAP_V4L2);
    
    if (!cap.isOpened()) {
        std::cout << "[DEBUG] V4L2失败,尝试默认后端..." << std::endl;
        cap.open(1);
    }
    
    if (!cap.isOpened()) {
        std::cout << "[DEBUG] 默认后端失败,尝试GStreamer后端..." << std::endl;
        std::string pipeline = "v4l2src device=/dev/video1 ! videoconvert ! appsink";
        cap.open(pipeline, cv::CAP_GSTREAMER);
    }
    
    if (!cap.isOpened()) {
        std::cerr << "[ERROR] 所有后端都无法打开摄像头索引1" << std::endl;
        return -1;
    }
    
    std::cout << "[DEBUG] 摄像头初始化成功" << std::endl;
    
    // 设置摄像头参数
    cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
    cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
    cap.set(cv::CAP_PROP_FPS, 30);
    cap.set(cv::CAP_PROP_BUFFERSIZE, 1); // 减少延迟
    
    // 验证设置
    double width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
    double height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
    double fps = cap.get(cv::CAP_PROP_FPS);
    
    std::cout << "[DEBUG] 摄像头实际参数: " << width << "x" << height << "@" << fps << "fps" << std::endl;
    
    // 创建推流器 - 绑定到0.0.0.0支持外部访问
    try {
        UnifiedMJPEGStreamer streamer("0.0.0.0", 8080);
        std::cout << "[DEBUG] 正在启动统一推流服务器..." << std::endl;
        streamer.start();
        
        std::cout << "[DEBUG] 统一推流服务器启动在端口8080 (支持外部访问)" << std::endl;
        std::cout << "[INFO] 访问 http://服务器IP:8080/ 查看主页面" << std::endl;
        std::cout << "[INFO] 访问 http://服务器IP:8080/stream 查看视频流" << std::endl;
        std::cout << "[INFO] 访问 http://服务器IP:8080/setparams 设置参数" << std::endl;
        
        // 等待服务器完全启动
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        
        cv::Mat frame;
        int frame_count = 0;
        
        while (true) {
            cap >> frame;
            if (frame.empty()) {
                std::cerr << "[ERROR] 无法读取摄像头帧" << std::endl;
                break;
            }
            
            // 每30帧打印一次状态
            if (frame_count % 30 == 0) {
                std::cout << "[DEBUG] 推流帧数: " << frame_count << ", 当前参数: '" << streamer.global_params << "'" << std::endl;
            }
            
            streamer.pushFrame(frame);
            frame_count++;
            
            // 检查键盘输入
            char key = cv::waitKey(1) & 0xFF;
            if (key == 'q' || key == 27) { // 'q'或ESC退出
                std::cout << "[DEBUG] 用户请求退出" << std::endl;
                break;
            }
        }
        
        std::cout << "[DEBUG] 正在关闭推流器..." << std::endl;
        streamer.stop();
    } catch (const std::exception& e) {
        std::cerr << "[ERROR] 异常: " << e.what() << std::endl;
        cap.release();
        return -1;
    } catch (...) {
        std::cerr << "[ERROR] 未知异常" << std::endl;
        cap.release();
        return -1;
    }
    
    cap.release();
    cv::destroyAllWindows();
    
    std::cout << "[DEBUG] 程序退出" << std::endl;
    return 0;
}


/*  build.sh

#!/bin/bash

echo "编译MJPEG推流测试程序..."

# 检查依赖
echo "检查OpenCV依赖..."
if [ ! -d "/usr/include/opencv4" ]; then
    echo "错误: 未找到OpenCV4开发包"
    echo "请安装: sudo apt-get install libopencv-dev"
    exit 1
fi

if [ ! -d "/usr/include/boost" ]; then
    echo "错误: 未找到Boost开发包"
    echo "请安装: sudo apt-get install libboost-all-dev"
    exit 1
fi

echo "依赖检查通过!"

# 设置编译标志
OPENCV_FLAGS="-I/usr/include/opencv4 -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_imgcodecs -lopencv_videoio"
BOOST_FLAGS="-I/usr/include/boost -lboost_system -lboost_thread"

echo "OpenCV标志: $OPENCV_FLAGS"
echo "Boost标志: $BOOST_FLAGS"

# 编译
g++ -std=c++11 \
    00_test1.cpp \
    $OPENCV_FLAGS \
    $BOOST_FLAGS \
    -pthread \
    -o mjpeg_streamer

if [ $? -eq 0 ]; then
    echo "编译成功!"
    echo "运行程序: ./mjpeg_streamer"
else
    echo "编译失败!"
    exit 1
fi

echo "运行 pkill -f mjpeg_streamer || ./mjpeg_streamer "

*/
posted @ 2025-12-08 16:02  小城熊儿  阅读(0)  评论(0)    收藏  举报