之前在使用scrcpy的时候走了不少弯路,但是也加深了自己对scrcpy的理解吧,那么现在就开始实现功能:
在运行scrcpy的时候可能会遇到这个问题:
先运行这个:
meson x --buildtype=release --strip -Db_lto=true
这一步的编译看似没有什么问题,他会在scrcpy/x中生成如下文件:
但是你们打开后可以发现server文件中是空的,少了一些文件,并且在执行ninja -Cx中会遇到:
~/scrcpy$ ninja -Cx ninja: Entering directory `x' [39/54] Generating server/scrcpy-server with a custom command Starting a Gradle Daemon, 1 incompatible and 2 stopped Daemons could not be reused, use --status for details
FAILURE: Build failed with an exception.
What went wrong: Could not determine the dependencies of task ':server:lintVitalRelease'.
SDK location not found. Define location with an ANDROID_SDK_ROOT environment variable or by setting the sdk.dir path in your project's local properties file at '/home/zhengxiting/scrcpy/local.properties'.
Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
Get more help at Gradle | Search for Help with Gradle
Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0. Use '--warning-mode all' to show the individual deprecation warnings. See Command-Line Interface
BUILD FAILED in 27s [53/54] Linking target app/scrcpy FAILED: server/scrcpy-server /home/zhengxiting/scrcpy/server/./scripts/build-wrapper.sh /home/zhengxiting/scrcpy/server server/scrcpy-server release ninja: build stopped: subcommand failed.
可以看到的是,我们在meason的时候并未加入scrcpy-server这个文件,并且本机并没有Android_SDK_ROOT。
由于我是真机调试,我不需要下载安装AndroidSDKROOT,具体安装步骤可以参考:Linux : ubuntu 安装Android SDK_孙战磊的博客-CSDN博客
0x01 重新开始
-
下载scrcpy源码:GitHub - Genymobile/scrcpy: Display and control your Android device
-
使用meson进行编译
meson build
-
在生成的build文件下有一个名为app的文件夹,将从GitHub上下载的scrcpy-server的压缩包放入其中。下载地址:Release scrcpy v1.23 · Genymobile/scrcpy · GitHub
-
打开scrcpy/build/app,将下载的scrcpy-server加入其中。
- 再编译一下
ninja -Cbuild
-
最后的文件目录如下:
-
之后在这个文件目录下执行
./scrcpy
0x02 通过scrcpy获取手机屏幕的yuv数据
先想实现其功能,那么先阅读一下scrcpy的官方文档:https://github.com/Genymobile/scrcpy/blob/master/DEVELOP.md,在下载的源码中也有对应的文档。
摘取要点:
-
对于屏幕数据的截取,需要利用到ffmpeg,需要了解FFmpeg的流程。
-
对于客户端的SDL,需要将获取的yuv数据在电脑上进行显示。
FFmpeg的解码流程:
-
av_register_all:注册所有的组件
先调用avcodec_register_all来注册所有的config.h里面开放的编解码器,然后会注册所有的Muxer和Demuxer(也就是封装格式),最后注册所有的Protocol(协议层的东西)。
-
avformat_alloc_context:
分配初始化一个AVFormatContext结构体。
-
avformat_find_stream_info:获取视频流信息
解码时,作用是从文件中提取流信,将所有的Stream的MetaData信息填充好,先read_packet一段数据解码分析流数据。穷举所有流,查找其中的种类为CODEC_TYPE_VIDEO。
-
avcodec_find_decoder:
找解码器,av_register_all已经将解码器和编码器放到一个链表中,根据codec ID和name循环遍历找出。
-
avcodec_open2:
打开编码器或编码器。
-
avcodec_alloc_frame:
为解码帧分配内存。
-
av_read_frame:不停地从码流中提取出帧数据
解码时,读取AVPacket,对应音频流,一个AVPacket可能包含多个AVFrame,对应视频流,一个AVPacket对应一个AVFrame。
-
AVPacket:获取这一帧的视频压缩数据。
-
avcodec_decode_video2:解压AVPacket中的压缩数据,并存入AVFrame中。(解压后的数据就是我们要获取的yuv数据)
-
avcodec_close:释放解码器。
-
av_close_input_file:关闭输入文件。
看到这应该就可以推测出在项目的文件中哪些是获取yuv数据的。回到scrcpy中,所有FFmpeg流程的文件在scrcpy/app/scr文件中。
yuv数据都是通过解码器解码后生成的,自然就要从decoder.c文件入手。而且我们也知道解码器解码生成的yuv数据放入了AVFrame中,那就直接在文件中找到有关AVFrame的字眼就可以找到yuv数据了。
在提取yuv数据之前,我们需要去了解yuv数据的存储方式。
YUV和RGB一样,都是像素数据的编码格式,一组YUV渲染屏幕上的一个像素,控制屏幕用色彩的形成将事物表现出来,其中Y表示像素中的亮度,U表示的是色度,V表示的是Chroma。这是一种压缩后的颜色表示方法,占用更少的物理空间,且对颜色的表现失真不明显,所以现在非常常用,很多视频在播放时都是使用这种形式展现的。
YUV的宏观存储方式:planar、packed。
-
planar:从字面意思上,就是平面的意思,平面比较平整,对应到存储方式上就是把YUV三种分量分别存储。
-
packed:从字面意思来看,packed是打包的意思,打包就不一定是平整的,对应到存储方式上就是把YUV三种分量交叉存储。
-
其他形式。
采样方式:
主流有三种采样方式,按照分量比例区分。
-
4:4:4 :一个像素应该包含一个Y,一个U,一个V,如果要完全存储,那一个一个像素点就要存储YUV三个分量。
因为人的眼睛对色度和饱和度不是特别敏感,所以一定程度上丢失一部分UV并不影响我们分辨颜色,所以为了节省存储空间,在存储时就故意丢掉部分UV分量。
-
4:2:2:用两个Y分量共用一组UV分量。
-
4:2:0:用四个Y分量共用一组UV,这种形式就是4:2:0。
在存储时YUV各占一个字节Byte,如果4:4:4方式,那么一个256 * 256分辨率的图片要占用256 * 256 * 3 = 196608Byte;4:2:2方式要占用256 * 256 * 2 =131072Byte;4:2:0方式要占用256 * 256 * 2/3 = 43690.7Byte,他的存储空间整整少了一半。
YUV与RGB相互转换的公式
Y=0.299R+0.587G+0.114B
U=-0.147R-0.289G+0.436B
V=0.615R-0.515G-0.100B
R=Y+1.14V
G=Y-0.39U-0.58V
B=Y+2.03U
在考虑读取yuv文件的时候,还需要考虑一下内存对齐的问题,这样才有利于我们编写读写程序:
内存对齐的原因:
计算机是以字节(Byte)为单位划分的,理论上说CPU是可以访问任一编号的字节数据的,CPU的寻址其实是通过地址总线来访问内存的。CPU又分为32位和64位,在32位的CPU一次可以处理4个字节(Byte)的数据,那么CPU实际寻址的步长就是4个字节,也就是只对编号是4的背书的内存地址进行寻址。那么同理对于64位的CPU的寻址步长是8字节,只对编号是8的倍数的内存地址进行寻址。这么做的好处也就是可以实现最快速的方式寻址且不会遗漏一个字节,也不会重复寻址。
那么为什么要内存对齐?
如果对于一个程序来说,如果一个变量的数据范围是在一个寻址步长范围内的话,那就一次寻址就可以解读完了。那么如果超出了步长范围的数据存储,就需要读取两次寻址再进行数据的拼接,效率明显就是降低了。如果我们的数据类型,没有按照内存规则进行排放,那也会造成资源的浪费以及效率的降低。数据对齐的规则,就是将数据尽量的存储在一个步长内,避免跨步长的存储,这就是内存对齐。在32位编译环境下默认4字节对齐,在64位编译环境下默认8字节对齐。归结下来就是以下这两点:
(1)移植原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
内存对齐规则
每个特定平台上的编译器都有自己的默认对齐系数,程序员可以通过预编译指令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中n就是你要指定的“对齐系数”。
-
数据成员对齐规则
结构(struct)(或联合(union))的数据成员:第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
结构或联合的整体对齐规则:数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
那么当#pragma back的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
总结的来说,内存对齐就是利用空间去节约时间。
0x03 获取yuv图像
我们可以将得到YUV图像的代码进行修改,下面是源代码:
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)) {
LOGE("Could not send frame to sink %d", i);
return false;
}
}
return true;
}
获取yuv图像:
-
ctx->height代表一帧数据中图片的高。
-
ctx->width代表同一帧数据中图片的宽。
-
frame->linesize代表视频数据一行数据的尺寸大小。
-
frame->data[0]装的yuv中的y数据的地址,也是y数据的存储的起点。
-
fwrite中的第一个参数frame->linesize[0]*i表示已经读取了i行的y数据,同时偏移至下一行。那么对于frame->data[0]+frame->linesize[0] * i就表示当前循环读取的第i+1行y数据的起点。
-
fwrite中的第二个参数1表示每次写入一个单位的数据。
-
fwrite中的第三个参数表示写入到的指定文件的名字。
FILE *fp_yuv=fopen("getyuv.yuv","wb+");
for(int i=0;i<(decoder->codec_ctx->height);i++)
{
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);
将这段代码添加到那段代码的前面,再次运行scrcpy的可执行文件。
因为我们修改了app中的scr文件,所以我们要重新按照上面的编译过程进行编译,我就新建了另外一个build_1。
之后我们就可以成功得到我们所需要的yuv数据啦!!