背景

开眼项目是一个纯Flutter开发的项目,并且使用Flutter开发了MV模版视频以及通用视频的编辑的功能。为了实现这些功能是怎样与音视频SDK去做通信的呢?

开眼项目前期接入编辑SDK是通过 Channel 调用编辑SDK提供的 Android与iOS 层接口的实现来与底层音视频SDK进行通信的,如图所示:

Untitled

Flutter 通过使用 grpc 分别传递到信息给到原生层 Android与iOS,然后再使用编辑SDK内部提供的两端中间层去与底层C层去通信的。
但是使用这种 Channel 方案会存在两个问题,一个就是在获取缩略图的场景下,Android端的图片需要先在jvm层拷贝一次,然后在传输到Flutter层这样jvm就会申请多余的内存,而且这个过程中也会消耗额外的像素拷贝时间。另一点就是在项目中业务这边需要写大量的Channel代码,基本上编辑SDK的每个接口都需要对应的写个Channel接口。

优化

那么如何解决以上提到的这种问题呢?

了解到 Flutter 在1.10版本以后,官方支持了 dart:ffi 功能,使得 dart 可以调用 c/c++ 代码成为了可能。
与音视频中台沟通后计划通过 ffi 的方式提供面向 Flutter 的接口,这样业务只需要调用编辑SDK提供出来的 Flutter 层接口就行不用在使用 Channel 的方式来通信了。流程如下图:

Untitled

这种新的方式使得业务接入成本与原生接入基本一致。同时编辑SDK内部通过 ffi 与 c/c++ 层通信,直接将数据通过 ffi 返回到flutter层解决了冗余内存和效率的问题。

问题

在实现ffi方案的时候总结了一些问题。

异步通信问题

在Flutter版本以前是无法进行异步通信的,原因是Flutter使用Dart语言是单线程的,Dart里面有个isolate的模块类似线程是一个典型的C/S架构只允许端口间通信。在Flutter 1.12版本上看相关的函数被strip,无法直接调用以下接口:

1
2
3
4
5
6
DART_EXPORT Dart_Port Dart_NewNativePort(const char* name,Dart_NativeMessageHandler handler,bool handle_concurrently);

DART_EXPORT bool Dart_PostCObject(Dart_Port port_id, Dart_CObject* message);

DART_EXPORT bool Dart_CloseNativePort(Dart_Port native_port_id);

Flutter 1.17版本以上Dart层暴露了对应的函数指针,这样就可以不用修改Flutter的引擎就可以直接使用了,下面是整个异步的流程:

Untitled

缓存jni环境

使用ffi后dart层与c层就直接通信了也不需要Android的jni环境。但是c层中某些时候也需要调用平台侧的一些代码,比如Android硬编硬解接口的调用。这时候c层再去调用java方法的时候就遇到了没有jni环境的问题,如下图:

Untitled

解决办法就是初始化的时候要把jni环境缓存下来以便下次能够直接调用。但是由于调用线程不一样,直接缓存的jni环境在实时使用的时候不一定能用。
有两种方式可以解决,一种是在初始化的时候把需要调用的硬编硬解class和method都先find后缓存下来,需要调用的时候直接用;第二种是缓存classloader,由于java加载类的核心是classloader,那么其实把classloader缓存下来也就能findClass,jniEnv在调用的时候在通过JavaVM创建一个新的就行。

缓存classloader的方式代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void SetJavaVm(JavaVM* vm) {
javaVM = vm;
#if defined(BUILD_WITH_FLUTTER) && KSE_OS_ANDROID
JNIEnv* env = GetEnv();
//it can be anyone of class
auto randomClass = env->FindClass("com/kwai/video/editorsdk2/EditorSdk2Utils");
jclass classClass = env->GetObjectClass(randomClass);
auto classLoaderClass = env->FindClass("java/lang/ClassLoader");
auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",
"()Ljava/lang/ClassLoader;");
gClassLoader = env->NewGlobalRef(env->CallObjectMethod(randomClass, getClassLoaderMethod));
gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",
"(Ljava/lang/String;)Ljava/lang/Class;");
#endif
if (vm != nullptr) {
assert(!pthread_once(&g_jni_ptr_once, &CreateJNIPtrKey));
} else {
pthread_key_delete(g_jni_ptr_once);
}
}

#if defined(BUILD_WITH_FLUTTER) && KSE_OS_ANDROID
jclass FindClass(const char* name) {
return static_cast<jclass>(GetEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, GetEnv()->NewStringUTF(name)));
}
#endif

字节对齐

由于cpu的访问效率,字节对齐是音视频开发中特别常见的一个问题。不过一般遇到的字节对齐问题都是Android的,因为iOS大部分细节CVPixelBuffer都帮忙处理了。但是ffi由于不和原生平台api打交道,所以需要额外处理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CVPixelBufferRef pixelBuff = pixelData->Get();
if (!pixelBuff) {
XLOGE("Dart_ThumbnailGenerator_getThumbnailASync pixelBuff is null");
return nullptr;
}
UniqueAVFramePtr frame = UniqueAVFramePtrCreate(AV_PIX_FMT_RGBA, width, height);
CVReturn err = CVPixelBufferLockBaseAddress(pixelBuff, kCVPixelBufferLock_ReadOnly);
if (err != kCVReturnSuccess) {
XLOGE("Dart_ThumbnailGenerator_getThumbnailASync error locking pixel buffer");
return nullptr;
}
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuff);
// 处理16对齐情况下
if (bytesPerRow > width*4) {
int targetPerRow = width*4;
uint8_t *dstPixel = (uint8_t*)malloc(targetPerRow*height);
uint8_t* srcPixel = (uint8_t*)CVPixelBufferGetBaseAddress(pixelBuff);
for (int line = 0; line < height; line++ ) {
memcpy(dstPixel+line*targetPerRow, srcPixel+line*bytesPerRow, targetPerRow);
}
imgData = dstPixel;
} else {
imgData = (uint8_t*)CVPixelBufferGetBaseAddress(pixelBuff);
}
CVPixelBufferUnlockBaseAddress(pixelBuff, kCVPixelBufferLock_ReadOnly);