【投屏】Scrcpy源码分析三(Client篇-投屏阶段)

Scrcpy源码分析系列
【投屏】Scrcpy源码分析一(编译篇)
【投屏】Scrcpy源码分析二(Client篇-连接阶段)
【投屏】Scrcpy源码分析三(Client篇-投屏阶段)
【投屏】Scrcpy源码分析四(最终章 - Server篇)

前一篇我们探究了Scrcpy Client端连接阶段的逻辑,这一篇我们继续探究Client端的投屏阶段。

1. 音视频和FFmpeg

因为投屏阶段用到了很多音视频编解码知识和FFmpeg相关的API,所以在继续分析代码之前,我们先简单快速地回顾一下这些内容。因FFmpeg功能很广,我们只介绍Scrcpy中用到的一部分。

1.1 音视频基础

1.1.1 编码/解码

编码(Encode)- 将一种音视频格式文件(通常是原始、未经压缩的)通过压缩技术转换成另一种格式文件。
解码(Decode)- 将压缩后的音视频格式文件还原成原始的音视频格式文件。

通常我们所说的编解码器(Codec),就是同时包含了编码和解码的能力。

编码的意义在于,未经压缩的原始类型,数据流是非常大的,不利于存储和网络传输,所以需要对其进行编码。常见的视频原始类型有YUVRAW等,音频原始类型有PCM。常见的视频编码类型有H264H265等,音频编码类型有AACMP3

1.1.2 容器

容器通常指包含了多路流的封装格式。比如一个容器内可以包含音频流、视频流、字幕流等,而对应音频流和视频流的数据格式就是音视频的编码类型。

混流/复用(mux)- 将多个流混合到一个容器中。
分流/解复用(demux)- 从一个容器中分解成多个流。

常见的容器有MP4FLVMKVAVI

1.1.3 音视频播放流程

音视频播放的流程通常是:

编码
混流
分流
解码
采集
YUV
H264
传输
H264
YUV
播放

如果只需要音频或视频则,则混流/分流的过程可以省略。

上一篇有提到Scrcpy的原理的Android设备侧不断录屏、编码,将视频流传输给PC,PC进行解码和渲染,就是类似上述的过程。

Android设备的编码用的是MediaCodec硬编码,这个暂且不用太关注,我们只需要只要Android是将YUV原始数据,编码生成H264,通过video_socket传给PC。PC侧收到视频流后,通过FFmpeg进行解码并通过SDL渲染出来。

1.2 FFmpeg

FFmpeg是一套音视频开源软件,提供强大的音视频处理能力,应用广泛。其中最基础就是编解码的能力。

Scrcpy主要用到FFmpeg的解码能力,并且此文我们的重点还是Scrcpy,所以只是简单描述一下FFmpeg的解码需要用到的API,方便后续分析。

FFmpeg解码的关键流程如下(代码不完整,只需关注关键API):

int ffmpeg_decode() {
    
    
	// 注册所有编解码器
	avcodec_register_all();
	
	// 创建解码器,传入对应的解码器ID,比如这里是H264解码器
	AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
	// 分配AVCodecContext空间并初始化
	AVCodecContext *codecContext = avcodec_alloc_context3(codec);
	// 通过AVCodec对AVCodecContext进行初始化
	avcodec_open2(codecContext, codec, NULL);
	// 初始化AVCodecParserContext
	AVCodecParserContext *parserContext = av_parse_init(AV_CODEC_ID_H264);
	
	// 分配AVPacket空间
	AVPacket *avPacket = av_packet_alloc();
	// 分配AVFramen空间
	AVFrame *frame = av_frame_alloc();
	
	while(!eof(input)) {
    
    
		// 解析一个packet
		av_parser_parse2(parserContext, codecContext,  &pkt->data, &pkt->size, data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
		// 解码
		decode();
	}

	// 资源释放
	avcodec_free_context(&codecContext);
	av_parse_close(parseContext);
	av_frame_free(&frame);
	av_packet_free(&avPacket);
}

int decode(AVCodecContext *codec_ctx, AVPacket *pkt, AVFrame *frame) {
    
    
	// 将packet送入解码器
	int ret = avcodec_send_packet(codec_ctx, pkt);

	while(ret >= 0) {
    
    
		// 从解码器中拿到解码后的帧数据
		ret = avcodec_receive_frame(codec_ctx, frame);
		// [TODO] 已经拿到帧数据frame->data
	}
}

上面是使用FFmpeg对H264进行视频解码的模板代码,主要有几个阶段:

  1. 初始化相关,此阶段需要创建AVCodecAVCodecContextAVCodecParserContext变量,并进行相关初始化。
  2. AVPacketAVFrame结构分配空间。AVPacket是指经过编码之后的一个数据包,AVFrame是解码后的一帧数据,视频中一帧代表一帧图片数据。
  3. 解码阶段,此阶段需要从输入源(文件或网络)解析一个packet,然后送入解码器解码,到到frame帧数据。
  4. 数据处理阶段,在拿到帧数据AVFrame->data后,可以根据业务需要对数据进行处理。

Scrcpy中使用FFmpeg进行解码流程也大致如上。

2. 投屏阶段

上回说到scrcpy()里的await_for_server()函数,这个函数内部在等待SDL事件,在收到连接成功的事件之后,则跳出等待,继续执行后续的逻辑。

// scrcpy.c
enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) {
    
    
	// 连接阶段...
	await_for_server();
	
	// 【投屏阶段】
	// 初始化文件上传相关数据结构
	sc_file_pusher_init(&s->file_pusher, serial, options->push_target)
	// 初始化解码相关数据结构
	sc_decoder_init(&s->decoder);
	// 初始化录制相关数据结构
	sc_recorder_init(&s->recorder,
                              options->record_filename,
                              options->record_format,
                              info->frame_size);
	// 初始化分流相关数据结构
	sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
	// 将解码器加到分流器的一路流中
	sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
	// 将录制器加到分流器的一路流中
	sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
	// 初始化键盘拦截相关数据结构
	sc_keyboard_inject_init(&s->keyboard_inject, &s->controller,
                                    options->key_inject_mode,
                                    options->forward_key_repeat);
	// 初始化鼠标拦截相关数据结构
	sc_mouse_inject_init(&s->mouse_inject, &s->controller);
	// 初始化控制socket
	sc_controller_init(&s->controller, s->server.control_socket,
                                acksync);
	// 开启两个控制相关的新线程,一个发,一个收
	sc_controller_start(&s->controller);
	// 初始化屏幕渲染相关数据结构
	sc_screen_init(&s->screen, &screen_params);
	// 将屏幕加到解码器的一路流中
	sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
	// 将v4l2加到解码器的一路流中
	sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
	// 开启新线程执行分流和解码
	sc_demuxer_start(&s->demuxer);
	// SDL事件循环等待事件
	event_loop(s);
	
	// 关闭窗口
	sc_screen_hide_window(&s->screen);
	// 关闭和释放服务相关资源
	sc_server_destroy(&s->server);
}

投屏阶段我们需要关注几个部分:

  1. sc_file_pusher_init - 初始化文件上传相关的数据结构。文件上传是指将文件从PC拖入镜像窗口中自动同步至/sdcard/Download目录中。
  2. sc_decoder_init & sc_recorder_init - 解码器和录制相关数据结构的初始化。主要设置struct sc_packet_sink_ops的回调函数,在openclosepush三个时机触发相应的动作。(注意:这里的回调是针对Packet的,如前面提到Packet指的是经过压缩编码后的一个数据包)。
  3. sc_demuxer_init - 对分流相关的数据结构进行初始化。
  4. sc_demuxer_add_sink - 将解码器和录制器加到分流中,Scrcpy的分流(Demuxer)和前面提到的容器分流不太一样。容器的分流是分离出多个流,而Scrcpy中的分流指的是把同一份数据送给不同的地方去处理。比如这里会送到解码器进行解码,如果在程序启动时指定了需要进行录制,那么也会送一份数据到录制器中进行数据保存。
  5. sc_keyboard_inject_init & sc_mouse_inject_init - 初始化键盘和鼠标拦截的数据结构。
  6. sc_controller_init - 对control_socket链路进行初始化。
  7. sc_controller_start - 开启两个控制相关的新线程,一个发,一个收。
  8. sc_screen_init - 对窗口进行初始化,并用SDL创建窗口。设置struct sc_frame_sink_ops的回调函数, 在openclosepush三个时机触发相应的动作。(注意:和前面不同,这里的回调是针对frame的,即packet解码后帧数据)。
  9. sc_decoder_add_sink - 将窗口和V4L2加到解码器的一路流中,同分流器一样,解码器解码后的帧数据也会送到窗口上和V4L2设备中(V4L2设备需在启动程序是指定,如不指定,则此处就不会触发V4L2逻辑)。
  10. sc_demuxer_start - 开启新线程执行分流和解码。
  11. event_loop - 事件循环,监听SDL事件。
  12. sc_server_destroy - 关闭和释放服务相关资源。因为上一步是死循环,只有在触发退出事件才会退出循环,走到这里的释放逻辑。

其中需要重点关注的 5781011,我们按照重要性顺序着重来看 - 8101157

2.1 sc_screen_init - 对窗口进行初始化

我们列出sc_screen_init函数的关键代码:

// screen.c
bool
sc_screen_init(struct sc_screen *screen,
               const struct sc_screen_params *params) {
    
    
	// 设置on_new_frame回调
	static const struct sc_video_buffer_callbacks cbs = {
    
    
        .on_new_frame = sc_video_buffer_on_new_frame,
    };
	// 对video buffer进行初始化
	sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, screen);
	// 开启新线程执行帧数据处理
	sc_video_buffer_start(&screen->vb);
	// 创建SDL窗口
	SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags);
	// 创建渲染器
	SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED);

	// 设置解码后frame数据回调
	static const struct sc_frame_sink_ops ops = {
    
    
	        .open = sc_screen_frame_sink_open,
	        .close = sc_screen_frame_sink_close,
	        .push = sc_screen_frame_sink_push,
	    };
	    
    screen->frame_sink.ops = &ops;
}

我们看到sc_screen_init的主要作用有四个:

  1. 设置on_new_frame,并将回调传入screen(也就是桌面窗口)的video_buffer的初始化方法中,可以简单理解为将这个回调和客户端做一个绑定,后面会用到。

  2. 开启新线程执行帧处理,这里的功能最终是从一个帧队列里取帧数据,然后送给on_new_frame函数。

    // video_buffer.c
    bool
    sc_video_buffer_start(struct sc_video_buffer *vb) {
          
          
    	// 开启新线程执行run_buffering函数
    	sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb);
    }
    
    static int
    run_buffering(void *data) {
          
          
    	for (;;) {
          
          
    		// 从&vb->b.queue队列中取帧
    		sc_queue_take(&vb->b.queue, next, &vb_frame);
    		// 调用此函数,将帧传入
    		sc_video_buffer_offer(vb, vb_frame->frame);
    	}
    }
    
    static bool
    sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) {
          
          
    	// 帧数据处理后,将数据通过on_new_frame回调传出
        vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata);
    }
    
  3. 通过SDL创建窗口和渲染器。

  4. 设置解码后frame数据的回调,正如前面提到,packet会送给解码器和录制器两路packet流,解码器里又可以分屏幕窗口和V4L2设备两路frame流。这里的回调就是这是解码器将数据解码后给到屏幕窗口的回调。

// screen.c
static const struct sc_frame_sink_ops ops = {
    
    
	        .open = sc_screen_frame_sink_open,
	        .close = sc_screen_frame_sink_close,
	        .push = sc_screen_frame_sink_push,
	    };

static bool
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
    
    
    return sc_video_buffer_push(&screen->vb, frame);
}

// video_buffer.c
bool
sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) {
    
    
	// 往&vb->b.queue队列中插帧数据
    sc_queue_push(&vb->b.queue, next, vb_frame);
}

分析完sc_screen_init函数后,我们知道这部分的流程基本如下图所示,那么现在遗留的问题就是外部怎么发起解码器的push回调,以及on_new_frame里到底做了什么。这里先埋个坑,我们后面填充。
在这里插入图片描述

2.2 sc_demuxer_start - 分流和解码

我们列出sc_demuxer_start函数的关键代码:

// demuxer.c
bool
sc_demuxer_start(struct sc_demuxer *demuxer) {
    
    
	sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", demuxer);
}

static int
run_demuxer(void *data) {
    
    
	// FFmpeg API: 初始化AVCodec和AVCodecContext
	AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
	demuxer->codec_ctx = avcodec_alloc_context3(codec);
	
	// open sinks,回调到struct sc_packet_sink_ops的.open回调
	sc_demuxer_open_sinks(demuxer, codec);
	
	// FFmpeg API: 初始化AVCodecParserContext和AVPacket
	demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
	AVPacket *packet = av_packet_alloc();

	// 不断地读packet,并将packet窗到sink中
	for(;;) {
    
    
		sc_demuxer_recv_packet(demuxer, packet);
		sc_demuxer_push_packet(demuxer, packet);
	}

	// FFmpeg API: 释放
	av_packet_free(&packet);
	av_parser_close(demuxer->parser);
	avcodec_free_context(&demuxer->codec_ctx);
}

我们看到,sc_demuxer_start主要就是在子线程中执行FFmpeg相关的数据结构初始化,然后在死循环中不断地读packet数据和push,具体是怎么做了,我们来看下sc_demuxer_recv_packetsc_demuxer_push_packet函数:

// demuxer.c
// sc_demuxer_recv_packet的作用就是接收packet
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
    
    
	// 通过video_socket从网络读packet header
	net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);
	// 通过video_socket从网络读packet数据
	net_recv_all(demuxer->socket, packet->data, len);
}

// sc_demuxer_push_packet的作用就是调用struct sc_packet_sink_ops的.push回调
static bool
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
    
    
	push_packet_to_sinks(demuxer, packet);
}

static bool
push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
    
    
  for (unsigned i = 0; i < demuxer->sink_count; ++i) {
    
    
        struct sc_packet_sink *sink = demuxer->sinks[i];
        if (!sink->ops->push(sink, packet)) {
    
    
            return false;
        }
    }
	return true;
}

注意,这里是才从网络获取到packet,还没有解码成frame,所以调用是struct sc_packet_sink_ops.push回调,并不是2.1节的解码后的push回调。这里packet的回调是在前文提到的sc_decoder_init函数中注册的:

void
sc_decoder_init(struct sc_decoder *decoder) {
    
    
    decoder->sink_count = 0;

    static const struct sc_packet_sink_ops ops = {
    
    
        .open = sc_decoder_packet_sink_open,
        .close = sc_decoder_packet_sink_close,
        .push = sc_decoder_packet_sink_push,
    };

    decoder->packet_sink.ops = &ops;
}

// packet的push回调方法
static bool
sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
                            const AVPacket *packet) {
    
    
    struct sc_decoder *decoder = DOWNCAST(sink);
    return sc_decoder_push(decoder, packet);
}

static bool
sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
    
    
	// FFmpeg API: 将packet送到packet送到解码器中
	avcodec_send_packet(decoder->codec_ctx, packet);
	// FFmpeg API: 从解码器中拿到解码后的帧数据
	avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
	// 将解码后的帧数据传给sinks
	push_frame_to_sinks(decoder, decoder->frame);
}

所以packet的push回调的主要功能就是通过解码器把packet解码成frame,这一点和前面说的FFmpeg解码流程是一致的。拿到frame之后,就该调用push_frame_to_sinks把frame发给了解码器的push回调了:

static bool
push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
    
    
    for (unsigned i = 0; i < decoder->sink_count; ++i) {
    
    
        struct sc_frame_sink *sink = decoder->sinks[i];
        if (!sink->ops->push(sink, frame)) {
    
    
            return false;
        }
    }

    return true;
}

对的,就是在这里触发了前面一节流程图的第一个问号,所以流程图可以填充一下:
在这里插入图片描述

2.3 event_loop - 事件循环

// scrcpy.c
static enum scrcpy_exit_code
event_loop(struct scrcpy *s) {
    
    
    SDL_Event event;
    while (SDL_WaitEvent(&event)) {
    
    
        switch (event.type) {
    
    
            case EVENT_STREAM_STOPPED:
                LOGW("Device disconnected");
                return SCRCPY_EXIT_DISCONNECTED;
            case SDL_QUIT:
                LOGD("User requested to quit");
                return SCRCPY_EXIT_SUCCESS;
            default:
                sc_screen_handle_event(&s->screen, &event);
                break;
        }
    }
    return SCRCPY_EXIT_FAILURE;
}

event_loop函数的结构比较清晰,就是一直在等待SDL事件,除了EVENT_STREAM_STOPPEDSDL_QUIT事件,其他的事件都是交给sc_screen_handle_event函数处理:

// screen.c
void
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
    
    
	switch (event->type) {
    
    
		// new frame事件
    	case EVENT_NEW_FRAME:
    		sc_screen_update_frame(screen);
    		return;
    	// SDL窗口事件,包括窗口最大化、恢复、窗口失去焦点等
        case SDL_WINDOWEVENT:
        	return;
        // 键盘事件
        case SDL_KEYDOWN:
        case SDL_KEYUP:
        // 鼠标事件
        case SDL_MOUSEWHEEL:
        case SDL_MOUSEMOTION:
        case SDL_MOUSEBUTTONDOWN:
        // 触摸事件
        case SDL_FINGERMOTION:
        case SDL_FINGERDOWN:
        case SDL_FINGERUP:
        case SDL_MOUSEBUTTONUP:
        	// 省略了部分代码
    }
    
    sc_input_manager_handle_event(&screen->im, event);
}

sc_screen_handle_event函数中,我们会处理EVENT_NEW_FRAME事件和其他鼠标和键盘事件。我们先着重关注EVENT_NEW_FRAME事件的。收到这个事件之后会执行sc_screen_update_frame函数,关键代码如下:

// screen.c
static bool
sc_screen_update_frame(struct sc_screen *screen) {
    
    
	// 更新数据
    update_texture(screen, frame);
	// 第一次执行则打开窗口
	if (!screen->has_frame) {
    
    
 		sc_screen_show_initial_window(screen);
 	}
	// 数据渲染
    sc_screen_render(screen, false);
}

static void
update_texture(struct sc_screen *screen, const AVFrame *frame) {
    
    
	// 将YUV数据写到SDL上下文中
    SDL_UpdateYUVTexture(screen->texture, NULL,
            frame->data[0], frame->linesize[0],
            frame->data[1], frame->linesize[1],
            frame->data[2], frame->linesize[2]);
}

static void
sc_screen_show_initial_window(struct sc_screen *screen) {
    
    
	// 展示窗口
	SDL_ShowWindow(screen->window);
}

static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
    
    
	// SDL模板代码,将上下文中的数据渲染到窗口上
	SDL_RenderClear(screen->renderer);
	SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
	SDL_RenderPresent(screen->renderer);
}

所以我们知道这个函数的作用就是打开窗口并把frame数据(即解码后的YUV)渲染到窗口中。源头就是EVENT_NEW_FRAME这个事件。那么这个事件是哪里发来的呢。就是前面的on_new_frame 回调,对应的是sc_video_buffer_on_new_frame函数:

// screen.c
static void
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
                             void *userdata) {
    
    
	// 这里将EVENT_NEW_FRAME通过SDL的事件机制发出
	static SDL_Event new_frame_event = {
    
    
	          .type = EVENT_NEW_FRAME,
	      };
	SDL_PushEvent(&new_frame_event);
}

看到这里,我们的流程图就可以填充完整了。
在这里插入图片描述

到目前为止,视频流这一块基本上已经分析完毕,这部分的数据走的是video_socket。上篇提到,还有一个control_socket,主要用于控制事件传输,比如鼠标键盘控制,也就是投屏中的反控功能,这也是投屏业务非常重要的一个环节,下面我们来看这部分。

2.4 sc_keyboard_inject_init & sc_mouse_inject_init - 键鼠事件

因为键盘和鼠标整体的逻辑差不多,所以这里我们追下键盘的流程,鼠标就不赘述。sc_keyboard_inject_init的主要功能就是注册键盘回调:

// mouse_inject.c
void
sc_keyboard_inject_init(struct sc_keyboard_inject *ki,
                        struct sc_controller *controller,
                        enum sc_key_inject_mode key_inject_mode,
                        bool forward_key_repeat) {
    
    
	 static const struct sc_key_processor_ops ops = {
    
    
        .process_key = sc_key_processor_process_key,
        .process_text = sc_key_processor_process_text,
    };

	ki->key_processor.ops = &ops;
}

static void
sc_key_processor_process_key(struct sc_key_processor *kp,
                             const struct sc_key_event *event,
                             uint64_t ack_to_wait) {
    
    
	sc_controller_push_msg(ki->controller, &msg)
}

bool
sc_controller_push_msg(struct sc_controller *controller,
                       const struct sc_control_msg *msg) {
    
    
    // 键盘事件入队列
	cbuf_push(&controller->queue, *msg);
}

可以看到键盘事件最终会放到队列中。那么键盘事件是哪里来的呢?就是前一节的event_loop。SDL会自动检测窗口收到的键盘和鼠标事件,只需要在event_loop中监听对应事件即可,最终会触发事件回调:

// input_manager.c
void
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {
    
    
	switch (event->type) {
    
    
		// ...
		case SDL_KEYDOWN:
        case SDL_KEYUP:
            sc_input_manager_process_key(im, &event->key);
            break;
        // ...
	}
}

static void
sc_input_manager_process_key(struct sc_input_manager *im,
                             const SDL_KeyboardEvent *event) {
    
    
    // 调用process_key回调
	im->kp->ops->process_key(im->kp, &evt, ack_to_wait);
}

所以目前为止,键盘鼠标事件的流程是:
在这里插入图片描述

2.5 sc_controller_start - 事件的收发

这里说的事件手法主要是和手机侧的事件交互,我们来看下是怎么做的:

bool
sc_controller_start(struct sc_controller *controller) {
    
    
	sc_thread_create(&controller->thread, run_controller,
                               "scrcpy-ctl", controller);

	receiver_start(&controller->receiver);
}

bool
receiver_start(struct receiver *receiver) {
    
    
	sc_thread_create(&receiver->thread, run_receiver,
                               "scrcpy-receiver", receiver);
}

sc_controller_start函数会开两个线程,一个负责收,一个负责发:

  • 收线程 - 主要从手机侧收粘贴板事件,手机侧触发的复制操作,会将数据传至PC侧,PC会放到粘贴板中。这里不细说,感兴趣的同学可以自行追下源码。

  • 发线程 - 将PC侧的事件发给手机。这是我们关注的重点,我们看下run_controller函数的核心逻辑:

// controller.c
static int
run_controller(void *data) {
    
    
	for(;;) {
    
    
		// 从队列里取事件
		cbuf_take(&controller->queue, &msg);
		// 处理事件
		process_msg(controller, &msg);
	}
}

static bool
process_msg(struct sc_controller *controller,
            const struct sc_control_msg *msg) {
    
    
    // 通过control_socket将事件发出去
	net_send_all(controller->control_socket, serialized_msg, length);
}

发线程的主要逻辑就是一个死循环,不断地从队列中取事件,然后通过control_socket发出去。

所以键鼠事件的流程可以完善一下了:
在这里插入图片描述

2.6 时序图

老规矩,抛出一张投屏阶段的时序图。不同的颜色代表不同的线程。

在这里插入图片描述

3. 小结

这一篇我们探究了Scrcpy Client端投屏阶段的逻辑。涉及的点有FFmpeg解码、SDL的窗口绘制和键盘鼠标反控。

至此Client端的逻辑已经介绍完了,分为连接阶段和投屏阶段。下一篇我们就要探究Server端,也就是手机侧的功能逻辑了,下篇见。

猜你喜欢

转载自blog.csdn.net/ZivXu/article/details/128932688
今日推荐