scrcpy源码阅读及在Ubuntu上的实现(三)——使用ZeroMQ传输yuv数据并使用Python订阅

目录

0x01 什么是ZeroMQ?

0x02 ZeroMQ的消息模型

0x03 回到任务

0x04 封装我们的yuv图像以及发布者

0x05 使用Python订阅ZeroMQ的发布

0x06 需要注意的


在上一篇博客已经实现了对于yuv数据的输出实现。那么现在就使用ZeroMQ发布到本机网络端口,并且使用Python+OpenCV订阅出来吧。

0x01 什么是ZeroMQ?

ZeroMQ(简称ZMQ)是一个基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。

ZMQ以嵌入式网络编程库的形式实现了一个并行开发框架,对于普通的socket是端到端,一对一的模式,但是对于ZMQ,他可以实现N:M的关系。普通的BSD套接字的了解较多是点对点的连接,对于点对点的连接需要显示地建立连接、销毁连接、选择协议(TCP/UDP)和处理错误等,而ZMQ屏蔽了这些细节。

ZeroMQ可以提供什么?

  • 能够提供进程内(inproc)、进程间(IPC)、网络(TCP)和广播方式的消息信道。

  • 支持扇出(fan-out)、发布-订阅(pub-sub)、任务分发(task distribution)、请求/响应(request-reply)等通信模式。

使用这种ZeroMQ的中间件进行传输,大大简化了消息传输的中间过程,简化了很多链接的操作。

ZeroMQ是网络通信中新的一层,介于应用层和传输层之间(按照TCP/IP划分),其是一个可伸缩层,可并行运行,分三在分布式系统间。

ZMQ不是单独的服务,而是一个嵌入式库,它封装了网络通信、消息队列、线程调度等功能,向上层提供简洁的API,应用程序通过加载库文件,调用API函数来实现高性能网络通信。

0x02 ZeroMQ的消息模型

  • 一对一模型(Exclusive-Pair)

    最简单的1:1消息通信模型,可以认为是一个TCP Connection,但是TCP Server只能接受一个连接。数据可以双向流动,这点不同于后面的请求回应模型。

  • 请求回应模型

    由请求端发起请求,然后等待回应端应答。一个请求必须对应一个回应,从请求端的角度来看是发-收配对,从回应端的角度是收-发配对。跟一对一结对模型的区别在于请求端可以是1~N个。该模型主要用于远程调用及任务分配等。Echo服务就是这种经典模型的应用。

  • 发布订阅模型

    发布端单向分发数据,且不关心是否把全部信息发送给订阅端。如果发布端开始发布信息时,订阅端尚未连接上来,则这些信息会被直接丢弃。订阅端未连接导致信息丢失的问题,可以通过与请求回应模型组合来解决,订阅端只负责接受,而不能反馈,且订阅端消费速度慢于发布端的情况下,会在订阅端堆积数据。该模型主要用于数据分发。

  • 推拉模型

    Server端作为push端,而client端作为pull端,如果有多个client端同时连接到Server端,则server端会在内部做一个负载均衡,采用平均分配的算法,将所有消息均衡发布到client端上。与发布订阅模型相比,推拉模型在没有消费者的情况下,发布的消息不会被消耗掉。在消费者能力不够的情况下,能够提供多消费者并行消费解决方案。该模型主要用于多任务并行。

0x03 回到任务

那么对于ZeroMQ的了解就到这一步了,回到使用ZeroMQ传输yuv数据的时候了。作为一个强大的传输中间件,我们需要确定其传输模式,由于我们的scrcpy是源源不断的发送一帧一帧的图片,而接收端是否接收到数据,对于发送端来说都不会停止,那么这里选择发布订阅模型是比较合适的。对于发送者(PUB)我们可以确定在scrcpy的这一端,对于接收者(SUB),可以确定在Python+OpenCV订阅的这一端。

0x04 封装我们的yuv图像以及发布者

上次提取yuv图像时,我们需要的变量有如下:

  • 手机的分辨率

  • 图像宽度

  • 图像高度

  • 图像的Y/U/V三个分量

那么我们对应一个结构体,对他进行封装:

#define yuv_size	1080*2400
typedef struct{
    int width;
    int height;
    uint8_t data_Y[yuv_size];
    uint8_t data_U[yuv_size/4];
    uint8_t data_V[yuv_size/4];
}myframe;

以上的结构体存储在decoder.h中,接下来回到decoder.c中进行对结构体的初始化:

//声明结构体
myframe yuv_frame;
//初始化长宽
yuv_frame.width= decoder->codec_ctx->width;
yuv_frame.height=decoder->codec_ctx->height;
//读取数据
int offest=0;
for(int i=0;i<yuv_frame.height;i++)
{
    memcpy(yuv_frame.data_Y+offest,frame->data[0]+frame->linesize[0]*i,yuv_frame.width);
    //指向下一行
    offest += yuv_frame.width;
}

offest=0;
for(int i=0;i<yuv_frame.height/2;i++)
{
    memcpy(yuv_frame.data_U+offest,frame->data[1]+frame->linesize[1]*i,yuv_frame.width/2);
    offest += yuv_frame.width/2;
}

offset=0;
for(int i=0;i<yuv_frame.height/2;i++)
{
    memcpy(yuv_frame.data_V+offset,frame->data[2]+frame->linesize[2]*i,yuv_frame.width/2);
    offest += yuv_frame.width/2;
}

在Scrcpy中创建的ZeroMQ的发布者:

对于一个多线程的项目,对于一个创建发布者对象,我们需要找到一个函数初始化的位置进行实现,而不是随随便便的写在decoder.c中,这样会使其重复调用多次,导致出现bug。那么我们就得去查找关于ZeroMQ初始化所对应的文件,之后在他要执行下一步的时候创建好发布者的对象。在此之前,我们研究一下scrcpy的函数吧:

下面是scrcpy客户端初始化的函数:

bool
scrcpy(struct scrcpy_options *options) {
    static struct scrcpy scrcpy;
    struct scrcpy *s = &scrcpy;

    // Minimal SDL initialization
    if (SDL_Init(SDL_INIT_EVENTS)) {
        LOGE("Could not initialize SDL: %s", SDL_GetError());
        return false;
    }

    atexit(SDL_Quit);

    bool ret = false;

    bool server_started = false;
    bool file_pusher_initialized = false;
    bool recorder_initialized = false;
#ifdef HAVE_V4L2
    bool v4l2_sink_initialized = false;
#endif
    bool demuxer_started = false;
#ifdef HAVE_USB
    bool aoa_hid_initialized = false;
    bool hid_keyboard_initialized = false;
    bool hid_mouse_initialized = false;
#endif
    bool controller_initialized = false;
    bool controller_started = false;
    bool screen_initialized = false;

    struct sc_acksync *acksync = NULL;

    //1.server_init()
    struct sc_server_params params = {
        .req_serial = options->serial,
        .select_usb = options->select_usb,
        .select_tcpip = options->select_tcpip,
        .log_level = options->log_level,
        .crop = options->crop,
        .port_range = options->port_range,
        .tunnel_host = options->tunnel_host,
        .tunnel_port = options->tunnel_port,
        .max_size = options->max_size,
        .bit_rate = options->bit_rate,
        .max_fps = options->max_fps,
        .lock_video_orientation = options->lock_video_orientation,
        .control = options->control,
        .display_id = options->display_id,
        .show_touches = options->show_touches,
        .stay_awake = options->stay_awake,
        .codec_options = options->codec_options,
        .encoder_name = options->encoder_name,
        .force_adb_forward = options->force_adb_forward,
        .power_off_on_close = options->power_off_on_close,
        .clipboard_autosync = options->clipboard_autosync,
        .downsize_on_error = options->downsize_on_error,
        .tcpip = options->tcpip,
        .tcpip_dst = options->tcpip_dst,
        .cleanup = options->cleanup,
    };

    static const struct sc_server_callbacks cbs = {
        .on_connection_failed = sc_server_on_connection_failed,
        .on_connected = sc_server_on_connected,
        .on_disconnected = sc_server_on_disconnected,
    };
    if (!sc_server_init(&s->server, &params, &cbs, NULL)) {
        return false;
    }

    if (!sc_server_start(&s->server)) {
        goto end;
    }
	
    //2.server_start()
    server_started = true;

    if (options->display) {
        sdl_set_hints(options->render_driver);
    }

    // Initialize SDL video in addition if display is enabled
    if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
        LOGE("Could not initialize SDL: %s", SDL_GetError());
        goto end;
    }

    sdl_configure(options->display, options->disable_screensaver);

    // Await for server without blocking Ctrl+C handling
    if (!await_for_server()) {
        goto end;
    }

    //3.server_connect_to
    // It is necessarily initialized here, since the device is connected
    struct sc_server_info *info = &s->server.info;

    const char *serial = s->server.serial;
    assert(serial);

    //4.file_handler_init
    struct sc_file_pusher *fp = NULL;

    if (options->display && options->control) {
        if (!sc_file_pusher_init(&s->file_pusher, serial,
                                 options->push_target)) {
            goto end;
        }
        fp = &s->file_pusher;
        file_pusher_initialized = true;
    }
	//5.decoder_init()
    struct sc_decoder *dec = NULL;
    bool needs_decoder = options->display;
#ifdef HAVE_V4L2
    needs_decoder |= !!options->v4l2_device;
#endif
    if (needs_decoder) {
        sc_decoder_init(&s->decoder);
        dec = &s->decoder;
    }

    struct sc_recorder *rec = NULL;
    if (options->record_filename) {
        if (!sc_recorder_init(&s->recorder,
                              options->record_filename,
                              options->record_format,
                              info->frame_size)) {
            goto end;
        }
        rec = &s->recorder;
        recorder_initialized = true;
    }

    //6.av_log_set_callback()
    av_log_set_callback(av_log_callback);

    //7.sc_demuxer_init()
    static const struct sc_demuxer_callbacks demuxer_cbs = {
        .on_eos = sc_demuxer_on_eos,
    };
    sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);

    //8.sc_demuxer_add_sink(dec)
    if (dec) {
        sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
    }

    //9.sc_demuxer_add_sink(rec)
    if (rec) {
        sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
    }

    //10.sc_controller_init();control_socket
    struct sc_controller *controller = NULL;
    struct sc_key_processor *kp = NULL;
    struct sc_mouse_processor *mp = NULL;

    //11.sc_controller_start();
    if (options->control) {
#ifdef HAVE_USB
        bool use_hid_keyboard =
            options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID;
        bool use_hid_mouse =
            options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID;
        if (use_hid_keyboard || use_hid_mouse) {
            bool ok = sc_acksync_init(&s->acksync);
            if (!ok) {
                goto end;
            }

            ok = sc_usb_init(&s->usb);
            if (!ok) {
                LOGE("Failed to initialize USB");
                sc_acksync_destroy(&s->acksync);
                goto aoa_hid_end;
            }

            assert(serial);
            struct sc_usb_device usb_device;
            ok = sc_usb_select_device(&s->usb, serial, &usb_device);
            if (!ok) {
                sc_usb_destroy(&s->usb);
                goto aoa_hid_end;
            }

            LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s",
                 usb_device.serial, usb_device.vid, usb_device.pid,
                 usb_device.manufacturer, usb_device.product);

            ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL);
            sc_usb_device_destroy(&usb_device);
            if (!ok) {
                LOGE("Failed to connect to USB device %s", serial);
                sc_usb_destroy(&s->usb);
                sc_acksync_destroy(&s->acksync);
                goto aoa_hid_end;
            }

            ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync);
            if (!ok) {
                LOGE("Failed to enable HID over AOA");
                sc_usb_disconnect(&s->usb);
                sc_usb_destroy(&s->usb);
                sc_acksync_destroy(&s->acksync);
                goto aoa_hid_end;
            }

            if (use_hid_keyboard) {
                if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) {
                    hid_keyboard_initialized = true;
                    kp = &s->keyboard_hid.key_processor;
                } else {
                    LOGE("Could not initialize HID keyboard");
                }
            }

            if (use_hid_mouse) {
                if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) {
                    hid_mouse_initialized = true;
                    mp = &s->mouse_hid.mouse_processor;
                } else {
                    LOGE("Could not initialized HID mouse");
                }
            }

            bool need_aoa = hid_keyboard_initialized || hid_mouse_initialized;

            if (!need_aoa || !sc_aoa_start(&s->aoa)) {
                sc_acksync_destroy(&s->acksync);
                sc_usb_disconnect(&s->usb);
                sc_usb_destroy(&s->usb);
                sc_aoa_destroy(&s->aoa);
                goto aoa_hid_end;
            }

            acksync = &s->acksync;

            aoa_hid_initialized = true;

aoa_hid_end:
            if (!aoa_hid_initialized) {
                if (hid_keyboard_initialized) {
                    sc_hid_keyboard_destroy(&s->keyboard_hid);
                    hid_keyboard_initialized = false;
                }
                if (hid_mouse_initialized) {
                    sc_hid_mouse_destroy(&s->mouse_hid);
                    hid_mouse_initialized = false;
                }
            }

            if (use_hid_keyboard && !hid_keyboard_initialized) {
                LOGE("Fallback to default keyboard injection method "
                     "(-K/--hid-keyboard ignored)");
                options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT;
            }

            if (use_hid_mouse && !hid_mouse_initialized) {
                LOGE("Fallback to default mouse injection method "
                     "(-M/--hid-mouse ignored)");
                options->mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT;
            }
        }
#else
        assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID);
        assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_HID);
#endif

        // keyboard_input_mode may have been reset if HID mode failed
        if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) {
            sc_keyboard_inject_init(&s->keyboard_inject, &s->controller,
                                    options->key_inject_mode,
                                    options->forward_key_repeat);
            kp = &s->keyboard_inject.key_processor;
        }

        // mouse_input_mode may have been reset if HID mode failed
        if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_INJECT) {
            sc_mouse_inject_init(&s->mouse_inject, &s->controller);
            mp = &s->mouse_inject.mouse_processor;
        }

        if (!sc_controller_init(&s->controller, s->server.control_socket,
                                acksync)) {
            goto end;
        }
        controller_initialized = true;

        if (!sc_controller_start(&s->controller)) {
            goto end;
        }
        controller_started = true;
        controller = &s->controller;

        if (options->turn_screen_off) {
            struct sc_control_msg msg;
            msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
            msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;

            if (!sc_controller_push_msg(&s->controller, &msg)) {
                LOGW("Could not request 'set screen power mode'");
            }
        }

    }

    // There is a controller if and only if control is enabled
    assert(options->control == !!controller);

    if (options->display) {
        const char *window_title =
            options->window_title ? options->window_title : info->device_name;

        struct sc_screen_params screen_params = {
            .controller = controller,
            .fp = fp,
            .kp = kp,
            .mp = mp,
            .forward_all_clicks = options->forward_all_clicks,
            .legacy_paste = options->legacy_paste,
            .clipboard_autosync = options->clipboard_autosync,
            .shortcut_mods = &options->shortcut_mods,
            .window_title = window_title,
            .frame_size = info->frame_size,
            .always_on_top = options->always_on_top,
            .window_x = options->window_x,
            .window_y = options->window_y,
            .window_width = options->window_width,
            .window_height = options->window_height,
            .window_borderless = options->window_borderless,
            .rotation = options->rotation,
            .mipmaps = options->mipmaps,
            .fullscreen = options->fullscreen,
            .start_fps_counter = options->start_fps_counter,
            .buffering_time = options->display_buffer,
        };
		
        //12.sc_screen_init()
        if (!sc_screen_init(&s->screen, &screen_params)) {
            goto end;
        }
        screen_initialized = true;

        //13.sc_decoder_add_sink()
        sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
    }
//14.sc_v4l2_sink_init()
#ifdef HAVE_V4L2
    if (options->v4l2_device) {
        if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
                               info->frame_size, options->v4l2_buffer)) {
            goto end;
        }

        sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);

        v4l2_sink_initialized = true;
    }
#endif
//15.启动流配置
    // now we consumed the header values, the socket receives the video stream
    // start the demuxer
    if (!sc_demuxer_start(&s->demuxer)) {
        goto end;
    }
    demuxer_started = true;
//16.event_loop()
    ret = event_loop(s);
    LOGD("quit...");

    // Close the window immediately on closing, because screen_destroy() may
    // only be called once the demuxer thread is joined (it may take time)
    sc_screen_hide_window(&s->screen);

end:
    // The demuxer is not stopped explicitly, because it will stop by itself on
    // end-of-stream
#ifdef HAVE_USB
    if (aoa_hid_initialized) {
        if (hid_keyboard_initialized) {
            sc_hid_keyboard_destroy(&s->keyboard_hid);
        }
        if (hid_mouse_initialized) {
            sc_hid_mouse_destroy(&s->mouse_hid);
        }
        sc_aoa_stop(&s->aoa);
        sc_usb_stop(&s->usb);
    }
    if (acksync) {
        sc_acksync_destroy(acksync);
    }
#endif
    if (controller_started) {
        sc_controller_stop(&s->controller);
    }
    if (file_pusher_initialized) {
        sc_file_pusher_stop(&s->file_pusher);
    }
    if (screen_initialized) {
        sc_screen_interrupt(&s->screen);
    }

    if (server_started) {
        // shutdown the sockets and kill the server
        sc_server_stop(&s->server);
    }

    // now that the sockets are shutdown, the demuxer and controller are
    // interrupted, we can join them
    if (demuxer_started) {
        sc_demuxer_join(&s->demuxer);
    }

#ifdef HAVE_V4L2
    if (v4l2_sink_initialized) {
        sc_v4l2_sink_destroy(&s->v4l2_sink);
    }
#endif

#ifdef HAVE_USB
    if (aoa_hid_initialized) {
        sc_aoa_join(&s->aoa);
        sc_aoa_destroy(&s->aoa);
        sc_usb_join(&s->usb);
        sc_usb_disconnect(&s->usb);
        sc_usb_destroy(&s->usb);
    }
#endif

    // Destroy the screen only after the demuxer is guaranteed to be finished,
    // because otherwise the screen could receive new frames after destruction
    if (screen_initialized) {
        sc_screen_join(&s->screen);
        sc_screen_destroy(&s->screen);
    }

    if (controller_started) {
        sc_controller_join(&s->controller);
    }
    if (controller_initialized) {
        sc_controller_destroy(&s->controller);
    }

    if (recorder_initialized) {
        sc_recorder_destroy(&s->recorder);
    }

    if (file_pusher_initialized) {
        sc_file_pusher_join(&s->file_pusher);
        sc_file_pusher_destroy(&s->file_pusher);
    }

    //17.销毁server
    sc_server_destroy(&s->server);

    return ret;
}

之后我们就在无条件循环for(;;)前创建ZeroMQ对象:

对于ZeroMQ的使用可以参考官网:ZeroMQ API - 0MQ Api

对于ZeroMQ的中文教程:ZeroMQ教程中文版_神马_逗_浮云的博客-CSDN博客_zeromq中文

//新建一个ZeroMQ对象
void *context = zmq_ctx_new();
//在使用任何ØMQ库函数之前,必须创建一个ØMQ context。在结束应用程序时,必须销毁(删除)这个context。

//创建ZMQ套接字,设定为发布订阅模式
//在.h中初始化void *response;
response=zmq_socket(context,ZMQ_PUB);

//绑定一个socket,接收发来的链接请求
int rc = zmq_bind(response,"tcp://127.0.0.1:5555");
//链接失败则返回0
assert(rc==0);

//在for循环中添加
//使用ZeroMQ发送数据
zmq_send(response,&yuv_frame,sizeof(yuv_frame),ZMQ_DONTWAIT);
    
//在for循环的最后关闭端口
zmq_close(response);

到这里我们的发送端就已经设置完成了,那么接下来就看接收端吧。

0x05 使用Python订阅ZeroMQ的发布

使用Python进行订阅:

import zmq
context = zmq.context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://127.0.0.1:5555")
socket.setsokopt(zmq.SUBSCRIBE,'')
while True:
	printf socket.rev()

在“发布-订阅模式”下,发布者绑定一个指定的网络端口,订阅者则连接到该地址,该模式下消息流是单向的,只允许从发布者流向订阅者,发布者只管发消息,不会理会是否存在订阅者。一个发布者可以拥有多个订阅者,一个订阅者也可以订阅多个发布者。

一些特点:

  • 公平排队:一个“订阅者”链接到多个发布者时,会均衡地从每个发布者读取消息,不会出现一个“发布者”淹没其他“发布者”的情况。

  • ZMQ3.0以上版本,过滤规则发生在“发布方”。ZMQ3.0以下版本,过滤规则则发生在订阅方。其实也就是处理消息的位置。

0x06 需要注意的

在编写自己的程序时可能会出现这个问题:

[-Wimplicit-function-declaration] 118 | zmq_send(response,&yuv_frame,sizeof(yuv_frame),ZMQ_DONTWAIT); | ^~~~ ../app/src/decoder.c:118:48: error: ‘ZMQ_DONTWAIT’ undeclared (first use in this function); did you mean ‘MSG_DONTWAIT’? 118 | zmq_send(response,&yuv_frame,sizeof(yuv_frame),ZMQ_DONTWAIT); | ^~~~ | MSG_DONTWAIT ../app/src/decoder.c:118:48: note: each undeclared identifier is reported only once for each function it appears in

之后发现包含了zmq.h也不行

/src_decoder.c.o -c ../app/src/decoder.c ../app/src/decoder.c:5:10: fatal error: zmq.h: 没有那个文件或目录 5 | #include <zmq.h> | ^~~ compilation terminated.

这个问题的原因是我们的Ubuntu上并没有安装zmq.h,那么安装:

sudo apt-get install libzmq3-dev

就可以解决以上问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郑烯烃快去学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值