At least on iOS, when hitting the capture button rapidly twice, it will try to take the picture twice which results in
CameraException (CameraException(No camera is streaming images, stopImageStream was called when no camera is streaming images.))
since the camera is off after the first picture has been taken. This is, because the button isn't immediately disabled when clicking it, leading to such race condition.
Anyone who doesn't want their app to crash needs to implement their own custom capture button:
import 'dart:async';
import 'dart:io';
import 'package:access_control/constants/ui_value_keys.dart';
import 'package:access_control/l10n/app_localizations.dart';
import 'package:access_control/ui/commons/trailing_app_bar_button.dart';
import 'package:access_control/ui/style.dart';
import 'package:access_control/util/debug.dart';
import 'package:access_control/xl10n.dart';
import 'package:face_camera/face_camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
// ignore:unused_element
const String _tag = "ui/screens/home_screen";
class FacecamScreen extends StatefulWidget {
const FacecamScreen({
super.key,
required this.materialTitle,
required this.onCapture,
});
final String materialTitle;
final void Function(File? image) onCapture;
@override
State<FacecamScreen> createState() => _FacecamScreenState();
}
class _FacecamScreenState extends State<FacecamScreen> {
late File? _capturedImage = null;
late bool _isCapturing = false;
late FaceCameraController _controller = FaceCameraController(
enableAudio: false,
autoCapture: false,
defaultCameraLens: CameraLens.front,
performanceMode: FaceDetectorMode.accurate,
onCapture: (image) {
setState(() {
this._isCapturing = false;
this._capturedImage = image;
});
},
);
@override
void initState() {
setState(() => unawaited(this._controller.initialize()));
super.initState();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final AppLocalizations l10n = AppLocalizations.of(context)!;
return PlatformScaffold(
appBar: PlatformAppBar(
material: MaterialAppBarData().onPushedRoute(
title: widget.materialTitle,
),
cupertino: CupertinoNavigationBarData().onPushedRoute(),
leading: this._capturedImage != null
? TrailingAppBarButton(
key: const ValueKey(UiValueKeys.facecamDelete),
text: pl10n(context, PlatformI10nKey.delete),
onPressed: () {
unawaited(this._capturedImage!.delete());
setState(() {
this._capturedImage = null;
});
},
color: theme.colorScheme.error,
)
: null,
trailingActions: this._capturedImage != null
? [
TrailingAppBarButton(
key: const ValueKey(UiValueKeys.facecamSave),
text: pl10n(context, PlatformI10nKey.save),
onPressed: () {
widget.onCapture(this._capturedImage);
Navigator.of(context).pop();
},
),
]
: null,
),
body: this._capturedImage != null
? ColoredBox(
color: theme.primaryColor,
child: SizedBox.expand(child: Image.file(this._capturedImage!)),
)
: Stack(
children: [
SmartFaceCamera(
controller: this._controller,
indicatorShape: IndicatorShape.circle,
showCaptureControl:
false, // We use our own button to fix issues
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsetsGeometry.only(bottom: 70),
child: _captureControlWidget(),
),
),
],
),
);
}
Widget _captureControlWidget() {
return IconButton(
icon: CircleAvatar(
radius: 35,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.camera_alt, size: 35),
),
),
onPressed: () {
if (this._isCapturing) return;
setState(() {
this._isCapturing = true;
});
final Face? face = this._controller.value.detectedFace?.face;
if (this._controller.enableControls &&
face != null &&
face.headEulerAngleY! <= 10 &&
face.headEulerAngleY! >= -10) {
this._controller.captureImage();
} else {
setState(() {
this._isCapturing = false;
});
}
},
);
}
@override
void dispose() {
printd(_tag, "dispose screen");
super.dispose();
}
}
At least on iOS, when hitting the capture button rapidly twice, it will try to take the picture twice which results in
since the camera is off after the first picture has been taken. This is, because the button isn't immediately disabled when clicking it, leading to such race condition.
Anyone who doesn't want their app to crash needs to implement their own custom capture button: