第一步 客户端如何连接到手机端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, ¶ms); ///> 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, ¶ms) 函数,把手机端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建立后,服务器、首先发送手机型号和屏幕配置信息、然后传输视频流数据。