在对minicap进行编译之后会得到minicap和minicap.so两个问题,不同的sdk版本的minicap文件是一样的,但是对于minicap.so文件就需要进行相对的替换。说明主要的实现方法都集中在minicap.so内,那么具体是怎么做的呢,就来分析一下。
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的内容主要还是对应不同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直接在消费者端进行视频编码,这样也丝毫不逊色。