Scrcpy源码分析系列
【投屏】Scrcpy源码分析一(编译篇)
【投屏】Scrcpy源码分析二(Client篇-连接阶段)
【投屏】Scrcpy源码分析三(Client篇-投屏阶段)
【投屏】Scrcpy源码分析四(最终章 - Server篇)
前一篇我们探究了Scrcpy Client端连接阶段的逻辑,这一篇我们继续探究Client端的投屏阶段。
因为投屏阶段用到了很多音视频编解码知识和FFmpeg相关的API,所以在继续分析代码之前,我们先简单快速地回顾一下这些内容。因FFmpeg功能很广,我们只介绍Scrcpy中用到的一部分。
编码(Encode)- 将一种音视频格式文件(通常是原始、未经压缩的)通过压缩技术转换成另一种格式文件。
解码(Decode)- 将压缩后的音视频格式文件还原成原始的音视频格式文件。
通常我们所说的编解码器(Codec),就是同时包含了编码和解码的能力。
编码的意义在于,未经压缩的原始类型,数据流是非常大的,不利于存储和网络传输,所以需要对其进行编码。常见的视频原始类型有YUV
、RAW
等,音频原始类型有PCM
。常见的视频编码类型有H264
、H265
等,音频编码类型有AAC
、MP3
。
容器通常指包含了多路流的封装格式。比如一个容器内可以包含音频流、视频流、字幕流等,而对应音频流和视频流的数据格式就是音视频的编码类型。
混流/复用(mux)- 将多个流混合到一个容器中。
分流/解复用(demux)- 从一个容器中分解成多个流。
常见的容器有MP4、FLV、MKV、AVI。
音视频播放的流程通常是:
如果只需要音频或视频则,则混流/分流的过程可以省略。
上一篇有提到Scrcpy的原理的Android设备侧不断录屏、编码,将视频流传输给PC,PC进行解码和渲染,就是类似上述的过程。
Android设备的编码用的是MediaCodec硬编码,这个暂且不用太关注,我们只需要只要Android是将YUV原始数据,编码生成H264,通过video_socket传给PC。PC侧收到视频流后,通过FFmpeg进行解码并通过SDL渲染出来。
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进行视频解码的模板代码,主要有几个阶段:
AVCodec
、AVCodecContext
、AVCodecParserContext
变量,并进行相关初始化。AVPacket
和AVFrame
结构分配空间。AVPacket
是指经过编码之后的一个数据包,AVFrame
是解码后的一帧数据,视频中一帧代表一帧图片数据。AVFrame->data
后,可以根据业务需要对数据进行处理。Scrcpy中使用FFmpeg进行解码流程也大致如上。
上回说到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);
}
投屏阶段我们需要关注几个部分:
sc_file_pusher_init
- 初始化文件上传相关的数据结构。文件上传是指将文件从PC拖入镜像窗口中自动同步至/sdcard/Download
目录中。sc_decoder_init
& sc_recorder_init
- 解码器和录制相关数据结构的初始化。主要设置struct sc_packet_sink_ops
的回调函数,在open
、close
、push
三个时机触发相应的动作。(注意:这里的回调是针对Packet的,如前面提到Packet指的是经过压缩编码后的一个数据包)。sc_demuxer_init
- 对分流相关的数据结构进行初始化。sc_demuxer_add_sink
- 将解码器和录制器加到分流中,Scrcpy的分流(Demuxer)和前面提到的容器分流不太一样。容器的分流是分离出多个流,而Scrcpy中的分流指的是把同一份数据送给不同的地方去处理。比如这里会送到解码器进行解码,如果在程序启动时指定了需要进行录制,那么也会送一份数据到录制器中进行数据保存。sc_keyboard_inject_init
& sc_mouse_inject_init
- 初始化键盘和鼠标拦截的数据结构。sc_controller_init
- 对control_socket链路进行初始化。sc_controller_start
- 开启两个控制相关的新线程,一个发,一个收。sc_screen_init
- 对窗口进行初始化,并用SDL创建窗口。设置struct sc_frame_sink_ops
的回调函数, 在open
、close
、push
三个时机触发相应的动作。(注意:和前面不同,这里的回调是针对frame的,即packet解码后帧数据)。sc_decoder_add_sink
- 将窗口和V4L2加到解码器的一路流中,同分流器一样,解码器解码后的帧数据也会送到窗口上和V4L2设备中(V4L2设备需在启动程序是指定,如不指定,则此处就不会触发V4L2逻辑)。sc_demuxer_start
- 开启新线程执行分流和解码。event_loop
- 事件循环,监听SDL事件。sc_server_destroy
- 关闭和释放服务相关资源。因为上一步是死循环,只有在触发退出事件才会退出循环,走到这里的释放逻辑。其中需要重点关注的 5、7 、8、10、11,我们按照重要性顺序着重来看 - 8、10 、11、5、7。
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
的主要作用有四个:
设置on_new_frame
,并将回调传入screen(也就是桌面窗口)的video_buffer的初始化方法中,可以简单理解为将这个回调和客户端做一个绑定,后面会用到。
开启新线程执行帧处理,这里的功能最终是从一个帧队列里取帧数据,然后送给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);
}
通过SDL创建窗口和渲染器。
设置解码后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
里到底做了什么。这里先埋个坑,我们后面填充。
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_packet
和 sc_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;
}
对的,就是在这里触发了前面一节流程图的第一个问号,所以流程图可以填充一下:
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_STOPPED
和SDL_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,主要用于控制事件传输,比如鼠标键盘控制,也就是投屏中的反控功能,这也是投屏业务非常重要的一个环节,下面我们来看这部分。
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);
}
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发出去。
老规矩,抛出一张投屏阶段的时序图。不同的颜色代表不同的线程。
这一篇我们探究了Scrcpy Client端投屏阶段的逻辑。涉及的点有FFmpeg解码、SDL的窗口绘制和键盘鼠标反控。
至此Client端的逻辑已经介绍完了,分为连接阶段和投屏阶段。下一篇我们就要探究Server端,也就是手机侧的功能逻辑了,下篇见。