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

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

Client篇-投屏阶段

  • 1. 音视频和FFmpeg
    • 1.1 音视频基础
      • 1.1.1 编码/解码
      • 1.1.2 容器
      • 1.1.3 音视频播放流程
    • 1.2 FFmpeg
  • 2. 投屏阶段
    • 2.1 ``sc_screen_init`` - 对窗口进行初始化
    • 2.2 `sc_demuxer_start` - 分流和解码
    • 2.3 ``event_loop`` - 事件循环
    • 2.4 ``sc_keyboard_inject_init`` & ``sc_mouse_inject_init`` - 键鼠事件
    • 2.5 ``sc_controller_start`` - 事件的收发
    • 2.6 时序图
  • 3. 小结

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 音视频播放流程

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

#mermaid-svg-9qOmJhMP5f3A1u9i {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i .error-icon{fill:#552222;}#mermaid-svg-9qOmJhMP5f3A1u9i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9qOmJhMP5f3A1u9i .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-9qOmJhMP5f3A1u9i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9qOmJhMP5f3A1u9i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9qOmJhMP5f3A1u9i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9qOmJhMP5f3A1u9i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9qOmJhMP5f3A1u9i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9qOmJhMP5f3A1u9i .marker.cross{stroke:#333333;}#mermaid-svg-9qOmJhMP5f3A1u9i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9qOmJhMP5f3A1u9i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i .cluster-label text{fill:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i .cluster-label span{color:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i .label text,#mermaid-svg-9qOmJhMP5f3A1u9i span{fill:#333;color:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i .node rect,#mermaid-svg-9qOmJhMP5f3A1u9i .node circle,#mermaid-svg-9qOmJhMP5f3A1u9i .node ellipse,#mermaid-svg-9qOmJhMP5f3A1u9i .node polygon,#mermaid-svg-9qOmJhMP5f3A1u9i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9qOmJhMP5f3A1u9i .node .label{text-align:center;}#mermaid-svg-9qOmJhMP5f3A1u9i .node.clickable{cursor:pointer;}#mermaid-svg-9qOmJhMP5f3A1u9i .arrowheadPath{fill:#333333;}#mermaid-svg-9qOmJhMP5f3A1u9i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9qOmJhMP5f3A1u9i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9qOmJhMP5f3A1u9i .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-9qOmJhMP5f3A1u9i .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-9qOmJhMP5f3A1u9i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9qOmJhMP5f3A1u9i .cluster text{fill:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i .cluster span{color:#333;}#mermaid-svg-9qOmJhMP5f3A1u9i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9qOmJhMP5f3A1u9i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

编码
混流
分流
解码
采集
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);// 初始化AVCodecParserContextAVCodecParserContext *parserContext = av_parse_init(AV_CODEC_ID_H264);// 分配AVPacket空间AVPacket *avPacket = av_packet_alloc();// 分配AVFramen空间AVFrame *frame = av_frame_alloc();while(!eof(input)) {// 解析一个packetav_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);// 初始化控制socketsc_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和AVCodecContextAVCodec *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和AVPacketdemuxer->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 headernet_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);// 将解码后的帧数据传给sinkspush_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端,也就是手机侧的功能逻辑了,下篇见。

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

  1. 【投屏】Scrcpy源码分析二(Client篇-连接阶段)

    Scrcpy源码分析系列 [投屏]Scrcpy源码分析一(编译篇) [投屏]Scrcpy源码分析二(Client篇-连接阶段) [投屏]Scrcpy源码分析三(Client篇-投屏阶段) [投屏]Sc ...

  2. 【投屏】Scrcpy源码分析四(最终章 - Server篇)

    Scrcpy源码分析系列 [投屏]Scrcpy源码分析一(编译篇) [投屏]Scrcpy源码分析二(Client篇-连接阶段) [投屏]Scrcpy源码分析三(Client篇-投屏阶段) [投屏]Sc ...

  3. 【投屏】Scrcpy源码分析一(编译篇)

    Scrcpy源码分析系列 [投屏]Scrcpy源码分析一(编译篇) [投屏]Scrcpy源码分析二(Client篇-连接阶段) [投屏]Scrcpy源码分析三(Client篇-投屏阶段) [投屏]Sc ...

  4. Nouveau源码分析(三):NVIDIA设备初始化之nouveau_drm_probe

    Nouveau源码分析(三) 向DRM注册了Nouveau驱动之后,内核中的PCI模块就会扫描所有没有对应驱动的设备,然后和nouveau_drm_pci_table对照. 对于匹配的设备,PCI模块 ...

  5. Spring源码分析(三)

    Spring源码分析 第三章 手写Ioc和Aop 文章目录 Spring源码分析 前言 一.模拟业务场景 (一) 功能介绍 (二) 关键功能代码 (三) 问题分析 二.使用ioc和aop重构 (一) ...

  6. JUC源码分析-线程池篇(五):ForkJoinPool - 2

    通过上一篇(JUC源码分析-线程池篇(四):ForkJoinPool - 1)的讲解,相信同学们对 ForkJoinPool 已经有了一个大概的认识,本篇我们将通过分析源码的方式来深入了解 ForkJ ...

  7. Kubernetes Node Controller源码分析之配置篇

    2019独角兽企业重金招聘Python工程师标准>>> Author: xidianwangtao@gmail.com Kubernetes Node Controller源码分析之 ...

  8. paho架构_MQTT系列最终章-Paho源码分析(三)-心跳与重连机制

    写在前面 通过之前MQTT系列-Eclipse.Paho源码分析(二)-消息的发送与接收的介绍,相信仔细阅读过的小伙伴已经对Eclipse.Paho内部发送和订阅消息的流程有了一个较为清晰的认识,今天 ...

  9. ABP源码分析三十四:ABP.Web.Mvc

    ABP.Web.Mvc模块主要完成两个任务: 第一,通过自定义的AbpController抽象基类封装ABP核心模块中的功能,以便利的方式提供给我们创建controller使用. 第二,一些常见的基础 ...

最新文章

  1. 【bzoj 3495】PA2010 Riddle
  2. java mplayer 源码_师兄写的一个JAVA播放器的源代码
  3. PAT甲级 -- 1005 Spell It Right (20 分)
  4. WindowsPhone-GameBoy模拟器开发四--Gameboy显示系统分析
  5. 怎样在DOS下查看屏蔽和开启端口了
  6. C# winform 按钮 响应鼠标经过变换图片,如何处理?
  7. JAVA简历1到三年
  8. 乐高机器人编程自学入门
  9. 鬼畜视频制作必备——vegas pro特别版歌声合成工具UTAU
  10. hdu 5755 Gambler Bo 三进制高斯消元(开关问题变形)
  11. 移动端手指事件和手机事件:
  12. 在win10基础上安装Ubuntu16.04双系统(双硬盘)
  13. mysql sql计算经纬度
  14. Unix Shell 介绍
  15. php玩偶,玩偶娃衣 织法教程|毛衣花样图解|视频教程-编织人生
  16. 3DMax_界面介绍及基本工具使用
  17. python中输出语句的怎么写_python 中简单的输出语句
  18. matlab模拟短波天波,短波的天波传播衰减预测模型
  19. 最难找工作的10种大学生
  20. 商品下单未支付,如何取消订单?

热门文章

  1. 路由器dns服务器怎么才能自动改变,更改路由器DNS 提高网速又一方法技巧
  2. 清北学堂2019.8.8
  3. Apple iPad
  4. 用Woocommerce建立一个网上商店 [03] 增加产品类别
  5. 清晰的理解大端和小端
  6. mysql ddl过程,MySQL基础教程3-DDL(创建表)
  7. t分布, 卡方x分布,F分布
  8. block标签、inline标签、inline-block标签的特点
  9. APP 合规讲堂 - 收集使用个人信息的目的、方式、范围发生变化时,是否以适当方式通知用户(五)
  10. 【408数据结构】备考常见必会算法图鉴