Minicap截图原理分析

        在对minicap进行编译之后会得到minicap和minicap.so两个问题,不同的sdk版本的minicap文件是一样的,但是对于minicap.so文件就需要进行相对的替换。说明主要的实现方法都集中在minicap.so内,那么具体是怎么做的呢,就来分析一下。

minicap分析

minicap的内容对应的是源码中的minicap.cpp这个文件,主要包括以下功能:

1. 解析参数

2. 设置信号handler

3. minicap线程池

4. help展示信息

5. 创建minicap用于捕获信息

6.开启socket端口,监听"minicap"虚拟端口

Minicap类作为原生类,根据不同的sdk版本实现 MinicapImpl继承Minicap类。具体的功能都在

int main(int argc, char* argv[]) //主入口

直接顺序看代码进行理解就行了,这部分内容比较简单就不上代码多加赘述,稍微懂点C语言的都很容易看得懂。但是这边为什么要这么设置呢?主要是因为不同sdk之间相差比较大,比如以CaptureMethod为例,里面列举了“framebuffer, screenshot和virtual display”三种截图方式,对于SDK比较就得需要用screenshot,对于新版SDK就可以使用virtual display的方法进行截图,效率更高。因此通过类继承的方式实现会比较方便。

minicap.so分析

        minicap.so的内容主要还是对应不同sdk版本的cpp文件实现的,这边以官方最新的minicap_29.cpp为例进行分析。

具体的核心代码主要是createVirtualDisplay方法,首先是设定配置,然后确定SurfaceComposerClient能用:

MCINFO("Creating SurfaceComposerClient");
android::sp sc = new android::SurfaceComposerClient();MCINFO("Performing SurfaceComposerClient init check");
if ((err = sc->initCheck()) != android::NO_ERROR) {
  MCERROR("Unable to initialize SurfaceComposerClient");  
  return err;
}

随后根据配置信息(真实和虚拟的长,宽,旋转)创建一个虚拟的显示窗口 virtual display:

// Create virtual display.
MCINFO("Creating virtual display");
mVirtualDisplay = android::SurfaceComposerClient::createDisplay(
  /* const String8& displayName */
  android::String8("minicap"), 
 /* bool secure */ 
 true
);

创建缓存队列,并且配置缓存的大小(宽,高和编码格式)

其中缓存队列分别设定了生产者和消费者,设置一个叫做"minicap"的CPU消费者,设置帧监听器,用于监听帧是否生成。

MCINFO("Creating buffer queue");
android::BufferQueue::createBufferQueue(&mBufferProducer, &mBufferConsumer, false);
CINFO("Setting buffer options");
mBufferConsumer->setDefaultBufferSize(targetWidth, targetHeight);
mBufferConsumer->setDefaultBufferFormat(android::PIXEL_FORMAT_RGBA_8888);
MCINFO("Creating CPU consumer");
mConsumer = new android::CpuConsumer(mBufferConsumer, 3, false);
mConsumer->setName(android::String8("minicap"));

随后就是主要的三个模块了:

数据生产

*通过surfaceComposerClient:Transaction设置virtualDisplay作为投影目标,并且作为缓存队列的生产者。

MCINFO("Publishing virtual display");
android::SurfaceComposerClient::Transaction t;
t.setDisplaySurface(mVirtualDisplay, mBufferProducer);
t.setDisplayProjection(mVirtualDisplay,  android::DISPLAY_ORIENTATION_0, layerStackRect, visibleRect);
t.setDisplayLayerStack(mVirtualDisplay, 0); // default 
stackt.apply();

这边需要理解的是一个SurfaceFlinger的概念,SurfaceFlinger计算生成了需要渲染在屏幕上的内容。每个带有UI的应用程序都需要与SurfaceFlinger服务简历一个连接,通过这个链接来请求SurfaceFlinger为它创建和渲染Surface。

由于每个应用程序都需要和SurfaceFlinger通信,为了能够区分哪个应用程序,所以有了SurfaceComposerClient,通过SurfaceComposerClient与SurfaceFlinger进行通信,跨进程交流。

而SurfaceComposerClient::Transaction是一个事务操作,提供了projection的投影方法,这样可以直接将SurfaceFlinger的渲染内容投影到我们的virtualdisplay上,并且作为缓存的生产数据。

数据消费

数据消费主要是通过consumePendingFrame的方法来获取帧,直接从缓存队列中取对应的缓存保存为帧对象即可

consumePendingFrame(Minicap::Frame* frame) {
  android::status_t err;  if ((err = mConsumer->lockNextBuffer(&mBuffer)) != android::NO_ERROR) {
    if (err == -EINTR) {
      return err;    }
    else {
      MCERROR("Unable to lock next buffer %s (%d)", error_name(err), err);      return err;    }
  }

  frame->data = mBuffer.data; 
  frame->format = convertFormat(mBuffer.format);
  frame->width = mBuffer.width;
  frame->height = mBuffer.height;
  frame->stride = mBuffer.stride;
  frame->bpp = android::bytesPerPixel(mBuffer.format);
  frame->size = mBuffer.stride * mBuffer.height * frame->bpp;
  mHaveBuffer = true;
  return 0;
 }

消息通知

这边的FrameProxy继承了ConsumerBase::FrameAvailableListener的方法,作为一个帧监听器。

MCINFO("Creating frame waiter");
mFrameProxy = new FrameProxy(mUserFrameAvailableListener);
mConsumer->setFrameAvailableListener(mFrameProxy);

消费者通过setFrameAvailableListener方法将一个listener加入到Productor端去,便于在帧数据可用时,可以监听到并告知Consumer去做数据的处理,Product需要在数据可用时触发这个listener的onFrameAvailable,从而让数据从Productor转到Consumer则,数据处理应该是在同一进程的而不是跨进程。

如果作为Consumer如CPUConsumer没有使用setFrameAvailableListener将自己加入到listener中去,会由ConsumerBase的接口onFrameAvailable来替代完成。

总结

        总体的方式还是

服务端:

        通过对SurFlinger的渲染内容投影到virtual Display上,然后进行jpeg encode,随后通过socket传送[4字节长度,图像数据]传输到连接到minicap虚拟端口的客户端。

客户端:

        通过adb forward的方式将本地端口映射到minicap虚拟端口,接收到服务端的图像数据,先读取4字节的长度,然后根据长度获取图片,作为img的blob直接渲染到ui上。(这边可以使用src="[blob]"直接渲染,不过实际使用中会频闪的问题,还是使用draw的方法对眼睛比较和谐。)

        总体看起来和scrcpy的方法殊途同归,可以考虑直接将jni写成app_process的方式,加上x264或者Mediacodec直接在消费者端进行视频编码,这样也丝毫不逊色。

你可能感兴趣的:(scrcpy,ADB系列,Unity性能分析,android,minicap)