第一篇主要讲解Scrcpy源码的编译以及yuv数据的提取等基础操作。

        Scrcpy作为Android投屏神器,除了能进行低延迟的投屏之外,还能通过将server端从电脑传入移动设备实现电脑控制手机的有趣操作。本文将介绍如何通过Scrcpy获取手机的yuv数据。通过简单了解,接下来就一起看看如何实现吧。

目录

一、编译scrcpy源码

二、通过scrcpy获取手机屏幕的yuv数据

2.1Scrcpy组成(大体上有两部分组成)

2.2利用了for循环的方式进行提取(容易理解,运行较慢。主要用于讲解),

2.3内存对齐的知识点讲解

2.4第二种是直接进行读取操作:


一、编译scrcpy源码

        1.首先,我们先从github上获取Scrcpy源码(https://github.com/Genymobile/scrcpy)。

        2.对项目进行编译,这个项目是通过meson进行编译的,个人认为,meson更加友好一些,cmake有时会报错提示找不到某些需要调用的包。编译过程:

        1. 在scrcpy-master文件下输入meson build

        2.在生成的meson app文件下 需要将scrcpy-server压缩包(Release scrcpy v1.23 · Genymobile/scrcpy · GitHub:划到最下方就能看到)手动放入到该文件夹中(项目文件夹路径:scrcpy-master/build/app)。完成项目编译之后,在build/app的文件夹中会生成如下图所示的scrcpy可执行文件(就是将scrcpy-server放入的那个文件夹)。

        3. 输入ninja -Cbuild

Scrcpy_ios投屏多台手机 scrcpy投屏原理_计算机视觉

         3. 运行build/app中会生成名为scrcpy的可执行文件,在该文件夹的终端下输入:

./scrcpy

就能看到投屏到电脑上的手机页面啦。(这里编译的地方如果有问题可以在评论区提出哈,我会一一解答的)。

        至此我们已经能成功运行scrcpy投屏软件啦,接下来就是对手机屏幕数据的获取。

二、通过scrcpy获取手机屏幕的yuv数据

        这里也是本篇博客的关键点了。想要从Scrcpy中获取手机的yuv数据,首先要简单了解Scrcpy项目的构成。如果想要详细了解还是要到官网查看相关文档(https://github.com/Genymobile/scrcpy/blob/master/DEVELOP.md)。这里我对我们需要用到的部分进行简单介绍。

2.1Scrcpy组成(大体上有两部分组成)

                1.对手机屏幕数据进行获取:这里用到的原理和ffmpeg基本一致。所以想要获得yuv数据,就要对ffmpeg的流程有一定的了解,我们接下来会对ffmpeg的流程作简单的介绍。

                2.生成SDL:SDL要做的事情就是将获取到的yuv数据在电脑上进行显示。这里不难看出,我们就是要在ffmpeg和SDL对接作为突破口获取yuv数据。所以SDL的流程和实现不做过多的介绍了。

        下图是ffmpeg运行时的流程图(图片来源于雷霄骅老师的视频雷老师讲的十分详细,极力推荐。基于FFmpeg+SDL的视频播放器的制作——雷霄骅_哔哩哔哩_bilibili):

Scrcpy_ios投屏多台手机 scrcpy投屏原理_数据_02

因为ffmpeg版本的更新,可能有些函数进行了合并或者改名。但是不影响整体的流程的解读。这里我就简单的解读各个函数的作用吧。

   

1.av_register_all():注册所有的组件
        2.avformat_open_input():打开视频流文件
        3.avformat_find_stream_info():获取视频流信息
        4.avcodec_find_decoder():找出适合的解码器
        5.avcodec_open2():打开解码器
        6.av_read_frame():读取一帧的视频压缩数据
        7.AVPacket:获取这一帧视频的压缩数据(例如:这里的数据格式为H.264格式)
        8.avcodec_decode_video2:解压AVPacket中的压缩数据,并存入AVFrame中(解压后的数据就是我们要获取的yuv数据啦)

看完这些函数的介绍,就可以判断出需要在项目中的哪个位置获取yuv数据了。接下来我们回到Scrcpy工程文件中寻找。所有ffmpeg流程的文件在scrcpy-master/app/src文件夹中,如下图所示

Scrcpy_ios投屏多台手机 scrcpy投屏原理_c++_03

 解读了ffmpeg的大致流程之后,我们知道所有的yuv数据都是通过解码器解码后生成的,自然就要从decoder.c文件入手啦(对应图片中第三行第五个文件)。并且我们还知道解码器将解码生成的yuv数据放入了AVFrame中,所以在该文件中找到有关AVFrame的字眼就能找到yuv数据了。如下图所示

Scrcpy_ios投屏多台手机 scrcpy投屏原理_Scrcpy_ios投屏多台手机_04

 在提取yuv数据之前,需要了解yuv数据的存储方式(这里不过多介绍,搜索即可)。这里将通过两种代码表达方式对yuv数据进行提取。

2.2利用了for循环的方式进行提取(容易理解,运行较慢。主要用于讲解),

这种方式更加直观,通俗易理解。代码如下(将这部分代码放入上图中push_frame_to_sinks函数内的最前面即可):

FILE *fp_yuv=fopen("testyuv.yuv","wb+");
# ctx->height代表一帧数据中图片的高
# cxt->width同理
# frame->linesize代表视频数据一行数据的尺寸大小
for(int i=0;i<decoder->codec_ctx->height;i++)
    {
    # 这里的frame->data[0]装的是yuv中的y数据。注意,这里有一个比较大的坑,就是我们没有使用ctx->width,而是使用linesize[0]。
    这里涉及到内存对齐的知识十分关键。
    这里fwirte()中:
    1.第一个参数frame->data[0]指向了y数据的地址,也就是y数据的存储的起点。
    frame->linesize[0]*i则表示已经读取了i行的y数据。
    所以frame->data[0]+frame->linesize[0]*i就表示当前循环所读取第i+1行y数据的起点
    2.第二个参数1表示每次写入一个单位的数据
    3.这里每次循环写入一行数据,所以是frame->linesize
    4.最后的参数表示写入到的指定文件的名字
   	fwrite(frame->data[0]+frame->linesize[0]*i,1,frame->linesize[0],fp_yuv);
        
    }
for(int i=0;i<(decoder->codec_ctx->height)/2;i++)
    {
    	fwrite(frame->data[1]+(frame->linesize[0]*i)/2,1,frame->linesize[0]/2,fp_yuv);
    }
for(int i=0;i<(decoder->codec_ctx->height)/2;i++)
    {
    	fwrite(frame->data[2]+(frame->linesize[0]*i)/2,1,frame->linesize[0]/2,fp_yuv);
    }
fclose(fp_yuv);

2.3内存对齐的知识点讲解

 在进行yuv数据提取时,如果直接使用手机分辨率的高(h

eight)和宽(width)为依据进行提取会发现图片是扭曲的。这就涉及到本文所讲到的内存对齐的问题了。

那我们的内心就会出现灵魂三问了。

什么是内存对齐?

如何进行内存对齐?

内存对齐对我们有什么好处?

我们来通过一个简答的例子理解解决以上三个问题

假设我们要读取一个int类型数据(长度为4个字节,如下图所示)。如果我们没有进行内存对齐,数据在内存中可以随意存储的话,将会出现以下情况。int如果存放在位置为内存为1-5的位置上,cpu读取时每4个单位读取一次(32位系统的情况,64位为8位)。那么我们想要获取int数据就需要读取两次,并且第一次需要剔除0,第二次需要剔除5,6,7。这样处理数据的效率就会极其低下。所以我们选择使用内存对齐的方式,将int类型存放在4的倍数的内存地址中。

Scrcpy_ios投屏多台手机 scrcpy投屏原理_数据_05

其实内存对齐的方法运用十分广泛,在进行结构体的创建时,如果注意到内存对齐的问题,我们将提高内存的利用率。避免浪费不必要的内存损失。下面我将深入的介绍一下关于结构体的内存对齐的知识。

        我们来看以下两个结构体。第一个结构体中char a和char b都只占一个字节,比我们的读取步长4要小,所以我们可以将它们相邻的存储在一起,占位为0~1。但是下一个数据类型是int,占4个字节,那么就存放到4~8的位置,那么结构体大小为8个字节。但是我们观察strucr M2,char a占一个字节,占0的位置。那么我们下一个int就只能存储在4~7的位置上(因为1~3的位置不够),char b就存储在8的位置上。为了下一个数据的对齐,那么struct的大小为12(注意我们是从0开始算起的,0~11有12个数。因为8~11剩余大小为3,除非下一个数据类型大小为1,否则再次进行内存对齐时必然会跳过这段内存将数据写在12的位置上)。

        两者对比我们会发现,如果根据数据类型合理的安排结构体中数据的存储位置,还能起到节省内存的效果。

//结构体1
struct M1
{
	char a;
	char b;
	int c;
};
//结构体2
struct M2
{
	char a;
	int c;
	char b;
};

1. 所以内存对齐就是为了方便计算机读取内存数据的一种存储机制。

2.内存读取的方式因编译器的不同而不同,有的默认4个字节,有的默认8个字节

我们还可以通过以下代码修改内存对齐数

pragma pack(内存对齐数)

        

3.内存对齐能提升计算机处理数据的速度,同时在结构体中根据数据类型合理安排存储数据的顺序还可以节省内存

内存对齐的目的在于方便cpu对数据的读取,因为cpu读取数据不是1个byte1个byte读取的,它的步长往往是4或者8。内存对齐使得cpu读取内存数据时不会将一个小于步长的数据分两次读取(原理所实现的目的简单来说就是这句话)。

2.4第二种是直接进行读取操作:

fwrite(frame->data[0],1,frame->linesize[0]*decoder->codec_ctx->height,fp_yuv);
fwrite(frame->data[1],1,frame->linesize[0]*decoder->codec_ctx->height/4,fp_yuv);
fwrite(frame->data[2],1,frame->linesize[0]*decoder->codec_ctx->height/4,fp_yuv);

保存代码之后,再次运行scrcpy可执行文件。在scrcpy同目录下就会生成我们创建的fp_yuv数据啦。我们可以拿yuv专属播放器播放就能获取到一帧的图片数据了,用于验证提取的数据是否正确。因为每个手机的分辨率不同,所以在播放数据时记得调整图片显示的分辨率。

确认自己提取的图片数据无误之后,我们就能通过Zeromq进行传输啦。接下来的内容请看我的下一篇博客([Ubuntu]Scrcpy+Zeromq实现手机屏幕yuv数据传输,并通过OpenCV实现连续播放——(二)(思路+代码解析)_又是谁在卷的博客。如果本文内容有帮助,期待一键三连哈~