scrcpy 源码走读之二 客户端如何连接到手机端server

第一步 客户端如何连接到手机端scrcpy

scrcpy是通过ADB FORWARD tcp:port tcp:port 方式,开启手机端的 ADB DAEMON 的守护线程,
监听 端口的连接。
本篇就分析这一过程的具体实现,我们还是走读客户端的源码。

第二步 scrcpy 客户端网络连接过程 server_connect_to()

此函数在scrcpy初始化过程中所处于的位置如下:

bool scrcpy(const struct scrcpy_options *options) {
    
    
	static struct scrcpy scrcpy;
	struct scrcpy *s = &scrcpy;
	server_init(&s->server);										///> 1. server_init()

	struct server_params params = {
    
    
		.serial = options->serial,
		.port_range = options->port_range,
		.bit_rate = options->bit_rate,
		.max_fps = options->max_fps,
		.display_id = options->display_id,
		.codec_options = options->codec_options,
		.encoder_name = options->encoder_name,
		.force_adb_forward = options->force_adb_forward,
	};
	server_start(&s->server, &params);								///> 2. server_start();

	server_started = true;
	sdl_init_and_configure(options->display, options->render_driver,
                                options->disable_screensaver);

	server_connect_to(&s->server, device_name, &frame_size);		///> 3. server_connect_to();


	file_handler_init(&s->file_handler, s->server.serial,
                               options->push_target);				///> 4. file_handler_init();  socket init & 服务端代码adb push

	decoder_init(&s->decoder);										///> 5. decoder_init();

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

	static const struct stream_callbacks stream_cbs = {
    
    				///> 7. stream_init();
        .on_eos = stream_on_eos,
    };
    stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL);


    stream_add_sink(&s->stream, &dec->packet_sink);					///> 8. stream_add_sink(); dec

    stream_add_sink(&s->stream, &rec->packet_sink);					///> 9. stream_add_sink(); rec


    controller_init(&s->controller, s->server.control_socket);		///> 10. controller_init(); control_socket

    controller_start(&s->controller);								///> 11. controller_start();


    struct screen_params screen_params = {
    
    
            .window_title = window_title,
            .frame_size = 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,
            .buffering_time = options->display_buffer,
    };
    screen_init(&s->screen, &screen_params);						///> 12. screen_init();

    decoder_add_sink(&s->decoder, &s->screen.frame_sink);			///> 13. decoder_add_sink();

#ifdef HAVE_V4L2
    sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size,
                               options->v4l2_buffer);				///> 14. sc_v4l2_sink_init();
    decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
#endif
    
    stream_start(&s->stream);                                ///> 14+.流启动配置,第一次发布时遗漏咯,很抱歉.补充上。 
    input_manager_init(&s->input_manager, &s->controller, &s->screen, options);		///> 15. input_manager_init();

    ret = event_loop(s, options);													///> 16. event_loop();


    ///> 程序推出释放资源相关内容
    screen_hide_window(&s->screen);
    controller_stop(&s->controller);
    file_handler_stop(&s->file_handler);
    screen_interrupt(&s->screen);
    server_stop(&s->server);
    stream_join(&s->stream);
    sc_v4l2_sink_destroy(&s->v4l2_sink);
    screen_join(&s->screen);
    screen_destroy(&s->screen);
    controller_join(&s->controller);
    controller_destroy(&s->controller);
    recorder_destroy(&s->recorder);
    file_handler_join(&s->file_handler);
    file_handler_destroy(&s->file_handler);

    server_destroy(&s->server);       										///> 销毁 server
    return ret;
}    

第三步 scrcpy_main() 函数获取程序运行入口参数

此部分内容应该在上一篇中分析,忽略函数入口参数值,走读代码有点费劲,特此补充此部分内容。

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

     struct scrcpy_cli_args args = {
    
    
        .opts = SCRCPY_OPTIONS_DEFAULT,
        .help = false,
        .version = false,
    };

    scrcpy_parse_args(&args, argc, argv);    ///> 此函数是解析入口参数程序

    sc_set_log_level(args.opts.log_level);

    av_register_all();                      ///> FFmpeg 注册所有的格式。包括解封装格式和加封装格式。

    avdevice_register_all();                ///> FFmpeg 高级功能初始化

    avformat_network_init();                ///> 用于初始化网络。FFmpeg本身也支持解封装RTSP的数据,如果要解封装网络数据格式,则可调用该函数。

    int res = scrcpy(&args.opts) ? 0 : 1;   ///> running scrcpy 函数

    avformat_network_deinit();              ///> ignore failure

    return res;
}

///> scrcpy 程序运行的缺省参数,此部分宏定义内容在 meson.build 文件中,
///> 如:DEFAULT_LOCAL_PORT_RANGE_FIRST = 27183,
#define SCRCPY_OPTIONS_DEFAULT {
      
       \
    .serial = NULL, \
    .crop = NULL, \
    .record_filename = NULL, \
    .window_title = NULL, \
    .push_target = NULL, \
    .render_driver = NULL, \
    .codec_options = NULL, \
    .encoder_name = NULL, \
    .v4l2_device = NULL, \
    .log_level = SC_LOG_LEVEL_INFO, \
    .record_format = SC_RECORD_FORMAT_AUTO, \
    .port_range = {
      
       \
        .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \   //> 27183
        .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \     //> 27199
    }, \
    .shortcut_mods = {
    
     \
        .data = {
    
    SC_MOD_LALT, SC_MOD_LSUPER}, \
        .count = 2, \
    }, \
    .max_size = 0, \
    .bit_rate = DEFAULT_BIT_RATE, \
    .max_fps = 0, \
    .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \
    .rotation = 0, \
    .window_x = SC_WINDOW_POSITION_UNDEFINED, \
    .window_y = SC_WINDOW_POSITION_UNDEFINED, \
    .window_width = 0, \
    .window_height = 0, \
    .display_id = 0, \
    .display_buffer = 0, \
    .v4l2_buffer = 0, \
    .show_touches = false, \
    .fullscreen = false, \
    .always_on_top = false, \
    .control = true, \
    .display = true, \
    .turn_screen_off = false, \
    .prefer_text = false, \
    .window_borderless = false, \
    .mipmaps = true, \
    .stay_awake = false, \
    .force_adb_forward = false, \
    .disable_screensaver = false, \
    .forward_key_repeat = true, \
    .forward_all_clicks = false, \
    .legacy_paste = false, \
    .power_off_on_close = false, \
}

主函数 功能相对比较笼统,初始化 ffmpeg 组件、把函数入口缺省参数配置一下,
程序就转到 scrcpy 函数。
我们重点是看获取函数入口参数的程序内容,如下。

scrcpy_parse_args(&args, argc, argv)
{
    
    
    struct scrcpy_options *opts = &args->opts;

    while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
                            long_options, NULL)) != -1) {
    
    
        switch (c) {
    
    
            case 'b':
                if (!parse_bit_rate(optarg, &opts->bit_rate)) {
    
    
                    return false;
                }
                break;
            case 'c':
                LOGW("Deprecated option -c. Use --crop instead.");
                // fall through
            case OPT_CROP:
                opts->crop = optarg;
                break;
            case OPT_DISPLAY_ID:
                if (!parse_display_id(optarg, &opts->display_id)) {
    
    
                    return false;
                }
                break;
            case 'f':
                opts->fullscreen = true;
                break;
            case 'F':
                LOGW("Deprecated option -F. Use --record-format instead.");
                // fall through
            case OPT_RECORD_FORMAT:
                if (!parse_record_format(optarg, &opts->record_format)) {
    
    
                    return false;
                }
                break;
            case 'h':
                args->help = true;
                break;
            case OPT_MAX_FPS:
                if (!parse_max_fps(optarg, &opts->max_fps)) {
    
    
                    return false;
                }
                break;
            case 'm':
                if (!parse_max_size(optarg, &opts->max_size)) {
    
    
                    return false;
                }
                break;
            case OPT_LOCK_VIDEO_ORIENTATION:
                if (!parse_lock_video_orientation(optarg,
                        &opts->lock_video_orientation)) {
    
    
                    return false;
                }
                break;
            case 'n':
                opts->control = false;
                break;
            case 'N':
                opts->display = false;
                break;
            case 'p':
                if (!parse_port_range(optarg, &opts->port_range)) {
    
    
                    return false;
                }
                break;
            case 'r':
                opts->record_filename = optarg;
                break;
            case 's':             ///> 此处是给 opts->serial 赋值部分,
                opts->serial = optarg; ///> 例如: scrcpy -s 192.168.1.107:5555 此部分ip+port就是 serial 的内容。
                break;
            case 'S':
                opts->turn_screen_off = true;
                break;
            case 't':
                opts->show_touches = true;
                break;
            case 'T':
                LOGW("Deprecated option -T. Use --always-on-top instead.");
                // fall through
            case OPT_ALWAYS_ON_TOP:
                opts->always_on_top = true;
                break;
            case 'v':
                args->version = true;
                break;
            case 'V':
                if (!parse_log_level(optarg, &opts->log_level)) {
    
    
                    return false;
                }
                break;
            case 'w':
                opts->stay_awake = true;
                break;
            case OPT_RENDER_EXPIRED_FRAMES:
                LOGW("Option --render-expired-frames has been removed. This "
                     "flag has been ignored.");
                break;
            case OPT_WINDOW_TITLE:
                opts->window_title = optarg;
                break;
            case OPT_WINDOW_X:
                if (!parse_window_position(optarg, &opts->window_x)) {
    
    
                    return false;
                }
                break;
            case OPT_WINDOW_Y:
                if (!parse_window_position(optarg, &opts->window_y)) {
    
    
                    return false;
                }
                break;
            case OPT_WINDOW_WIDTH:
                if (!parse_window_dimension(optarg, &opts->window_width)) {
    
    
                    return false;
                }
                break;
            case OPT_WINDOW_HEIGHT:
                if (!parse_window_dimension(optarg, &opts->window_height)) {
    
    
                    return false;
                }
                break;
            case OPT_WINDOW_BORDERLESS:
                opts->window_borderless = true;
                break;
            case OPT_PUSH_TARGET:
                opts->push_target = optarg;
                break;
            case OPT_PREFER_TEXT:
                opts->prefer_text = true;
                break;
            case OPT_ROTATION:
                if (!parse_rotation(optarg, &opts->rotation)) {
    
    
                    return false;
                }
                break;
            case OPT_RENDER_DRIVER:
                opts->render_driver = optarg;
                break;
            case OPT_NO_MIPMAPS:
                opts->mipmaps = false;
                break;
            case OPT_NO_KEY_REPEAT:
                opts->forward_key_repeat = false;
                break;
            case OPT_CODEC_OPTIONS:
                opts->codec_options = optarg;
                break;
            case OPT_ENCODER_NAME:
                opts->encoder_name = optarg;
                break;
            case OPT_FORCE_ADB_FORWARD:
                opts->force_adb_forward = true;
                break;
            case OPT_DISABLE_SCREENSAVER:
                opts->disable_screensaver = true;
                break;
            case OPT_SHORTCUT_MOD:
                if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) {
    
    
                    return false;
                }
                break;
            case OPT_FORWARD_ALL_CLICKS:
                opts->forward_all_clicks = true;
                break;
            case OPT_LEGACY_PASTE:
                opts->legacy_paste = true;
                break;
            case OPT_POWER_OFF_ON_CLOSE:
                opts->power_off_on_close = true;
                break;
            case OPT_DISPLAY_BUFFER:
                if (!parse_buffering_time(optarg, &opts->display_buffer)) {
    
    
                    return false;
                }
                break;
#ifdef HAVE_V4L2
            case OPT_V4L2_SINK:
                opts->v4l2_device = optarg;
                break;
            case OPT_V4L2_BUFFER:
                if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) {
    
    
                    return false;
                }
                break;
#endif
            default:
                // getopt prints the error message on stderr
                return false;
        }
    }
}

跟踪 serial 变量赋值过程,可以看到用户启动 scrcpy 时入口参数为 ‘-s’ 时, 函数就把 IP : PORT 的值赋值给 serial 变量,
如: scrcpy -s 192.168.5.107:5555 -b 5M … 此命令,seriarl = “192.168.5.107:5555” 值。
分析此端函数主要目的是找到 server_socket 的 IP地址是多少,为下面的程序分析做准备。

第四步 server_connect_to() 函数

上一篇的 server_start(&s->server, &params) 函数,把手机端scrcpy程序通过 app_process 方式运行成功。
程序接下来执行的 server_connect_to() 函数。

///> 1. 函数 server_connect_to()
server_connect_to(&s->server, device_name, &frame_size)
{
    
    
    if (!server->tunnel_forward) {
    
              ///>程序系统调用 adb forward tcp:5555 tcp:5555 会设置 tunnel_forward = true
        
        server->video_socket = net_accept(server->server_socket);

        server->control_socket = net_accept(server->server_socket);

    }else{
    
    
                                            ///> 因此 scrcpy 连接是本地 ip 和 port;
        server->video_socket =
            connect_to_server(server->local_port, attempts, delay);  ///> 视频socket连接本地ip和本地端口并读取服务端发送过来的1字节0,
                                                                     ///> 缺省与服务器连接时成功的。
        server->control_socket =
            net_connect(IPV4_LOCALHOST, server->local_port);    ///> IPV4_LOCALHOST =  0x7F000001(127.0.0.1)
    }                                                           ///> 控制socket连接本地IP和本地端口.

}

///> 2. accept server_socket 端口.
net_accept(server->server_socket)
{
    
    
    SOCKADDR_IN csin;
    socklen_t sinsize = sizeof(csin);
    return accept(server_socket, (SOCKADDR *) &csin, &sinsize);   ///> 系统网络函数 accept 接受客户端连接。
}

///> 3. 
connect_to_server(server->local_port, attempts, delay)
{
    
    
    socket_t socket = connect_and_read_byte(port);   ///> 该函数是中间函数,实际调用函数为此函数
}

///> 4. 连接 本地端口并验证模式
connect_and_read_byte(port)
{
    
    
    socket_t socket = net_connect(IPV4_LOCALHOST, port);        ///> 此地址 IPV4_LOCALHOST =  0x7F000001(127.0.0.1)
    char byte;
    // the connection may succeed even if the server behind the "adb tunnel"
    // is not listening, so read one byte to detect a working connection
    if (net_recv(socket, &byte, 1) != 1) {
    
                          ///> 验证 网络通信方法
        // the server is not listening yet behind the adb tunnel
        net_close(socket);
        return INVALID_SOCKET;
    }
    return socket;
}

///> 5.net_connect(IPV4_LOCALHOST, port)
socket_t net_connect(uint32_t addr, uint16_t port) {
    
    
    socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET) {
    
    
        net_perror("socket");
        return INVALID_SOCKET;
    }

    SOCKADDR_IN sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = htonl(addr);
    sin.sin_port = htons(port);

    if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
    
    
        net_perror("connect");
        net_close(sock);
        return INVALID_SOCKET;
    }
    return sock;
}

由代码可见,scrcpy 客户端(c语言端代码定义为客户端)程序网络通信模式主动连接本地ip和端口,
网络通信模式也是作为客户端。此处的疑问是程序为啥没有直接连接scrcpy的服务端IP地址,而是
本机的ip地址呢?

第五步 实验验证

实例分析:以下过程是我实验过程记录。

### 打开 adb tcpip 5555 端口
robot@ubuntu:~/scrcpy/scrcpy$ adb tcpip 5555

### 链接手机的wifi网络
robot@ubuntu:~/scrcpy/scrcpy$ adb connect 192.168.5.107:5555
connected to 192.168.5.107:5555

### 通过 WIFI 链接手机上的 scrcpy 程序运行,手工执行 ./build/app/scrcpy 程序
robot@ubuntu:~/scrcpy/scrcpy$ ./build/app/scrcpy -s 192.168.5.107:5555 -b 5000000
INFO: scrcpy 1.19 <https://github.com/Genymobile/scrcpy>
../app/src/adb.c, ADB-CMD: adb -s 192.168.5.107:5555 push                                    ///> 第一步 adb -s ip:port push  服务端程序到android手机的路径和文件
/usr/local/share/scrcpy/scrcpy-server:...shed. 1.3 MB/s (37330 bytes in 0.028s)
../app/src/adb.c, ADB-CMD: adb -s 192.168.5.107:5555 reverse localabstract:scrcpy tcp:27183  ///> 第二步 ADB REVERSE 设置反向代理模式,27183是本机端口,错误未成功。
error: more than one device/emulator
ERROR: "adb reverse" returned with value 1
WARN: 'adb reverse' failed, fallback to 'adb forward'
../app/src/adb.c, ADB-CMD: adb -s 192.168.5.107:5555 forward  tcp:27183 localabstract:scrcpy ///> 第三步 设置转发 tcp:27183 localabstract:scrcpy,把27183本机端口转发值服务器端
../app/src/adb.c, ADB-CMD: adb -s 192.168.5.107:5555 shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.19\ 
                                                                     info 0 5000000 0 -1 true - true true 0 false false -  //> 第四步 ADB SHELL启动scrcpy-server 程序。 
[server] INFO: Device: HUAWEI PRA-AL00X (Android 8.0.0)
../app/src/adb.c, ADB-CMD: adb -s 192.168.5.107:5555 forward  --remove tcp:27183            ///> 第五步 移除 adb 转发池中 tcp:27183 端口.
INFO: Renderer: opengl
INFO: OpenGL version: 3.3 (Compatibility Profile) Mesa 21.0.3
INFO: Trilinear filtering enabled
INFO: Initial texture: 1080x1920

此部分实验过程内容是学习认识 scrcpy 的宝贵资料,实验结果与代码走读结果相符。
其中第二步和第三步的逻辑,为什么要这么处理呢?大家自行分析。

第六步 客户端连接云手机中的 scrcpy-server 过程描述

第一部分 云手机部署 scrcpy-server过程
robot@ubuntu:~/scrcpy/scrcpy$ adb tcpip 5555
robot@ubuntu:~/scrcpy/scrcpy$ adb connect 192.168.5.107:5555
robot@ubuntu:~/scrcpy/scrcpy$ adb -s 192.168.5.107:5555 shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.19
脚本部署(前置条件是软件已经拷贝到/data/local/tmp/路径下):
CLASSPATH=/data/local/tmp/server-debug.apk app_process /system/bin com.genymobile.scrcpy.Server 1.19 info 0 5000000 0 -1 true - true true 0 false false -  - false
[server] INFO: Device: unknown Android SDK built for x86_64 (Android 8.1.0)
至此云端部署完成

第二部分 客户端连接云手机过程
robot@ubuntu:$ adb connect 192.168.5.107:5555
robot@ubuntu:$ adb -s 192.168.5.107:5555 forward  tcp:27555 localabstract:scrcpy
robot@ubuntu:$ 本地程序建立2个至 127.0.0.1:27555 的连接,第一个连接缺省为视频流socket,第二个连接为控制流socket。
备注:当视频的socket建立连接后,会接收到00确认连接码,当控制流socket建立后,服务器、首先发送手机型号和屏幕配置信息、然后传输视频流数据。

猜你喜欢

转载自blog.csdn.net/weixin_38387929/article/details/121029492
今日推荐