背景
开眼项目是一个纯Flutter开发的项目,并且使用Flutter开发了MV模版视频以及通用视频的编辑的功能。为了实现这些功能是怎样与音视频SDK去做通信的呢?
开眼项目前期接入编辑SDK是通过 Channel 调用编辑SDK提供的 Android与iOS 层接口的实现来与底层音视频SDK进行通信的,如图所示:
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 的方式来通信了。流程如下图:
这种新的方式使得业务接入成本与原生接入基本一致。同时编辑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的引擎就可以直接使用了,下面是整个异步的流程:
缓存jni环境
使用ffi后dart层与c层就直接通信了也不需要Android的jni环境。但是c层中某些时候也需要调用平台侧的一些代码,比如Android硬编硬解接口的调用。这时候c层再去调用java方法的时候就遇到了没有jni环境的问题,如下图:
解决办法就是初始化的时候要把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(); 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);
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);
|