注册

Flutter 桌面端实践之识别外接媒体设备

最近我们希望Flutter技术在桌面端的应用能有所突破,所以笔者跨进了本不熟悉的桌面端应用领域。今天给大家分享下我们是如何让Flutter如何识别外接媒体设备,并且实现视频流渲染和拍照;从官方插件外界纹理platformView实践,都尝试了一遍,最后选择了webRtc,整个预研过程一波三折,学到了很多知识!



需求背景


需求是在win10和Android9的设备上支持外接摄像头,能够进行实时拍摄,做一个类似相机的应用。

从技术流程上来分析,我们需要识别出相机设备,拿到媒体流信息然后做渲染(渲染机制一般通过外接纹理Texture去实现),最后捕获帧进行拍照/录制。Flutter中,任何对象渲染后自然能拿到RanderObject,只要有RanderObject这个真实的渲染对象,我们就能进行照片的存储。

以上流程,理论上库已经帮我们做好,但是桌面端的生态,往往没那么简单~~~


一、官方Plugin


Android端使用camera,windows使用camera_windows。官方的库对于内置相机的支持做的很不错,直接引用后在手机和普通电脑上效果都很好;但是两个库都是明确不支持外接设备,见issus-1issus-2,优先级分别是P4、P5,显然官方认为这些问题优先级不高。
而纵观整个Flutter生态对USB外设的支持,并没有一个官方的库,pub上的基本也是参差不齐,大多只支持单一平台。


实现原理



  • Android端的camera插件,使用原生Camera2 Api,通过TextureRegistry创建纹理,然后Flutter用Texture进行绘制。


  1. 创建相机实例,返回textureId

// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) async {
try {
final Map<String, dynamic>? reply = await _channel
.invokeMapMethod<String, dynamic>('create', <String, dynamic>{
'cameraName': cameraDescription.name,
'resolutionPreset': resolutionPreset != null
? _serializeResolutionPreset(resolutionPreset)
: null,
'enableAudio': enableAudio,
});

return reply!['cameraId']! as int;
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}


  1. 预览控件返回Flutter Texture Widget,与原生返回的纹理id形成绑定,从而接收纹理信息然后绘制

// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Widget buildPreview(int cameraId) {
return Texture(textureId: cameraId);
}


  1. Android端通过TextureRegistry创建createSurfaceTexture,把textureId返回到Dart层。

// camera_android-0.9.8+3\android\src\main\java\io\flutter\plugins\camera\MethodCallHandlerImpl.java
private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException {
String cameraName = call.argument("cameraName");
String preset = call.argument("resolutionPreset");
boolean enableAudio = call.argument("enableAudio");

TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture =
textureRegistry.createSurfaceTexture();
DartMessenger dartMessenger =
new DartMessenger(
messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper()));
CameraProperties cameraProperties =
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);

camera =
new Camera(
activity,
flutterSurfaceTexture,
new CameraFeatureFactoryImpl(),
dartMessenger,
cameraProperties,
resolutionPreset,
enableAudio);

Map<String, Object> reply = new HashMap<>();
reply.put("cameraId", flutterSurfaceTexture.id());
result.success(reply);
}

值得一提的是Flutter3.0后,官方的原生绘制方式已经抛弃了VirtualDisplay,拥抱TextureLayer,性能上已经优化了不少,让Flutter的音视频渲染能力提升了不少。 但问题就是在instantiateCamera之前,官方在Camera2的实现上,没有对外界设备进行处理,从而搜索不到对应的外接相机。



  • Windows端的实现完全一样,都是通过Texture做渲染,原因也是获取相机列表的时候没有做外接设备的实现,这里不在赘述。

解决方案


基于多端的camera接口做处理,把外设设备的逻辑加上,应该就可以了。 在Texture纹理这块官方的实现是没有问题的。
当然这个思路我目前只停留在理论层面,并未真正去实现,原因如下:



  1. 两个库都是设计原生知识,我们维护成本会很大;
  2. 官方的库维护的很频繁,后面更多优化还得看官方,很有可能哪个版本就得全部推翻重新来一遍。

二、PlatformView


明确一个观点,这个方案不可落地。预研这个方案的原因是我们本身已经有原生的代码封装,基于CameraX的Android实现,我只需要在Plugin上注册下视图即可,具体实现代码如下:



  1. 注册视图

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val key: String = "camera"

channel = MethodChannel(flutterPluginBinding.binaryMessenger, "camera_plugin")
channel.setMethodCallHandler(this)
CameraInfoManager.getCameraInfoList().forEach {
Log.d(TAG, "onAttachedToEngine: $it")
}

// 注册视图
flutterPluginBinding.platformViewRegistry.registerViewFactory(
key,
CameraFactory(flutterPluginBinding.binaryMessenger)
)
}


  1. 视图工厂

class CameraFactory(private val messenger: BinaryMessenger) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {

override fun create(context: Context?, id: Int, args: Any?): PlatformView {
return CameraPlatformView(context!!)
}

}


  1. 引入CameraX视图

class CameraPreView(context: Context, attrs: AttributeSet?) :
LinearLayout(context, attrs), LifecycleOwner {

private var camera: PreviewView

private val mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)

init {
val view: View = LayoutInflater.from(context).inflate(R.layout.layout_camera_preview, this)
camera = view.findViewById(R.id.camera_preview_view)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)

// 这里是封装好的CameraX预览视图
CameraXPreview
.bindLifecycle(this)
.setPreviewView(camera)
.setCameraId(0)
.startPreview(context)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}

override fun getLifecycle(): Lifecycle = mLifecycleRegistry
}

问题显而易见,Flutter引擎启动白屏300ms,视图同步产生延时,内存平均新增20M+,而且视图生命周期没法同步,都是致命问题。

基于上面的实践,Windows上我们没有再做尝试了,Fail。


三、webRtc


上面两种方案都以失败告终后,大佬提到了webRtc,从基础协议出发,往往能解决核心问题。于是flutter_webrtc上场,WebRTC提供音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android,已经被纳入被纳入W3C推荐标准。webRtc开发文档



  • 引用flutter_webrtc这个库,其渲染原理依旧是外接纹理,使用方法查看官方的example实例即可;
  • 重点在实现拍照功能,拍照无非就是进行帧捕获,Android已经实现:

  final videoTrack = localStream!
.getVideoTracks()
.firstWhere((track) => track.kind == 'video');
final frame = await videoTrack.captureFrame();

// 使用image.memory即可渲染
frameList = frame.asUint8List();


  • 而windows端很遗憾,还没有实现拍照功能,见issus;于是我想到了曲线救国,通过截取屏幕来保存图像由于是使用Texture渲染,通常的RenderRepaintBoundary+GlobalKey是没办法拿到RanderObject的!
    幸好插件提供了截取屏幕的方式,也算完成曲线救国了。

try {
var sources = await desktopCapturer.getSources(types: [SourceType.Window]);
DesktopCapturerSource capture =
sources.firstWhere((element) => element.name == 'my_camera');

// 使用image.memory即可渲染
frameList = capture.thumbnail;
return;
} catch (e) {
print(e.toString());
}

写在最后


到此,坎坷的外接相机预研之路告一段落。但是性能比起原生,真的差了一截,这让我们意识到,在官方不支持外接设备之前,针对此类需求,还是少用Flutter来实现。

Flutter桌面应用虽然发布了Stable版本,但说句实话生态确实比移动端差了不少,这意味着我们需要共同建设这个生态,但是趋势起来了,我们也愿意社区共建!

另外插个题外话,关于上面windows截取屏幕的需求,其实是有issus未关闭的,7月1号下午刚参与了issue的讨论,傍晚作者就拉了pull request,并且更了一版,解了燃眉之急啊!!!

怎么说呢,开源万岁,Respect!


image.png


image.png


作者:Karl_wei
链接:https://juejin.cn/post/7115674087682375717
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册