简介
前面一篇文章介绍了scrcpy 的使用,以及对应的编译,可以实现在Ubuntu 编译调试的客户端代码,这篇文章会介绍 简要的介绍下scrcpy的源码实现,在介绍scrcpy源码之前,可以查看 https://github.com/Genymobile/scrcpy/blob/master/DEVELOP.md 关于scrcpy的大概实现
比如客户端包含了三个线程,每个线程负责的工作
服务端包含了俩个线程,每个线程负责的工作
scrcpy的原理
从上面的链接也可以大概的知道 scrcpy 的实现原理,服务端负责发送界面上的数据到客户端,客户端使用ffmpeg,sdl 显示出来,客户端要监听界面上的操作,如果有操作触发的时候,要将操作发送给服务端,服务端,利用发射的原理 模拟用户调用的过程
源码分析
首先是客户端启动,也即是对应的main方法运行, 这里启动运行的时候,并没有传递任何的参数,所以 args代表默认的参数
int main(int argc, char *argv[]) {
#ifdef __WINDOWS__
// disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL);
setbuf(stderr, NULL);
#endif
//默认的参数选项
struct args args = {
.serial = NULL,
.crop = NULL,
.record_filename = NULL,
.help = SDL_FALSE,
.version = SDL_FALSE,
.show_touches = SDL_FALSE,
.port = DEFAULT_LOCAL_PORT, // #define DEFAULT_LOCAL_PORT 27183
.max_size = DEFAULT_MAX_SIZE, //#define DEFAULT_MAX_SIZE 0
.bit_rate = DEFAULT_BIT_RATE, // #define DEFAULT_BIT_RATE 8000000
};
//解析参数,当前我们没有传递参数进来
if (!parse_args(&args, argc, argv)) {
return 1;
}
...
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
//注册FFmpeg
av_register_all();
#endif
//FFmpeg 注册网络
if (avformat_network_init()) {
return 1;
}
//将解析之后的选项 赋值到options 中,这个代表最终的选项,也即是后面要应用的选项结构体
struct scrcpy_options options = {
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
};
//启动
int res = scrcpy(&options) ? 0 : 1;
avformat_network_deinit(); // ignore failure
...
}
客户端main函数做的操作,主要是解析传递过来的参数,当然如果你没有传递参数的时候,也有默认的实现,当参数解析完之后,将最终的参数结果保存到 options 中,之后调用
scrcpy(&options),这才是关键的实现
//启动scrcpy
SDL_bool scrcpy(const struct scrcpy_options *options) {
//判断是否需要录制文件
SDL_bool send_frame_meta = !!options->record_filename;
//1 使用adb连接手机,发送命令 ,如果返回值为false,代表出现了错误
if (!server_start(&server, options->serial, options->port,options->max_size, options->bit_rate, options->crop, send_frame_meta)) {
return SDL_FALSE;
}
process_t proc_show_touches = PROCESS_NONE;
SDL_bool show_touches_waited;
//2 是否有设置显示touch
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
show_touches_waited = SDL_FALSE;
}
//下面初始化SDL
SDL_bool ret = SDL_TRUE;
//3 初始化SDL 激活视频子模块
if (!sdl_init_and_configure()) {
ret = SDL_FALSE;
goto finally_destroy_server;
}
//4 连接server端 ,返回连接的socket
socket_t device_socket = server_connect_to(&server);
//验证socket是否合法
if (device_socket == INVALID_SOCKET) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
//#define DEVICE_NAME_FIELD_LENGTH 64 ,用来存储设备的名称
char device_name[DEVICE_NAME_FIELD_LENGTH];
struct size frame_size;
//5 当连接上的时候,服务端会告知客户端设备的名称,以及视频的大小
if (!device_read_info(device_socket, device_name, &frame_size)) {
//读取失败,停止服务
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
//6 根据客户端告知的视频的大小,初始化SDL 视频显示的界面
if (!frames_init(&frames)) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
//7 初始化file_handler
if (!file_handler_init(&file_handler, server.serial)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_frames;
}
struct recorder *rec = NULL;
//8 如果选项里面有制定录制文件
if (options->record_filename) {
if (!recorder_init(&recorder, options->record_filename, frame_size)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_file_handler;
}
rec = &recorder;
}
//9 初始化解码的结构体
decoder_init(&decoder, &frames, device_socket, rec);
// now we consumed the header values, the socket receives the video stream
// start the decoder
//10 创建解码的线程,用来事实的将客户端的界面渲染出来
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_recorder;
}
//11 初始化control 结构体
if (!controller_init(&controller, device_socket)) {
ret = SDL_FALSE;
goto finally_stop_decoder;
}
//12 初始化 controller 线程,这个线程的作用就是发送界面上的点击事件给服务端
if (!controller_start(&controller)) {
ret = SDL_FALSE;
goto finally_destroy_controller;
}
//13 初始化SDL 显示窗口 device_name 为 客户端告知的设备号, fame_size 为当前设备
if (!screen_init_rendering(&screen, device_name, frame_size)) {
ret = SDL_FALSE;
goto finally_stop_and_join_controller;
}
//是否要显示触摸点
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = SDL_TRUE;
}
//是否要显示全屏
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
//14 事件循环
ret = event_loop();
LOGD("quit...");
screen_destroy(&screen);
...
return ret;
}
可以看到这个函数每一步的操作都是非常关键的,下面会分点的方式依次来解析 ,首先是
//使用adb连接手机,发送命令
if (!server_start(&server, options->serial, options->port,options->max_size, options->bit_rate, options->crop, send_frame_meta)) {
return SDL_FALSE;
}
//server_start函数实现
SDL_bool server_start(struct server *server, const char *serial,
Uint16 local_port, Uint16 max_size, Uint32 bit_rate,
const char *crop, SDL_bool send_frame_meta) {
server->local_port = local_port;
//serial不为空,代表在多个手机的时候选择一个手机
if (serial) {
server->serial = SDL_strdup(serial);
if (!server->serial) {
return SDL_FALSE;
}
}
//启动adb push 将服务端的jar推送到手机上,如果返回false,代表出现了错误,返回false
if (!push_server(serial)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
//标识推送jar成功
server->server_copied_to_device = SDL_TRUE;
//执行 adb reverse ,adb forward 隐射,也即是至少有一个成功,要不然都会返回false
if (!enable_tunnel(server)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
//如果到了这里,并且tunnel_forward 为fale,代表执行 adb reverse 成功 ,也即是将远端的手机端口隐射到了pc端口上
if (!server->tunnel_forward) {
//远端的手机端口映射到了本地的pc端口上,监听这个端口
server->server_socket = listen_on_port(local_port);
//判断是否出现了错误,出现了错误,删除端口映射 ,释放资源
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
SDL_free((void *) server->serial);
return SDL_FALSE;
}
}
//服务端将会连接我们的server_socket ,这一步骤就是启动我们服务端的服务了
server->process = execute_server(serial, max_size, bit_rate,
server->tunnel_forward, crop,
send_frame_meta);
//如果执行不成功,返回false ,关闭socket
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
SDL_free((void *) server->serial);
return SDL_FALSE;
}
server->tunnel_enabled = SDL_TRUE;
return SDL_TRUE;
}
执行 push_server 函数,这个函数主要是将我们的服务端jar 使用adb 命令发送给服务器,下面是对应的函数实现
//推送jar
static SDL_bool push_server(const char *serial) {
//启动adb 推送服务端的jar到 /data/local/tmp/scrcpy-server.jar 中 process 为执行的结果
process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH);
//检查结果 如果不为PROCESS_SUCCESS 返回false
return process_check_success(process, "adb push");
}
//获取到jar存放的目录
static const char *get_server_path(void) {
//如果环境变量中有设置,就取环境变量的值,否则取默认的
const char *server_path = getenv("SCRCPY_SERVER_PATH");
if (!server_path) {
//PREFIX PREFIXED_SERVER_PATH /home/yuhui/WorkSpace/scrcpy/app/scrcpy-server.jar 这个路径就是我存放jar的路径
server_path = DEFAULT_SERVER_PATH;
}
return server_path;
}
而PREFIX,PREFIXED_SERVER_PATH 是在编译scrcpy的时候生成的config.h文件中定义的
//服务端jar的路径
#define PREFIX "/home/yuhui/WorkSpace/scrcpy/app"
#define PREFIXED_SERVER_PATH "/scrcpy-server.jar"
//执行 adb push命令 ,对于 push_server 调用来说, local 为 我本地存放这个jar的路径 /home/yuhui/WorkSpace/scrcpy/app/scrcpy-server.jar remote为服务端存放jar的地方
//也即是 /data/local/tmp/scrcpy-server.jar
process_t adb_push(const char *serial, const char *local, const char *remote) {
...
//local为本地的server的jar remote为服务端jar存放的路径
const char *const adb_cmd[] = {"push", local, remote};
//执行adb push
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
...
//返回执行的结果
return proc;
}
//执行adb的命令
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len) {
const char *cmd[len + 4];
int i;
process_t process;
//cmd[0] = adb
cmd[0] = get_adb_command();
//serial代表是否是在多个手机中选择一个连接
if (serial) {
cmd[1] = "-s";
cmd[2] = serial;
i = 3;
} else {
i = 1;
}
//将adb要执行的参数,填充到cmd 中
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
//字符串的结束标识
cmd[len + i] = NULL;
//执行adb的命令,r 代表执行的结果
enum process_result r = cmd_execute(cmd[0], cmd, &process);
//如果出现了错误
if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r);
return PROCESS_NONE;
}
return process;
}
这个函数是主要完成adb 命令的拼接,对于pursh_server 来说,当前的命令就为 adb push /home/yuhui/WorkSpace/scrcpy/app/scrcpy-server.jar /data/local/tmp/scrcpy-server.jar
如果当前只有连接一个手机的化,当然如果当前有多个手机拼接的命令就变成了 adb -s "手机的编号" push .... 之后执行 cmd_execute() 函数
//执行adb的命令
enum process_result cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
int fd[2];
//首先创建一个管道
if (pipe(fd) == -1) {
perror("pipe");
return PROCESS_ERROR_GENERIC;
}
enum process_result ret = PROCESS_SUCCESS;
//fork 进程, 注意fork会被执行俩次,第一次执行fork大于0 代表为父进程,第二次执行fork 为子进程,所以对于管道来说,子进程父进程都是有的
*pid = fork();
//fork失败
if (*pid == -1) {
perror("fork");
ret = PROCESS_ERROR_GENERIC;
goto end;
}
//父进程
if (*pid > 0) {
// parent close write side 关闭掉写的一断
close(fd[1]);
fd[1] = -1;
// wait for EOF or receive errno from child 父进程监听读的一端,这个函数会阻塞,这里父进程在等到子进程的执行结果,这里父进程监听到结果之后,将内容赋值到ret中
if (read(fd[0], &ret, sizeof(ret)) == -1) {
perror("read");
ret = PROCESS_ERROR_GENERIC;
goto end;
}
} else if (*pid == 0) {//子进程
// child close read side 子进程关闭掉读的一段
close(fd[0]);
//fcntl是计算机中的一种函数,通过fcntl可以改变已打开的文件性质。f
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
//execvp(执行文件) 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。 也即是在子进程中执行adb的命令
execvp(path, (char *const *)argv);
//ENOENT 2 无此文件或目录
if (errno == ENOENT) {
ret = PROCESS_ERROR_MISSING_BINARY;
} else {
ret = PROCESS_ERROR_GENERIC;
}
//出现了错误
perror("exec");
} else {
perror("fcntl");
ret = PROCESS_ERROR_GENERIC;
}
// send ret to the parent 发送执行的结果给父进程
if (write(fd[1], &ret, sizeof(ret)) == -1) {
perror("write");
}
// close write side before exiting 之后子进程退出
close(fd[1]);
_exit(1);
}
end:
//关闭管道
if (fd[0] != -1) {
close(fd[0]);
}
if (fd[1] != -1) {
close(fd[1]);
}
return ret;
}
这个函数通过创建一个管道用于子进程跟父进程通信,在子进程中调用了 execvp函数,执行命令 ,将执行的结果 发送给父进程,之后子进程退出,这里要注意,如果父进程先退出的化,
子进程就变成了孤儿进程了,执行 execvp函数的时候,如果当前你并没有设置adb的环境变量,这里会出现错误的,最终将结果一层层返回,最终回到调用 push_server 函数,
如果返回值为false 就代表出现了错误
if (!push_server(serial)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
接着 执行 adb reverse ,adb forward 隐射,也即是至少有一个成功,要不然都会返回false
if (!enable_tunnel(server)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
static SDL_bool enable_tunnel(struct server *server) {
//执行adb reverse命令 将手机指定的端口隐射到pc 的local_port,如果允许 就直接返回true了
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return SDL_TRUE;
}
//那到了这里就是不允许反向隐射 下面执行正向的隐射
LOGW("'adb reverse' failed, fallback to 'adb forward'");
//标示为 使用adb forward 替换 adb referse
server->tunnel_forward = SDL_TRUE;
//执行 adb forward命令 完成端口的隐射 将本地PC指定Port端口,映射到设备手机指定Port端口上.
return enable_tunnel_forward(server->serial, server->local_port);
}
static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
//检查adb reverse执行的结果
return process_check_success(process, "adb reverse");
}
//执行正向隐射
static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward");
}
对于enable_tunnel函数,主要是执行 adb reverse 判断是否能完成反向映射,如果成功,就直接返回,如果不成功,执行 adb forward 完成端口的正向隐射,对于adb命令的执行,这里就不分析了,
前面已经分析过,接着判断是否反向映射成功,如果成功的化,我们就要执行tcp的端口监听
//如果到了这里,并且tunnel_forward 为fale,代表执行 adb reverse 成功 ,也即是将远端的手机端口隐射到了pc端口上
if (!server->tunnel_forward) {
//远端的手机端口映射到了本地的pc端口上,监听这个端口
server->server_socket = listen_on_port(local_port);
//判断是否出现了错误,出现了错误,删除端口映射 ,释放资源
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
SDL_free((void *) server->serial);
return SDL_FALSE;
}
}
//监听端口
static socket_t listen_on_port(Uint16 port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
//SOCK_STREAM 代表当前 创建的是一个tcp的socket
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
return INVALID_SOCKET;
}
int reuse = 1;
//设置socket的属性 SO_REUSEADDR 使得这个监听的端口变成可以复用的。
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) {
perror("setsockopt(SO_REUSEADDR)");
}
SOCKADDR_IN sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY
sin.sin_port = htons(port);
//绑定端口
if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("bind");
return INVALID_SOCKET;
}
//坚听端口
if (listen(sock, backlog) == SOCKET_ERROR) {
perror("listen");
return INVALID_SOCKET;
}
//返回当前的socket对象
return sock;
}
接着客户端发送指令 启动服务端的jar 服务端将会连接我们的server_socket ,这一步骤就是启动我们服务端的服务了
server->process = execute_server(serial, max_size, bit_rate,
server->tunnel_forward, crop,
send_frame_meta);
//执行服务端的jar tunnel_forward 代表是否执行 adb forward ,如果为false为 执行了 adb referse
static process_t execute_server(const char *serial,
Uint16 max_size, Uint32 bit_rate,
SDL_bool tunnel_forward, const char *crop,
SDL_bool send_frame_meta) {
char max_size_string[6];
char bit_rate_string[11];
//C 语言中的 格式化输出
sprintf(max_size_string, "%"PRIu16, max_size);
sprintf(bit_rate_string, "%"PRIu32, bit_rate);
//在android系统中运行jar操作步骤:
//1. 打包编译jar包
//2. 将jar包导入android设备中
//adb push test.jar /data/local/tmp //将PC端编译好的jar包push到android设备中的/data/local/tmp目录下
//3. 设置CLASSPATH
//export CLASSPATH=/data/local/tmp/apt.jar
//4. 启动jar
//app_process /data/local/tmp com.app.process.test.Test // data/local/apt.jar包所在路径,com.app.process.test.Test含有main方法的类名
//要执行的命令
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"app_process",
"/", // unused
"com.genymobile.scrcpy.Server",
max_size_string,
bit_rate_string,
tunnel_forward ? "true" : "false",
crop ? crop : "''",
send_frame_meta ? "true" : "false",
};
//adb shell 执行命令,这样手机端指定路径下的jar就会被启动,执行main函数
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
客户端通过执行 adb shell 命令将服务端jar启动起来,这样服务端的程序就启动起来了,此时我们就有必要分析下服务端的代码了
//客户端通过adb shell 执行命令,让服务端的jar启动起来, 也即是会执行这个main函数,args为传递的参数
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e);
}
});
//解析传递过来的参数,配置运行
Options options = createOptions(args);
//启动服务端的逻辑,根据客户端传递过来的参数
scrcpy(options);
}
createOptions 函数完成客户端传递参数的解析,并将最终的参数保存到 Options ,这里有一个参数比较重要 tunnelForward ,代表客户端是否要监听端口
private static Options createOptions(String... args) {
Options options = new Options();
...
//解析mas_size属性
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
options.setMaxSize(maxSize);
//解析
int bitRate = Integer.parseInt(args[1]);
options.setBitRate(bitRate);
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
//解析客户端传递的 tunnel_forward 标识,这个标识了 客户端是执行了 adb forward 还是执行了 adb referse 之后的结果,如果为true 代表 客户端执行了 adb forward
//也即是客户端将pc端口映射到了 手机的的端口上, 这个时候,服务端要完成监听端口的操作
boolean tunnelForward = Boolean.parseBoolean(args[2]);
//标识客户端是否执行了 adb forward
options.setTunnelForward(tunnelForward);
...
return options;
}
之后服务端执行 scrcpy(options) 函数
private static void scrcpy(Options options) throws IOException {
//构建Device 对象,确定屏幕的大小,视频的大小,注册屏幕旋转监听
final Device device = new Device(options);
//判断服务端是否需要监听端口
boolean tunnelForward = options.isTunnelForward();
//获取跟客户端的连接对象,并且发送当前手机的型号,屏幕的大小
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
//从客户端传递过来的参数中,初始化ScreenEncoder 对象
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
// asynchronous 服务端开启一个控制同步线程,用于同步客户端发送的按键内容
startEventController(device, connection);
try {
// synchronous 主线程主要用来压缩屏幕的视频界面发送给客户端显示
screenEncoder.streamScreen(device, connection.getFd());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
}
}
}
public Device(Options options)
{
//根据客户端传递过来的参数,确定屏幕的大小
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
//给WindowManagerSerice 中的 watchRotation 方法,注册这个AIDL回调监听,所以当rotation 改变的时候,这里会接收到内容
registerRotationWatcher(new IRotationWatcher.Stub()
{
@Override
public void onRotationChanged(int rotation) throws RemoteException
{
//当WindowMangerService 监听到屏幕发生了旋转的时候,会触发这个这个方法
synchronized (Device.this)
{
screenInfo = screenInfo.withRotation(rotation);
// notify
if (rotationListener != null)
{
rotationListener.onRotationChanged(rotation);
}
}
}
});
}
接着服务端执行 DesktopConnection connection = DesktopConnection.open(device, tunnelForward)
//tunnelForward 为服务端是否需要监听端口,还是连接端口
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket socket;
//监听端口
if (tunnelForward) {
//内部会调用accept函数,会阻塞直到有客户端连接上。才会返回
socket = listenAndAccept(SOCKET_NAME);
// send one byte so the client may read() to detect a connection error
//所以如果到了这里,就说明客户端已经连接上了,服务端发送一个字节告知他们连接成功
socket.getOutputStream().write(0);
} else {
//如果为false 代表 服务端直接执行连接,客户端在监听
socket = connect(SOCKET_NAME);
}
//到了这里就说明俩者已经连接 , 构建一个DesktopConnection 代表俩者的连接对象
DesktopConnection connection = new DesktopConnection(socket);
//服务端得到手机的视频大小
Size videoSize = device.getScreenInfo().getVideoSize();
//服务端告知客户端 当前设备的名称 以及 当前屏幕的大小
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
}
//listenAndAccept 函数实现 服务端注册一个tcp的sokcet 等待连接
private static LocalSocket listenAndAccept(String abstractName) throws IOException {
//创建一个Socket
LocalServerSocket localServerSocket = new LocalServerSocket(abstractName);
try {
//等待连接,这里要注意这个函数会阻塞的,直到有客户端连接上
return localServerSocket.accept();
} finally {
localServerSocket.close();
}
}
如果服务端要负责监听端口的化,会执行 localServerSocket.accept(),阻塞直到客户端有连接,要不然是不会往下执行的,此时回到客户端代码,此时客户端的server_start函数就执行完毕了,回到scrcpy 函数,执行第二点
process_t proc_show_touches = PROCESS_NONE;
SDL_bool show_touches_waited;
//是否有设置显示touch
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
show_touches_waited = SDL_FALSE;
}
//执行 adb shell settings put system show_touches true
static process_t set_show_touches_enabled(const char *serial, SDL_bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
判断是否要显示touch 对应的也是调用adb 命令
接着执行 下面初始化SDL
SDL_bool ret = SDL_TRUE;
//初始化SDL 激活视频子模块
if (!sdl_init_and_configure()) {
ret = SDL_FALSE;
goto finally_destroy_server;
}
//SDL初始化
SDL_bool sdl_init_and_configure(void) {
//分析SDL的初始化函数SDL_Init()。该函数可以确定希望激活的子系统。SDL_Init()函数原型如下 SDL_INIT_VIDEO:视频
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGC("Could not initialize SDL: %s", SDL_GetError());
return SDL_FALSE;
}
//atexit()用来设置一个程序正常结束前调用的函数. 当程序通过调用exit()或从main 中返回时, 参数function 所指定的函数会先被调用, 然后才真正由exit()结束程序. 可以用来释放
atexit(SDL_Quit);
// Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
LOGW("Could not enable bilinear filtering");
}
return SDL_TRUE;
}
接着 连接server端 ,返回连接的socket
socket_t device_socket = server_connect_to(&server);
//验证socket是否合法
if (device_socket == INVALID_SOCKET) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
//客户端连接服务端
socket_t server_connect_to(struct server *server) {
//如果是反向连接,也即是将远端手机端的端口隐射到pc上
if (!server->tunnel_forward) {
//等待服务端的连接,如果成功,返回一个socket
server->device_socket = net_accept(server->server_socket);
} else {
//如果是正向连接,将pc端的端口隐射到远端的手机端
Uint32 attempts = 100;
Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay);
}
//判断连接socket是否合法
if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
//由于服务端已经连接上我们监听的端口,并且创建了一个新的socket 所以这个监听的socket就没有用了,关闭
if (!server->tunnel_forward) {
// we don't need the server socket anymore
close_socket(&server->server_socket);
}
// the server is started, we can clean up the jar from the temporary folder 由于这个服务端的代码jar已经运行了,此时移除推送的jar
remove_server(server->serial); // ignore failure
//标识服务端的jar已经移除
server->server_copied_to_device = SDL_FALSE;
// we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure
//移除之后,将端口映射改为false
server->tunnel_enabled = SDL_FALSE;
//返回这个连接的socket
return server->device_socket;
}
这样客户端就跟服务端连接上了,服务端也不会继续阻塞了,此时回到服务端代码
//tunnelForward 为服务端是否需要监听端口,还是连接端口
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
//内部会调用accept函数,会阻塞直到有客户端连接上。才会返回
socket = listenAndAccept(SOCKET_NAME);
// send one byte so the client may read() to detect a connection error
//所以如果到了这里,就说明客户端已经连接上了,服务端发送一个字节告知他们连接成功
socket.getOutputStream().write(0);
//到了这里就说明俩者已经连接 , 构建一个DesktopConnection 代表俩者的连接对象
DesktopConnection connection = new DesktopConnection(socket);
//服务端得到手机的视频大小
Size videoSize = device.getScreenInfo().getVideoSize();
//服务端告知客户端 当前设备的名称 以及 当前屏幕的大小
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
}
也即是客户端会先发送一个字节,用来确认是否已经连接,然后发送设备的名称,以及当前屏幕的大小,回到客户端的代码,前面已经分析了,此时客户端跟服务端已经连接上了
接着客户端解析服务端传递过来的内容
char device_name[DEVICE_NAME_FIELD_LENGTH];
struct size frame_size;
//当连接上的时候,服务端会告知客户端设备的名称,以及视频的大小
if (!device_read_info(device_socket, device_name, &frame_size)) {
//读取失败,停止服务
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
//读取设备的名称,以及视频的大小,
SDL_bool device_read_info(socket_t device_socket, char *device_name, struct size *size) {
unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4];
//socket接受内容
int r = net_recv_all(device_socket, buf, sizeof(buf));
if (r < DEVICE_NAME_FIELD_LENGTH + 4) {
LOGE("Could not retrieve device information");
return SDL_FALSE;
}
//填充device_name ,以及 size
buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; // in case the client sends garbage
// strcpy is safe here, since name contains at least DEVICE_NAME_FIELD_LENGTH bytes
// and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
strcpy(device_name, (char *) buf);
size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 1];
size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 3];
return SDL_TRUE;
}
接着客户端执行 根据客户端告知的视频的大小,初始化 frames 结构体,创建对应frame
if (!frames_init(&frames)) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
//初始化frames 的成员变量
SDL_bool frames_init(struct frames *frames) {
//初始化解码帧
if (!(frames->decoding_frame = av_frame_alloc())) {
goto error_0;
}
//初始化渲染帧
if (!(frames->rendering_frame = av_frame_alloc())) {
goto error_1;
}
//初始化锁
if (!(frames->mutex = SDL_CreateMutex())) {
goto error_2;
}
...
}
接着客户端 初始化解码的结构体
decoder_init(&decoder, &frames, device_socket, rec);
//初始化decoder ,注意这里的 video_socket 为客户端跟服务端通信的socket 对象 ,frames 为 执行 frames_init 初始化的对象
void decoder_init(struct decoder *decoder, struct frames *frames,
socket_t video_socket, struct recorder *recorder) {
decoder->frames = frames;
decoder->video_socket = video_socket;
decoder->recorder = recorder;
}
接着客户端开启这个视频编解码线程 创建解码的线程,用来事实的将客户端的界面渲染出来
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_recorder;
}
//开启解码的线程
SDL_bool decoder_start(struct decoder *decoder) {
LOGD("Starting decoder thread");
//sdl 创建一个线程,线程启动的时候会执行 run_decoder 方法,decoder为传递的参数
decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder);
if (!decoder->thread) {
LOGC("Could not start decoder thread");
return SDL_FALSE;
}
return SDL_TRUE;
}
所以关键的函数就在 run_decoder 函数指针里面 decoder 解码视频线程启动
static int run_decoder(void *data) {
struct decoder *decoder = data;
//找到 h264解码Code
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto run_end;
}
//创建解码对应的Context对象
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGC("Could not allocate decoder context");
goto run_end;
}
//打开h264 解码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Could not open H.264 codec");
goto run_finally_free_codec_ctx;
}
//初始化格式化的结构体成员
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto run_finally_close_codec;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto run_finally_free_format_ctx;
}
// initialize the receiver state 初始化 接受者的状态
decoder->receiver_state.frame_meta_queue = NULL;
decoder->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets 如果当前允许录制,服务端 要将 头部的内容要提前发送,后面再发送视频的内容
int (*read_packet)(void *, uint8_t *, int) = decoder->recorder ? read_packet_with_meta : read_raw_packet;
//read_packet 函数将赋值给read_packet,当调用avcodec_send_packet函数,将会从ReadInputData读取指定的的BUF_SIZE,也即是后面读取解析的内容,要从read_packet 中读取
//decoder为 函数运行的时候,函数的参数
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto run_finally_free_format_ctx;
}
//将avio_alloc_context产生的AVIOContext设置到AVFormatContext的pb变量。
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
goto run_finally_free_avio_ctx;
}
//如果有设置录制的化,打开录制
if (decoder->recorder && !recorder_open(decoder->recorder, codec)) {
LOGE("Could not open recorder");
goto run_finally_close_input;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet)) {
// the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0)
int ret;
//发送给解码器
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
goto run_quit;
}
//接受解码之后的内容
ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame);
if (!ret) {
// a frame was received 将解码之后的内容,推送给SDL 显示
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
av_packet_unref(&packet);
goto run_quit;
}
if (decoder->recorder) {
// we retrieve the PTS in order they were received, so they will
// be assigned to the correct frame
uint64_t pts = receiver_state_take_meta(&decoder->receiver_state);
packet.pts = pts;
packet.dts = pts;
// no need to rescale with av_packet_rescale_ts(), the timestamps
// are in microseconds both in input and output
if (!recorder_write(decoder->recorder, &packet)) {
LOGE("Could not write frame to output file");
av_packet_unref(&packet);
goto run_quit;
}
}
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
break;
}
}
LOGD("End of frames");
...
return 0;
}
这里面就跟FFmpeg函数使用有关了,这里不介绍怎么使用, 这里主要介绍下他是怎么样从服务端获取到视频数据,主要是使用到了 avio_alloc_context 函数 可以指定数据源从哪里获取,这里指定
int (*read_packet)(void *, uint8_t *, int) = decoder->recorder ? read_packet_with_meta : read_raw_packet; 如果不录制视频的化,默认为 read_raw_packet
//如果没有指明录制视频,那么服务端就会直接发送视频的数据
static int read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct decoder *decoder = opaque;
ssize_t r = net_recv(decoder->video_socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
}
最终调用 recv 函数完成数据的接收
ssize_t net_recv(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, 0);
}
现在看看服务端是怎么样发送视频数据的,服务端在确认了跟客户端连接之后,开启一个
//从客户端传递过来的参数中,初始化ScreenEncoder 对象
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
// synchronous 主线程主要用来压缩屏幕的视频界面发送给客户端显示
screenEncoder.streamScreen(device, connection.getFd());
//获取到界面的视频内容,发送给客户端
public void streamScreen(Device device, FileDescriptor fd) throws IOException
{
//根据制定的 bitRate, frameRate, iFrameInterval 创建MediaFormat对象
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
//设置屏幕的旋转监听
device.setRotationListener(this);
boolean alive;
try
{
do
{
//创建Codec
MediaCodec codec = createCodec();
IBinder display = createDisplay();
//获取到屏幕显示的范围
Rect contentRect = device.getScreenInfo().getContentRect();
//获取到视频显示的范围
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
//设置大小为视频的宽高
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
//Requests a Surface to use as the input to an encoder, in place of input buffers. This
//may only be called after {@link #configure} and before {@link #start}
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
codec.start();
try
{
//获取到屏幕的数据,并且执行发送
alive = encode(codec, fd);
}
finally
{
//释放操作
codec.stop();
destroyDisplay(display);
codec.release();
surface.release();
}
}
while (alive);//无限循环
}
finally
{
device.setRotationListener(null);
}
}
//创建对应格式的MediaCodec
private static MediaCodec createCodec() throws IOException
{
return MediaCodec.createEncoderByType("video/avc");
}
//根据bitRate frameRate iFrameInterval 创建对应的MediaFormat 对象
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException
{
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "video/avc");
//设置比特率
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
//设置帧率
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
//设置帧率的延时时间
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
return format;
}
//编码视频数据
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException
{
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//如果屏幕的方向发生了变化,并且没有收到流的结尾,就一直循环获取
while (!consumeRotationChange() && !eof)
{
//将输出缓冲区出列,最多阻塞“timeoutUs”微秒。返回已成功输出缓冲区的索引 解码或其中一个INFO_ *常量
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
//判断是否到了结尾
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try
{
//如果屏幕的方向发生了变化,必须重新的启动压缩编码
if (consumeRotationChange())
{
// must restart encoding with new size
break;
}
//已成功输出缓冲区的索引
if (outputBufferId >= 0)
{
//获取到outputBufferId 中对应的buff
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
//是否需要发送元数据,如果是要录制视频的时候需要发送
if (sendFrameMeta)
{
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
//发送视频数据
IO.writeFully(fd, codecBuffer);
}
}
finally
{
if (outputBufferId >= 0)
{
codec.releaseOutputBuffer(outputBufferId, false);
}
}
}
return !eof;
}
//发送视频数据 ,这里的 fd 为 socket的描述符,这样就将数据发送给了客户端
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
int remaining = from.remaining();
while (remaining > 0) {
try {
int w = Os.write(fd, from);
if (BuildConfig.DEBUG && w < 0) {
// w should not be negative, since an exception is thrown on error
throw new AssertionError("Os.write() returned a negative value (" + w + ")");
}
remaining -= w;
} catch (ErrnoException e) {
if (e.errno != OsConstants.EINTR) {
throw new IOException(e);
}
}
}
}
回到客户端,服务端发送了视频数据之后,客户端相应的收到了数据,利用FFmpeg将视频解码成原始的数据,交给SDL显示,这个函数后面再来分析,先看看SDL的初始化
// a frame was received 将解码之后的内容,推送给SDL 显示
push_frame(decoder);
//客户端继续往下执行 初始化 controller 线程,这个线程的作用就是发送界面上的点击事件给服务端
if (!controller_start(&controller)) {
ret = SDL_FALSE;
goto finally_destroy_controller;
}
//启动一个control 线程,负责收集界面上的操作,发送给服务端
SDL_bool controller_start(struct controller *controller) {
LOGD("Starting controller thread");
//创建一个线程,线程启动的时候,会执行run_controller 函数,controller为函数执行的参数
controller->thread = SDL_CreateThread(run_controller, "controller", controller);
if (!controller->thread) {
LOGC("Could not start controller thread");
return SDL_FALSE;
}
return SDL_TRUE;
}
//control 线程,负责收集界面上的操作,同步给服务端
static int run_controller(void *data) {
struct controller *controller = data;
//保证线程不会退出,一直做收集操作
for (;;) {
//加锁
mutex_lock(controller->mutex);
//如果当前controller线程没有停止,并且queue队列没有内容,那么该线程处于等待
while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
cond_wait(controller->event_cond, controller->mutex);
}
//如果处于停止状态,解锁,退出这个循环,线程执行结束
if (controller->stopped) {
// stop immediately, do not process further events
mutex_unlock(controller->mutex);
break;
}
//那如果到了这里就说明,不处于停止状态,并且queue中有内容,那么获取队列里的内容
struct control_event event;
//获取queue中的下一个内热
SDL_bool non_empty = control_event_queue_take(&controller->queue, &event);
//确保不为空,解锁
SDL_assert(non_empty);
mutex_unlock(controller->mutex);
//处理这个事件,发送给服务端
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
break;
}
}
return 0;
}
从这个线程的run方法可知,他主要是监控 controller中的 queue 队列是否有内容,如果没有内容,使用 cond_wait 阻塞,如果有内容就获取到队列中的内容,最后使用process_event()函数将操作发送给服务端
这个函数先不分析,先假设当前没有事件,那么就会阻塞,客户端继续往下执行
//初始化SDL 显示窗口 device_name 为 客户端告知的设备号, fame_size 为当前设备
if (!screen_init_rendering(&screen, device_name, frame_size)) {
ret = SDL_FALSE;
goto finally_stop_and_join_controller;
}
//初始化显示的窗口,
SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, struct size frame_size) {
screen->frame_size = frame_size;
//根据服务端传递的界面大小,跟当前的pc界面的大小,算出一个合适的大小
struct size window_size = get_initial_optimal_size(frame_size);
Uint32 window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
//创建这个window,同时保存在window这个成员上 ,窗口的标题为 device_name
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height, window_flags);
//窗口创建失败
if (!screen->window) {
LOGC("Could not create window: %s", SDL_GetError());
return SDL_FALSE;
}
//创建纹理渲染器 SDL_Renderer对象
screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
}
if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, frame_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
}
//读取android 小图标
SDL_Surface *icon = read_xpm(icon_xpm);
if (!icon) {
LOGE("Could not load icon: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
}
//设置界面图标
SDL_SetWindowIcon(screen->window, icon);
SDL_FreeSurface(icon);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height);
//创建纹理对象 SDL_Texture
screen->texture = create_texture(screen->renderer, frame_size);
//创建纹理失败
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
}
return SDL_TRUE;
}
这样SDL默认的界面就显示出来了,最终客户端调用 ret = event_loop(); 来轮训的监听界面上的操作
//SDL 事件循环
static SDL_bool event_loop(void) {
SDL_Event event;
//在处理SDL的事件有两种模式,一种是等待 SDL_WaitEvent,另一种是轮询SDL_PollEvent 目前,在网上可以查到的文章,大多数都使用了轮询,还有人指出等待有时会导致事件处理延迟。
//但是我在实际coding中,使用SDL_PollEvent,一运行就风扇不停的转,用top看了下,cpu占用到了99%。
//由于SDL_WaitEvent语句,在用户未发生任何操作前,程序处于等待状态,不会进入下一次循环,这样就避免了cpu占用的问题。 也即是用户有操作的时候才会触发
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case EVENT_DECODER_STOPPED://视频渲染停止
LOGD("Video decoder stopped");
return SDL_FALSE;
case SDL_QUIT://SDL 退出
LOGD("User requested to quit");
return SDL_TRUE;
case EVENT_NEW_FRAME://当客户端收到了服务端发送的视频数据,并且解码之后,会发送这个事件,通知SDL可以渲染界面了
//如果当前屏幕还没有界面,也即是第一帧,显示当前的window
if (!screen.has_frame) {
screen.has_frame = SDL_TRUE;
// this is the very first frame, show the window
screen_show_window(&screen);
}
//更新界面的内容
if (!screen_update_frame(&screen, &frames)) {
return SDL_FALSE;
}
break;
case SDL_WINDOWEVENT://Windwow事件,比如界面大小发生变化
switch (event.window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
break;
case SDL_TEXTINPUT://监听到了键盘的输入事件
input_manager_process_text_input(&input_manager, &event.text);
break;
...
}
}
return SDL_FALSE;
}
前面分析过服务端发送视频数据,客户端收到之后,通过FFmpeg解码成原始的视频数据之后,会调用 push_frame(decoder); 推送给SDL,让SDL完成显示,现在就可以分析这个函数的实现了
//将视频界面之后的原始数据通知SDL,显示界面
static void push_frame(struct decoder *decoder) {
SDL_bool previous_frame_consumed = frames_offer_decoded_frame(decoder->frames);
if (!previous_frame_consumed) {
// the previous EVENT_NEW_FRAME will consume this frame
return;
}
//构建一个EVENT_NEW_FRAME 事件,利用SDL发送这个事件
static SDL_Event new_frame_event = {
.type = EVENT_NEW_FRAME,
};
SDL_PushEvent(&new_frame_event);
}
可以看到这里是通过构建一个EVENT_NEW_FRAME 事件,使用SDL_PushEvent 来通知SDL,那么SDL就能接收到这个事件,完成对应的渲染
case EVENT_NEW_FRAME://当客户端收到了服务端发送的视频数据,并且解码之后,会发送这个事件,通知SDL可以渲染界面了
//如果当前屏幕还没有界面,也即是第一帧,显示当前的window
if (!screen.has_frame) {
screen.has_frame = SDL_TRUE;
// this is the very first frame, show the window
screen_show_window(&screen);
}
//更新界面的内容
if (!screen_update_frame(&screen, &frames)) {
return SDL_FALSE;
}
break;
//渲染屏幕 根据解码之后的frame
SDL_bool screen_update_frame(struct screen *screen, struct frames *frames) {
mutex_lock(frames->mutex);
const AVFrame *frame = frames_consume_rendered_frame(frames);
//新的大小
struct size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) {
mutex_unlock(frames->mutex);
return SDL_FALSE;
}
//填充数据
update_texture(screen, frame);
mutex_unlock(frames->mutex);
//更新纹理,显示界面
screen_render(screen);
return SDL_TRUE;
}
//更新纹理,显示界面
void screen_render(struct screen *screen) {
SDL_RenderClear(screen->renderer);
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
SDL_RenderPresent(screen->renderer);
}
这样服务端发送的视频数据,客户端就能接受到了,并且通过SDL显示出来, 下面分析下客户端界面的点击操作,这里先分析下服务端开启controller线程监控事件的接收
// asynchronous 服务端开启一个控制同步线程,用于同步客户端发送的按键内容
startEventController(device, connection);
//服务端开启一个控制同步线程,用于同步客户端发送的按键内容
private static void startEventController(final Device device, final DesktopConnection connection) {
new Thread(new Runnable() {
@Override
public void run() {
try {
new EventController(device, connection).control();
} catch (IOException e) {
// this is expected on close
Ln.d("Event controller stopped");
}
}
}).start();
}
public EventController(Device device, DesktopConnection connection) {
this.device = device;
//俩者连接的对象
this.connection = connection;
initPointer();
}
//开始同步
public void control() throws IOException {
// on start, turn screen on 开始的时候,打开屏幕
turnScreenOn();
//无限循环,同步客户端发送的按键操作
while (true) {
handleEvent();
}
}
//打开屏幕
private boolean turnScreenOn() {
//injectKeycode(KeyEvent.KEYCODE_POWER); 也即是模仿 按下电源键
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
}
//由于要模拟某个按键 keyCode,所以对应的就要有 按下以及抬起的动作
private boolean injectKeycode(int keyCode) {
//模拟keyCode 对应的按下 , 模拟 keyCode 对应的抬起
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
//根据传递过来的内容,构建一个KeyEvent ,deviceId 为VIRTUAL_KEYBOARD ,source 为 SOURCE_KEYBOARD
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD);
//发射调用执行这个按键
return injectEvent(event);
}
//发射调用按键
private boolean injectEvent(InputEvent event) {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectInputEvent(InputEvent inputEvent, int mode)
{
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
}
public InputManager(IInterface manager) {
this.manager = manager;
try {
// 也即是获取到 InputManager 中的 injectInputEvent public boolean injectInputEvent(InputEvent event, int mode)
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
}
//反射调用这个按键
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
try {
return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
现在看下客户端的收集按键的操作,对应的实现就是通过SDL 的event_looper实现的,通过SDL可以捕获到界面上的操作,比如
case SDL_TEXTINPUT://监听到了键盘的输入事件
input_manager_process_text_input(&input_manager, &event.text);
break;
//监听到了键盘的输入事件,
void input_manager_process_text_input(struct input_manager *input_manager,const SDL_TextInputEvent *event) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
SDL_assert(event->text[1] == '\0');
// letters and space are handled as raw key event
return;
}
//创建一个control_event 对象,填充对象的内容
struct control_event control_event;
//类型
control_event.type = CONTROL_EVENT_TYPE_TEXT;
//文本
control_event.text_event.text = SDL_strdup(event->text);
if (!control_event.text_event.text) {
LOGW("Cannot strdup input text");
return;
}
//将当前的control_event 对象,添加到 input_manager 中的 controller 成员的队列
if (!controller_push_event(input_manager->controller, &control_event)) {
SDL_free(control_event.text_event.text);
LOGW("Cannot send text event");
}
}
//将当前的control_event 事件, 添加到 controller 的queue队列中
SDL_bool controller_push_event(struct controller *controller, const struct control_event *event) {
SDL_bool res;
//加锁
mutex_lock(controller->mutex);
//判断是否为空
SDL_bool was_empty = control_event_queue_is_empty(&controller->queue);
//添加到队列中
res = control_event_queue_push(&controller->queue, event);
//如果当前为空,那么另一边control线程就会处于阻塞的状态,所以这边要发送一个信号,control线程才不会阻塞
if (was_empty) {
cond_signal(controller->event_cond);
}
//解锁
mutex_unlock(controller->mutex);
return res;
}
通过SDL捕获到事件之后,这里通过构建一个control_event 事件,然后添加到 control_event 中的queue队列中, 不知道前面还有没有印象,客户端启动了一个controll线程用来收集用户的行为操作,监控的
就是这个control_event中的queue 队列,也即是下面的代码实现
//control 线程,负责收集界面上的操作,同步给服务端
static int run_controller(void *data) {
struct controller *controller = data;
//保证线程不会退出,一直做收集操作
for (;;) {
//加锁
mutex_lock(controller->mutex);
//如果当前controller线程没有停止,并且queue队列没有内容,那么该线程处于等待
while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
cond_wait(controller->event_cond, controller->mutex);
}
//如果处于停止状态,解锁,退出这个循环,线程执行结束
if (controller->stopped) {
// stop immediately, do not process further events
mutex_unlock(controller->mutex);
break;
}
//那如果到了这里就说明,不处于停止状态,并且queue中有内容,那么获取队列里的内容
struct control_event event;
//获取queue中的下一个内热
SDL_bool non_empty = control_event_queue_take(&controller->queue, &event);
//确保不为空,解锁
SDL_assert(non_empty);
mutex_unlock(controller->mutex);
//处理这个事件,发送给服务端
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
break;
}
}
return 0;
}
由于队列中含有元素,这样就不会进行阻塞,进而执行process_event()函数
//处理control 事件,发送给服务端
static SDL_bool process_event(struct controller *controller, const struct control_event *event) {
//#define TEXT_MAX_LENGTH 300 #define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH 也即是长度为303
unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE];
//将对应的事件,填充内容到serialized_event 中
int length = control_event_serialize(event, serialized_event);
if (!length) {
return SDL_FALSE;
}
//发送事件
int w = net_send_all(controller->video_socket, serialized_event, length);
return w == length;
}
这样客户端就完成了收集界面上的操作,然后相应的发送给了服务端,服务端就可以接收到,通过判断事件的种类,然后客户端相应的通过反射来模拟用户的点击操作,从而达到操作同步
private void handleEvent() throws IOException {
ControlEvent controlEvent = connection.receiveControlEvent();
switch (controlEvent.getType()) {
case ControlEvent.TYPE_KEYCODE:
injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState());
break;
case ControlEvent.TYPE_TEXT:
injectText(controlEvent.getText());
break;
case ControlEvent.TYPE_MOUSE:
injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition());
break;
case ControlEvent.TYPE_SCROLL:
injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll());
break;
case ControlEvent.TYPE_COMMAND:
executeCommand(controlEvent.getAction());
break;
default:
// do nothing
}
}
总结
客户端通过adb命令将服务端的jar推送到手机,执行通过执行 adb reverse, adb forward 的结果来决定应该由哪端来监听tcp的端口,通过adb shell 启动服务端jar,并且发送对应的参数,比如是否应该服务端开启端口的监听,这样服务端的main函数启动起来, 接着服务端等待客户端的连接,连接通过之后发送设备的信息,比如手机的型号,视频的大小等,客户端接收到之后完成对应的SDL窗口的设置,紧接着客户端会创建一个video encoder 线程,专门用来接受服务端发送的视频数据,服务端会在主线程通过MediaCodec 完成视频的编码,客户端收到之后,通过FFmpeg 解码成原始的数据,通过SDL 将界面渲染出来同时客户端还会启动一个Controller 线程,负责收集界面上的操作,通过SDL 捕获到界面上的操作,然后 发送给服务端,服务端也会开启一个线程专门用于同步客户端发送的界面操作,然后通过反射的方式,模拟用户的操作