New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[camera] The camera plugin is crashing the Android app #110378
Comments
Hi @tiloc, please provide a minimal, reproducible example and the output of |
The smallest example can be found here: https://github.com/tiloc/flutter-issue110378 A video of the crash is available here, it always happens right after I have confirmed the camera permissions: Output from
|
I have created a fix, which can be found here: https://github.com/tiloc/plugins/tree/fix-110378 This is reliably fixing the issue, but I'm a bit overwhelmed by the official contribution process. |
@tiloc I can reproduce this issue. I'll label this issue for further insights from the team Reproducible on
InfoLogs
Code Sampleimport 'package:camera/camera.dart';
import 'package:flutter/material.dart';
List<CameraDescription> cameras = <CameraDescription>[];
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize
cameras = await availableCameras();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CaptureSignBoard(cameras: cameras),
);
}
}
/// Capture a sign board
class CaptureSignBoard extends StatefulWidget {
final List<CameraDescription> _cameras;
/// Default Constructor
const CaptureSignBoard({super.key, required List<CameraDescription> cameras})
: _cameras = cameras;
@override
State<CaptureSignBoard> createState() {
return _CaptureSignBoardState();
}
}
class _CaptureSignBoardState extends State<CaptureSignBoard>
with WidgetsBindingObserver, TickerProviderStateMixin {
CameraController? _controller;
Future<void>? _ensureControllerFuture;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_ensureControllerFuture = _ensureCameraController();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_disposeCameraController().ignore();
super.dispose();
}
CameraDescription _findFirstBackCamera() {
// Find the first back camera.
final cameraDescription = widget._cameras.firstWhere((currDescription) =>
currDescription.lensDirection == CameraLensDirection.back);
return cameraDescription;
}
bool get isCameraControllerNotInitialized {
return !isCameraControllerInitialized;
}
bool get isCameraControllerInitialized {
final CameraController? cameraController = _controller;
return !(cameraController == null || !cameraController.value.isInitialized);
}
Future<void> _disposeCameraController() async {
final CameraController? oldController = _controller;
print('dispose :: oldController is null :: ${oldController == null}');
if (oldController != null) {
// `_controller` needs to be set to null before getting disposed,
// to avoid a race condition when we use the controller that is being
// disposed. This happens when camera permission dialog shows up,
// which triggers `didChangeAppLifecycleState`, which disposes and
// re-creates the controller.
_controller = null;
_ensureControllerFuture = null;
if (!oldController.value.isInitialized) {
// Controllers which have not finished initialization cannot be disposed.
// TODO: Would there be any benefit in waiting for it to initialize and then dispose?
return;
}
await oldController.dispose();
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('didChangeAppLifecycleState :: $state');
if (state == AppLifecycleState.inactive) {
_disposeCameraController().ignore();
} else if (state == AppLifecycleState.resumed) {
// Resuming should fully recreate the camera controller
_ensureControllerFuture =
_disposeCameraController().then((_) => _ensureCameraController());
}
}
@override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Center(
child: _cameraPreviewWidget(),
),
),
),
],
),
),
);
}
Widget _waitForCameraWidget() {
return Align(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(),
Text(
'Warming up the Camera',
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
fontWeight: FontWeight.w900,
),
)
],
),
);
}
/// Display the preview from the camera (or a message if the preview is not available).
Widget _cameraPreviewWidget() {
final CameraController? cameraController = _controller;
if (cameraController == null || _ensureControllerFuture == null) {
return _waitForCameraWidget();
} else {
return FutureBuilder<void>(
future: _ensureControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraPreview(
_controller!,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
);
}),
);
} else {
// Otherwise, display a loading indicator.
return _waitForCameraWidget();
}
},
);
}
}
void showInSnackBar(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
Future<void> _ensureCameraController() async {
print(
'_ensureCameraController :: isCameraControllerInitialized :: $isCameraControllerInitialized');
if (isCameraControllerInitialized) {
return;
}
final CameraController cameraController = CameraController(
_findFirstBackCamera(),
ResolutionPreset.max,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420,
);
// If the controller is updated then update the UI.
cameraController.addListener(() {
if (mounted) {
setState(() {});
}
if (cameraController.value.hasError) {
showInSnackBar(
'Camera error ${cameraController.value.errorDescription}');
}
});
try {
await cameraController.initialize();
} on CameraException catch (e) {
print(e);
}
_controller = cameraController;
if (mounted) {
setState(() {});
}
}
} Flutter Doctor
|
@exaby73 thanks! Just wondering whether it should also get the "severe: fatal crash" label, as it is actually killing the entire process? |
/cc @camsim99 |
I think there are two separate issues here: 1: Forward non- It is my understanding that we want to forward errors that the "native implementation is likely to throw" (source). If there are examples of these that are not explicitly caught and handled, we should add handling for those. I think the example of the abandoned 2: This seems to be a garbage collection issue but may also relate to known issues with the Camera2 library. @tiloc Can you provide the error logs that produce this error so that I can debug further? |
If it's a bug in our implementation (which seems likely, since presumably this is referring to the surface that we are creating), then I agree, we should handle it within the native implementation. If it's unavoidable (e.g., if it's a bug with the camera implementation on some devices that we can't find any workaround for), then passing it along for the client to do something with (even if it's just showing some generic error message about being unable to use the camera) is better than crashing on the native side. |
since filing this issue I have also encountered a few more (mainly the same as #96140 and black preview screens). I forked the plugin, put lots of diagnostic logging into it and observed its flow. Here are my observations and thoughts, maybe some of it is helpful. None of my issues were in my opinion caused by bugs in Camera2, but I could find root causes for all of them in the Java code. I added fixes for these into my fork and am experiencing very stable behavior now (fork lives here: https://github.com/tiloc/plugins/tree/camplugin-better-diagnostics). The typical sequence of events that lead the plugin into these erroneous behaviors was similar: In rarer cases, the rapid succession causes the "onDisconnect" to fire, which also invokes close() on the camera. Step 4: Further calls from the Flutter code on the half-initialised, closed, etc. Java camera result in all kinds of random behaviours. This will also throw the first exceptions that the Flutter code will finally notice and can react to. The main ingredients to stabilising my fork were:
writing
Hope my detective work is beneficial. |
Good morning! @tiloc great work investigating! |
This crash seems to be first noticed on Android 13. |
cc @camsim99 |
This crash should not occur with the CameraX implementation of the camera plugin. If you try it out and find a similar issue with that implementation, please file a separate issue! |
Hi anyone who is experiencing this, I was able to fix it, with the ff. code
|
Steps to Reproduce
I am developing a complex app, based on the camera plugin example. Despite following the example closely, it is occasionally running into conditions which are crashing the native code, mainly "java.lang.IllegalArgumentException: Surface was abandoned". These do not get caught by the native code, and thus result in a crash of the overall app.
Expected results: native code catches all native exceptions and forwards them to Flutter code
Actual results: native code only catches CameraAccessException, allowing any other exception to crash the app.
I am proposing to make the catch-statement in Camera onOpened less specific, in order to catch any exception and send it to Flutter for handling.
The text was updated successfully, but these errors were encountered: