作者 / Very Good Ventures Team
我们 (Very Good Ventures 团队) 与 Google 合作,在今年的 Google I/O 大会上推出了照相亭互动体验 (I/O Photo Booth)。您可以与深受喜爱的 Google 吉祥物合影: Flutter 的 Dash、Android Jetpack、Chrome 的 Dino 和 Firebase 的 Sparky,并用各种贴纸装饰照片,包括派对帽、披萨、时髦眼镜等。当然,您也可以通过社交媒体下载并分享,或者用作您的个人头像!
https://photobooth.flutter.cn/
- Flutter Dashhttps://flutter.cn/dash
△ Flutter 的 Dash、firebase 的 Sparky、Android Jetpack 和 Chrome 的 Dino
我们使用 Flutter web 和 Firebase 构建了 I/O 照相亭。因为 Flutter 现在支持打造 Web 应用,我们认为这将是一个很好的方式,可以让世界各地的与会者在今年的线上 Google I/O 大会上轻松访问这一应用。Flutter web 消除了必须通过应用商店安装应用的障碍,同时用户还可以灵活选择运行应用的设备: 移动设备、桌面设备或平板电脑。因此,只要能使用浏览器,用户便可无需下载直接使用 I/O 照相亭。
- Flutter webhttps://flutter.cn/web
- Firebasehttps://firebase.google.com/
尽管 I/O 照相亭旨在提供 Web 体验,但所有代码均采用与平台无关的架构编写而成。当相机插件等原生功能的支持在各个平台就绪后,这套代码即可在所有平台 (桌面、Web 和移动设备) 通用。
使用 Flutter 构建虚拟照相亭
构建 Web 版 Flutter 相机插件
第一个挑战即在 Web 上为 Flutter 构建摄像头插件。最初,我们联系了Baseflow 团队,因为他们负责维护现有的开源 Flutter 摄像头插件。Baseflow 致力于构建适用于 iOS 和 Android 的一流摄像头插件支持,我们也很乐于与其合作,使用联合插件 (federated plugin) 方法为插件提供 Web 支持。我们尽可能符合官方插件接口,以便我们可以在准备就绪时将其合并回官方插件。
- Baseflowhttps://www.baseflow.com/
- Flutter 摄像头插件https://github.com/Baseflow/flutter-plugins
- 联合插件https://flutter.cn/docs/development/packages-and-plugins/developing-packages#federated-plugins
我们确定了两个对于在 Flutter 中构建 I/O 照相亭相机体验至关重要的 API。
- 初始化摄像头: 应用首先需要访问您的设备摄像头。对于桌面设备,访问的可能是网络摄像头,而对于移动设备,我们选择了访问前置摄像头。我们还提供了 1080p 的预期分辨率,以根据用户设备类型充分提高拍摄质量。
- 拍照: 我们使用了内置的 HtmlElementView,该控件使用平台视图将原生 Web 元素渲染为 Flutter widget。在此项目中,我们将VideoElement 渲染为原生 HTML 元素,这便是您在拍照前会在屏幕上看到的内容。我们还使用了一个 CanvasElement,用于在您点击拍照按钮时从媒体流中捕获图像。
- HtmlElementViewhttps://api.flutter.cn/flutter/widgets/HtmlElementView-class.html
- VideoElementhttps://api.flutter.cn/flutter/dart-html/VideoElement-class.html
- CanvasElementhttps://api.flutter.cn/flutter/dart-html/CanvasElement-class.html
Future<CameraImage> takePicture() async {
final videoWidth = videoElement.videoWidth;
final videoHeight = videoElement.videoHeight;
final canvas = html.CanvasElement(
width: videoWidth,
height: videoHeight,
);
canvas.context2D
..translate(videoWidth, 0)
..scale(-1, 1)
..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight);
final blob = await canvas.toBlob();
return CameraImage(
data: html.Url.createObjectUrl(blob),
width: videoWidth,
height: videoHeight,
);
}
摄像头权限
在 Web 上完成 Flutter 摄像头插件后,我们创建了一个抽象布局,以根据相机权限显示不同的界面。例如,在等待您允许或拒绝使用浏览器摄像头时,或者如果没有可供访问的摄像头时,我们可以显示一条说明性消息。
Camera(
controller: _controller,
placeholder: (_) => const SizedBox(),
preview: (context, preview) => PhotoboothPreview(
preview: preview,
onSnapPressed: _onSnapPressed,
),
error: (context, error) => PhotoboothError(error: error),
)
在上面的抽象布局中,placeholder 会在应用等待您授予摄像头权限时返回初始界面。Preview 则会在您授予权限后返回真实的界面,并显示摄像头的实时视频流。结尾的 Error 构造语句则可以在错误发生时捕获错误并显示相应的消息。
生成镜像照片
我们的下一个挑战是生成镜像照片。如果我们照原样使用摄像头拍摄的照片,那么您看到的内容将与您在照镜子时所看到的内容不一样。某些设备会提供专门处理这一问题的设置选项,所以,如果您用前置摄像头拍照,您看到的其实是照片的镜像版本。
- 镜像拍摄https://9to5mac.com/2020/07/09/iphone-mirror-selfie-photos/
在我们的第一种方法中,我们尝试捕捉默认的摄像头视图,然后围绕 y 轴对其进行 180 度翻转。这种方法似乎有效,但后来我们遇到了一个问题,即 Flutter 偶尔会覆盖这个翻转,导致视频恢复到未镜像的版本。
- HtmlElementView 的变形被覆盖https://GitHub.com/flutter/flutter/issues/79519
在 Flutter 团队的帮助下,我们将 VideoElement 放在 DivElement 中,并更新 VideoElement 以填充 DivElement 的宽度和高度,解决了这个问题。这样一来,我们能够为视频元素应用镜像,同时因为父元素是 div,所以不会被 Flutter 覆盖翻转效果。如此一来,我们便获得了所需的镜像摄像头视图!
- DivElementhttps://api.flutter.cn/flutter/dart-html/DivElement-class.html
△ 未镜像的视图