As per my understanding, this error occurs because of virtual cameras that exist in the system but are inactive. When the camera package tries to enumerate available devices, it also includes these virtual ones. However, when the package attempts to initialize or access them, it fails and throws the cameraNotReadable error.
How I solved this issue:
I wrapped the availableCameras() call in a safe method that falls back to enumerateDevices() if the camera plugin fails. This way, the app can still list real and active cameras, while skipping virtual or locked ones.
Here’s the working example:
import 'dart:html' as html;
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
class CameraApp extends StatefulWidget {
@override
_CameraAppState createState() => _CameraAppState();
}
class _CameraAppState extends State<CameraApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Camera test')),
body: AppBody(),
));
}
}
class AppBody extends StatefulWidget {
@override
_AppBodyState createState() => _AppBodyState();
}
class _AppBodyState extends State<AppBody> {
bool cameraAccess = false;
String? error;
List<CameraDescription>? cameras;
@override
void initState() {
super.initState();
getCameras();
}
Future<void> getCameras() async {
try {
final cameras = await safeAvailableCameras();
setState(() {
cameraAccess = true;
this.cameras = cameras
..sort((a, b) {
final aIsVirtual = a.name.toLowerCase().contains('virtual');
final bIsVirtual = b.name.toLowerCase().contains('virtual');
if (aIsVirtual == bIsVirtual) return 0;
return aIsVirtual ? 1 : -1;
});
});
} on html.DomException catch (e) {
setState(() {
error = '${e.name}: ${e.message}';
});
}
}
Future<List<CameraDescription>> safeAvailableCameras() async {
try {
return await availableCameras();
} catch (e) {
print('availableCameras() failed: $e');
final devices =
await html.window.navigator.mediaDevices!.enumerateDevices();
final videoInputs = devices.where((d) => d.kind == 'videoinput');
return videoInputs.map((d) {
return CameraDescription(
name: d.label.isNotEmpty ? d.label : 'Camera ${d.deviceId}',
lensDirection: CameraLensDirection.front,
sensorOrientation: 0,
);
}).toList();
}
}
@override
Widget build(BuildContext context) {
if (error != null) return Center(child: Text('Error: $error'));
if (!cameraAccess) return Center(child: Text('Camera access not granted yet.'));
if (cameras == null) return Center(child: Text('Reading cameras'));
return CameraView(cameras: cameras!);
}
}
class CameraView extends StatefulWidget {
final List<CameraDescription> cameras;
const CameraView({Key? key, required this.cameras}) : super(key: key);
@override
_CameraViewState createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView> {
String? error;
CameraController? controller;
late CameraDescription cameraDescription = widget.cameras[0];
Future<void> initCam(CameraDescription description) async {
setState(() {
controller = CameraController(description, ResolutionPreset.max);
});
try {
await controller!.initialize();
} catch (e) {
setState(() {
error = e.toString();
});
}
setState(() {});
}
@override
void initState() {
super.initState();
initCam(cameraDescription);
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (error != null) {
return Center(child: Text('Initializing error: $error'));
}
if (controller == null) return Center(child: Text('Loading controller...'));
if (!controller!.value.isInitialized) {
return Center(child: Text('Initializing camera...'));
}
return Column(
children: [
AspectRatio(aspectRatio: 16 / 9, child: CameraPreview(controller!)),
DropdownButton<CameraDescription>(
value: cameraDescription,
onChanged: (CameraDescription? newValue) async {
if (controller != null) await controller!.dispose();
setState(() {
controller = null;
cameraDescription = newValue!;
});
initCam(newValue!);
},
items: widget.cameras.map((value) {
return DropdownMenuItem(
value: value,
child: Text('${value.name}: ${value.lensDirection}'),
);
}).toList(),
),
],
);
}
}