Flutter 桌面端实践之识别外接媒体设备
最近我们希望Flutter技术在桌面端的应用能有所突破,所以笔者跨进了本不熟悉的桌面端应用领域。今天给大家分享下我们是如何让Flutter如何识别外接媒体设备,并且实现视频流渲染和拍照;从
官方插件外界纹理
到platformView实践
,都尝试了一遍,最后选择了webRtc
,整个预研过程一波三折,学到了很多知识!
需求背景
需求是在win10和Android9的设备上支持外接摄像头,能够进行实时拍摄,做一个类似相机的应用。
从技术流程上来分析,我们需要识别出相机设备,拿到媒体流信息然后做渲染(渲染机制一般通过外接纹理Texture
去实现),最后捕获帧进行拍照/录制。Flutter中,任何对象渲染后自然能拿到RanderObject,只要有RanderObject这个真实的渲染对象,我们就能进行照片的存储。
以上流程,理论上库已经帮我们做好,但是桌面端的生态,往往没那么简单~~~
一、官方Plugin
Android端使用camera,windows使用camera_windows。官方的库对于内置相机的支持做的很不错,直接引用后在手机和普通电脑上效果都很好;但是两个库都是明确不支持外接设备,见issus-1、issus-2,优先级分别是P4、P5,显然官方认为这些问题优先级不高。
而纵观整个Flutter生态对USB外设的支持,并没有一个官方的库,pub上的基本也是参差不齐,大多只支持单一平台。
实现原理
- Android端的
camera
插件,使用原生Camera2 Api,通过TextureRegistry创建纹理,然后Flutter用Texture进行绘制。
- 创建相机实例,返回
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);
}
}
- 预览控件返回Flutter Texture Widget,与原生返回的纹理id形成绑定,从而接收纹理信息然后绘制
// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Widget buildPreview(int cameraId) {
return Texture(textureId: cameraId);
}
- 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纹理这块官方的实现是没有问题的。
当然这个思路我目前只停留在理论层面
,并未真正去实现,原因如下:
- 两个库都是设计原生知识,我们维护成本会很大;
- 官方的库维护的很频繁,后面更多优化还得看官方,很有可能哪个版本就得全部推翻重新来一遍。
二、PlatformView
明确一个观点,这个方案不可落地。预研这个方案的原因是我们本身已经有原生的代码封装,基于CameraX的Android实现,我只需要在Plugin上注册下视图即可,具体实现代码如下:
- 注册视图
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)
)
}
- 视图工厂
class CameraFactory(private val messenger: BinaryMessenger) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
return CameraPlatformView(context!!)
}
}
- 引入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!
作者:Karl_wei
链接:https://juejin.cn/post/7115674087682375717
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。