将Python视频流传输到C++服务器

3

我正在通过一个项目学习C++,该项目涉及将视频从一个设备流式传输到另一个设备以进行显示。我有一个PiCamera和以下客户端代码,用于捕获帧并通过TCP流发送它们。

import io
import socket
import struct
import time
import picamera


def stream():
    servver_ip = "0.0.0.0" #intentionally left out
    client_socket = socket.socket()
    client_socket.connect((server_ip, 8123))
    connection = client_socket.makefile("wb") 

    try:
        camera = picamera.PiCamera()
        camera.resolution = (640, 480)
        camera.start_preview()
        time.sleep(2)
        start = time.time()
        stream = io.BytesIO()
        for image in camera.capture_continuous(stream, 'jpeg'):
            connection.write(struct.pack("<L", stream.tell()))
            connection.flush()
            stream.seek(0)
            connection.write(stream.read()) 
            stream.seek(0)
            stream.truncate()
        connection.write(struct.pack("<L", 0)) 
    except Exception as e:
        raise e
    finally:
        connection.close()
        client_socket.close()

def main():
    while True:
        try:
            stream()
        except ConnectionRefusedError as e:
            print("Connection refused. It the server on?")
            time.sleep(2)
        except Exception as e:
            print(e)
            break 

if __name__ == "__main__":
    main()

上面的代码直接来自于一个PiCamera的示例,如果我在另一端有一个Python脚本,则它可以很好地工作。然而,当我尝试使用以下C ++代码接收和显示流时,我只能得到部分帧,且流程不流畅。有时我会得到0数据、1帧或慢慢杂乱无章的消息。改变usleep增量似乎可以得到一些改善,但我担心这不是正确的答案。我还尝试在recv中使用MSG_WAITALL标志,但这似乎阻止了任何数据的传输,这可能表明我的缓冲区大小的值不正确。
// the standard stuff
#include <iostream>
#include <string>
#include <unistd.h>

// opencv
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/video/video.hpp>

//socket stuffs
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <netinet/in.h>


int main(int argc, const char * argv[]) {

    int key;

    int yes = 1;

    int height = 480;
    int width = 640;

    char* listenPort = "8123";

    int bytes = 0;

    int status;
    struct addrinfo hints; // define our interface
    struct addrinfo *serviceinfo; // will point to results
    struct sockaddr_storage their_addr;
    memset(&hints, 0, sizeof(hints)); // make sure the struct is empy
    hints.ai_family = AF_UNSPEC; // don't care if IP4 or IP6
    hints.ai_socktype = SOCK_STREAM; // TCP
    hints.ai_flags = AI_PASSIVE; // fill in my IP for me

    // look up address info for this machine. Will populate service info
    if ((getaddrinfo(NULL, listenPort, &hints, &serviceinfo)) == -1){
        fprintf(stderr, "getaddinfo error: %s\n", gai_strerror(status));
        exit(1);
    }

    // make the socket
    int sockfd = socket(serviceinfo->ai_family, serviceinfo->ai_socktype, serviceinfo->ai_protocol);

    //bind to the correct port
    if ((bind(sockfd, serviceinfo->ai_addr, serviceinfo->ai_addrlen)) == -1){
        fprintf(stderr, "failed to bind: %s\n", gai_strerror(status));
        exit(1);
    }

    // allow us to reuse the port
    if ((setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))) == -1){
        perror("setsockopt");
        exit(1);
    }

    // number of connections to let in the queue
    int backlog = 1;

    // start listening for incoming connections
    if ((listen(sockfd, backlog)) == -1){
        perror("listen");
        exit(1);
    }

    // start accepting incoming connections
    socklen_t addr_size = sizeof(their_addr);
    int new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size); // use this for all send and receive calls
    if (new_fd == -1){
        perror("accept");
        exit(1);
    }

    // empty image object
    cv::Mat img = cv::Mat::zeros(height, width, CV_8UC1);

    if (!img.isContinuous()){
        img = img.clone();
    }

    int image_size = img.total() * img.elemSize();

    cv::Mat rawImage = cv::Mat::zeros(1, image_size, CV_8UC1);

    if (!rawImage.isContinuous()){
        rawImage = rawImage.clone();
    }
    cv::Mat flipped = cv::Mat::zeros(height, width, CV_8UC1);

    std::cout << "Press q to quit" << std::endl;
    cv::namedWindow("Sentry", cv::WINDOW_AUTOSIZE);

    while (key != 'q'){
        std::cout << "Capturing" << std::endl;

        if ((bytes = recv(new_fd, rawImage.data, image_size, 0)) == -1){
            perror("Failed to receive bytes!");
            exit(1);
        }

        img = cv::imdecode(rawImage, CV_LOAD_IMAGE_COLOR);
        cv::flip(img, flipped, 1);


        if (flipped.data != NULL) {
            // show image. using a global here
            cv::imshow("Sentry", flipped);
        }

        memset(rawImage.data, 0x0, sizeof(rawImage));

        // look for quit key
        key = cv::waitKey(10);

        // pause for half a second
        usleep(500000);
    };

    cv::destroyAllWindows();

    freeaddrinfo(serviceinfo); // clear the linked list

    close(sockfd);

    return 0;

}

我正在寻找任何提示、答案或只是指向正确方向的建议。谢谢提前。

编辑:可行的解决方案
首先,感谢Kevin帮助我。我的问题在于我没有意识到我的初始Python客户端正在发送图像大小。通过搜索和使用下面的答案,我开始抓取那前4个字节,并调整我cv :: Mat rawImage的大小。我不再编写检查recv以确保我得到所有数据的逻辑,而是使用MSG_WAITALL。这个标志与获取正确的有效载荷大小结合起来,使一切都可以顺利地工作。这是一个非常令人惊讶的经历。

#define CHUNKSIZE 500

int main(int argc, const char * argv[]) {

    // skipping code from before

    int32_t image_size = 0;
    cv::Mat rawImage;
    long data_received = 0;
    long success;

    cv::namedWindow("Sentry", cv::WINDOW_AUTOSIZE);

    while (key != 'q'){
        std::cout << "Capturing" << std::endl;

        // Very important!! Grab the image_size here
        success = recv(new_fd, &image_size, 4 , NULL);
        if (success == -1){
            perror("Failed to receive file size!");
            exit(1);
        }
        // if your image size is extremely large, it's probably 
        // because you're grabing the wrong number of bytes above
        if (image_size > 300000){
            std::cout << "Image size is too large " << image_size << std::endl;
            exit(1);
        }

        if (image_size > 0) {

            //
            rawImage = cv::Mat::zeros(1, image_size, CV_8UC1);
            success = recv(new_fd, rawImage.data, image_size, MSG_WAITALL);
            if (success == -1){
                perror("Failed to receive");
                exit(1);
            }

            img = cv::imdecode(rawImage, CV_LOAD_IMAGE_COLOR);

            if (img.data != NULL) {
                // show image. using a global here
                cv::imshow("Sentry", img);
            } else {
                std::cout << "Image is null!" << std::endl;
            }

            memset(&rawImage, 0x0, sizeof(rawImage));

            // look for quit key
            key = cv::waitKey(10);
        } else {
            std::cout << "No message yet" << std::endl;

        }
        image_size = 0;
        // pause for half a second
        usleep(500000);
    };

    cv::destroyAllWindows();

    freeaddrinfo(serviceinfo); // clear the linked list

    close(sockfd);

    return 0;

}
2个回答

2

当我用C ++通过wifi将Raspberry Pi实时流传输到我的PC时,也遇到了同样的问题。

我解决它的方法是仅发送图像块。在大约500字节左右测试块,然后一旦看到它可以工作,就增加它。不要发送整个文件,否则程序将只获取帧的一部分。

此外,不要读取send()或recv()函数的返回代码,如@Sam所述,我发现在创建实时流时其不可靠。只需按块发送即可。

如果您想查看源代码,我有源代码。我知道使用C ++创建实时流服务器非常困难,因为几乎没有文档,所以我想帮助您,因为这给我带来了很多头痛!如果您需要源代码,请告诉我,祝您好运!

客户端应该类似于这样,这无法编译! 我试图使其易于阅读:

//Client    
#define CHUNK_SIZE 500

int main()
{
    //do all the connection stuff here and connect to it
    int filesize;
    int dataReceived = 0;
    char received_message[10];
    std::vector<char> imageData;
    while (1)
    {
        recv(Connection, received_message, 11, NULL); //recieved File size of image
        filesize = atoi(received_message); //Turning File size char into an integer 

        imageData.resize(filesize); //Setting up vector to recieve image data

        while (dataReceived < filesize) //keep running until we get enough bytes
        {
            if (filesize - dataReceived >= CHUNK_SIZE) //if the amount of bytes left to recieve is greater than or equal to chunk size, then lets receive a CHUNK_SIZE of the image
            {
                recv(Connection, &imageData[dataReceived], CHUNK_SIZE, 0);
                dataReceived += CHUNK_SIZE;
            }
            else //Else, if the amount of bytes left to recieve is smaller than the CHUNK_SIZE then lets receive a specific amount of bytes
            {
                recv(Connection, &imageData[dataReceived], fileSize - dataReceived, 0);
                dataReceived += fileSize - dataReceived;
            }
         Sleep(Amount of sleep); //Note, python code will always be slower than C++ so you must set a Sleep() function.
        }
        std::cout << "Got the image, it is stored in the imageData vector!" << std::endl;
        std::cout << "Press enter to get another image!" << std::endl;
        std::cin.get(); 

    }
} 

@Andrew 不,我忘了提到你需要先发送文件大小!先将文件大小发送到 C++ 然后将其分成块。我将编辑我的答案,这样你就可以看到我在说什么了,给我一秒钟。 - Kevin Duarte
@Andrew 在代码中更新了一些关键内容,我忘记添加了。 - Kevin Duarte
感谢提供代码示例。我正在努力理解你在这里的内容。&fileData[dataReceived]应该是什么?那应该是imageData还是另一个缓冲区? - Andrew
@Andrew 是的,那是个打字错误,非常抱歉!fileData 应该是 imageData。我已经修复了。 - Kevin Duarte
@Keving_Duarte 谢谢,这帮了我很多!不幸的是,我现在遇到了下一个错误:“损坏的JPEG”。 - Andrew
显示剩余3条评论

1
所示代码大多忽略了从recv()返回的值。
if ((bytes = recv(new_fd, rawImage.data, image_size, 0)) == -1){

确实,错误会导致recv()返回-1。

但是,如果recv()没有返回-1,并不意味着recv()已经读取了image_size字节。

但是,现在显示的代码似乎假设整个图像已被读取,即image_size字节。这是一个错误的假设。当涉及到网络套接字时,如果接收方迄今为止接收到的字节数少于您期望的字节数,则recv()将立即返回。

recv()的正返回值表示实际接收并放入缓冲区的字节数。如果这比您预期的字节数少,您需要负责再次调用recv()。请注意,您不能只是

recv(new_fd, rawImage.data, image_size, 0)

再次提醒,因为rawImage.data的初始部分已经包含了第一次调用recv()接收到的所有字节。你需要在这里设计一些简单的逻辑来读取其余部分(要记住第二次调用recv()也不保证能读取剩余部分)。


你好,能否重新打开我这个编辑过的问题。我完全改变了我一个月前发布的问题。我不能提出新问题,只能改进现有的问题。请帮忙。http://stackoverflow.com/q/41820728/6789999 - minigeek
如果我只是将recv放在while循环中,并在字节=0时中断,那么这会按顺序将数据分配给缓冲区吗? - Andrew
不。请重新阅读我写的内容。无论您传递给recv()的缓冲指针是什么,recv()都会将任何接收到的数据放置在那里。recv()没有魔法知识,即您之前已经使用指向相同缓冲区的指针调用了recv(),因此它应该将任何附加接收到的数据放置在先前接收的数据之后的缓冲区中。如果在调用recv()时,您收到的字节数少于预期,则您有责任使用重置为指向现在已接收的数据之后的指针和要接收的剩余字节数的计数来重新调用recv() - Sam Varshavchik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接