Flutter Camera Plugin Records Dark/Black Videos on Android - Preview is Fine but Recorded Video is Dark
I’m developing a Flutter app with video recording functionality using the official camera
package (version ^0.10.6
). The camera preview looks perfect with correct brightness, but when I record and play back the video, it’s significantly darker (almost black in some cases).
Environment
- Flutter SDK: 3.8.1
- camera package: 0.10.6
- Camera Plugin Dependencies:yaml
camera: ^0.10.6 permission_handler: ^11.3.1 video_player: ^2.7.0
The Problem
- ✅ Camera preview displays with correct brightness
- ❌ Recorded video is very dark/black
- ❌ Issue occurs in both normal and low lighting conditions
- ✅ Photos taken with the same camera work fine
What I’ve Already Tried
1. Disabled Impeller Rendering Engine
Added to AndroidManifest.xml
:
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
Result: No improvement
2. Verified Permissions
All necessary permissions are declared and granted:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Current Camera Implementation
Camera Initialization
Future<void> _initializeCamera() async {
if (!mounted) return;
setState(() {
_errorMessage = null;
_isInitializing = true;
});
try {
// Request camera permission
final hasPermission = await _mediaService.hasCameraPermission();
if (!hasPermission) {
final granted = await _mediaService.requestCameraPermission();
if (!granted) {
if (!mounted) return;
setState(() {
_errorMessage = 'Camera permission required';
_isInitializing = false;
});
return;
}
}
// Get available cameras
_cameras = await availableCameras();
if (_cameras.isEmpty) {
if (!mounted) return;
setState(() {
_errorMessage = 'No cameras found';
_isInitializing = false;
});
return;
}
// Select camera based on facing direction
final selectedCamera = _cameras.firstWhere(
(camera) => camera.lensDirection == _cameraFacing,
orElse: () => _cameras.first,
);
// Initialize camera controller
_cameraController = CameraController(
selectedCamera,
ResolutionPreset.high,
enableAudio: true,
);
await _cameraController!.initialize();
await _cameraController!.setFlashMode(_flashMode);
if (!mounted) return;
setState(() {
_isInitializing = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_errorMessage = 'Failed to initialize camera: $e';
_isInitializing = false;
});
}
}
Video Recording
Future<void> _startVideoRecording() async {
if (_cameraController == null || !_cameraController!.value.isInitialized) return;
try {
// Enable torch mode if flash is on
if (_flashMode == FlashMode.always) {
await _cameraController!.setFlashMode(FlashMode.torch);
}
// Start recording
await _cameraController!.startVideoRecording();
if (!mounted) return;
setState(() {
_isRecording = true;
_recordingDuration = 0;
});
// Start timer for duration display
_recordingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_recordingDuration++;
});
}
});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to start recording: $e')),
);
}
}
Future<void> _stopVideoRecording() async {
if (_cameraController == null || !_isRecording) return;
try {
final video = await _cameraController!.stopVideoRecording();
_recordingTimer?.cancel();
// Reset flash mode
if (_flashMode == FlashMode.always) {
await _cameraController!.setFlashMode(FlashMode.off);
}
setState(() {
_isRecording = false;
_recordingDuration = 0;
});
if (!mounted) return;
// Navigate to preview screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CreatePostScreen(preSelectedMedia: [video]),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to stop recording: $e')),
);
}
}
Camera Configuration
_cameraController = CameraController(
selectedCamera,
ResolutionPreset.high, // Using high resolution
enableAudio: true,
);
Observations
- The preview shows correct brightness in real-time
- Photos captured work perfectly with correct exposure
- Only video recording produces dark output
- The darkness is consistent throughout the video (not just the first few frames)
- Increasing device brightness doesn’t affect the recorded video
Questions
- Is there a specific camera setting or initialization parameter I’m missing for video recording?
- Should I be setting exposure or ISO values before starting video recording?
- Is there a known workaround for this issue with the Flutter camera plugin?
- Are there alternative camera packages that handle video recording better?
Any help would be greatly appreciated! I’ve been stuck on this for days and can’t find a working solution.
Brief Answer
The dark video recording issue in Flutter’s camera plugin on Android is typically caused by an exposure metering mode mismatch between preview and video recording. The preview uses continuous metering while video recording often defaults to center-weighted or spot metering, resulting in underexposure. To fix this, you need to explicitly set the exposure mode to lock
before recording and potentially adjust the exposure compensation value.
Contents
- Understanding the Exposure Metering Problem
- Solutions for Dark Video Recordings
- Advanced Camera Configuration
- Alternative Camera Packages
- Testing and Verification
Understanding the Exposure Metering Problem
The discrepancy between your camera preview and recorded video brightness stems from how Android’s camera API handles exposure metering during different operations. When you’re only showing the preview, the camera system continuously adjusts exposure to maintain optimal brightness in the viewfinder. However, once you start recording video, the camera may switch to a different metering mode that prioritizes different aspects of the scene.
Key factors contributing to this issue:
-
Metering Mode Differences: The preview and video recording often use different metering modes (center-weighted, spot, or evaluative) that can produce different exposure values.
-
Exposure Lock: Your preview likely maintains continuous auto-exposure, while video recording might lock exposure at the moment recording starts, which could be suboptimal.
-
Android Camera API Behavior: Different Android device manufacturers implement camera behaviors differently, with some aggressively optimizing for preview quality over recording quality.
-
Flash Mode Handling: The way you’re switching flash modes (especially from “always” to “torch”) might be disrupting the exposure calculation.
The fact that photos work fine but videos don’t indicates that still capture maintains the preview’s exposure settings, while video recording follows a different code path with potentially different defaults.
Solutions for Dark Video Recordings
Solution 1: Lock Exposure Before Recording
The most effective solution is to lock the exposure just before starting video recording. This ensures consistent exposure settings between preview and recording:
Future<void> _startVideoRecording() async {
if (_cameraController == null || !_cameraController!.value.isInitialized) return;
try {
// Lock exposure before recording
await _cameraController!.lockExposure();
// Optional: Set exposure compensation if needed
await _cameraController!.setExposureOffset(0.0); // 0.0 is no compensation
// Enable torch mode if flash is on
if (_flashMode == FlashMode.always) {
await _cameraController!.setFlashMode(FlashMode.torch);
}
// Start recording
await _cameraController!.startVideoRecording();
// Rest of your implementation...
} catch (e) {
// Error handling...
}
}
Future<void> _stopVideoRecording() async {
if (_cameraController == null || !_isRecording) return;
try {
final video = await _cameraController!.stopVideoRecording();
_recordingTimer?.cancel();
// Reset flash mode
if (_flashMode == FlashMode.always) {
await _cameraController!.setFlashMode(FlashMode.off);
}
// IMPORTANT: Unlock exposure after recording
await _cameraController!.unlockExposure();
// Rest of your implementation...
} catch (e) {
// Error handling...
}
}
Solution 2: Configure Camera Controller with Video Settings
Modify your camera initialization to explicitly set video recording parameters:
_cameraController = CameraController(
selectedCamera,
ResolutionPreset.high,
enableAudio: true,
// Add these parameters for video recording
imageFormatGroup: ImageFormatGroup.yuv420, // Better for video processing
);
await _cameraController!.initialize();
await _cameraController!.setFlashMode(_flashMode);
// Set exposure mode to continuous for recording
await _cameraController!.setExposureMode(ExposureMode.locked);
await _cameraController!.setExposureOffset(0.3); // Positive value for brighter video
Solution 3: Platform-Specific Configuration
Create a platform channel to configure the camera with Android-specific parameters:
// In your Dart code
const platform = MethodChannel('com.yourapp/camera');
Future<void> configureAndroidCamera() async {
try {
await platform.invokeMethod('configureCamera', {
'exposureMode': 'lock',
'exposureCompensation': 0.3,
'focusMode': 'continuous',
'videoStabilization': true,
});
} on PlatformException catch (e) {
print("Error configuring camera: ${e.message}");
}
}
// In your Android MainActivity.kt
private val CHANNEL = "com.yourapp/camera"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "configureCamera") {
val exposureMode = call.argument<String>("exposureMode")
val exposureCompensation = call.argument<Double>("exposureCompensation")
// Configure camera here using CameraX or Camera2 API
configureCameraParameters(exposureMode, exposureCompensation)
result.success(null)
}
}
}
Advanced Camera Configuration
Using CameraX for More Control
For more advanced control over camera settings, consider using CameraX directly with Flutter through platform channels:
// CameraX configuration for better video quality
private fun configureCameraForVideo(camera: CameraSelector, preview: Preview, videoCapture: VideoCapture) {
val exposure = ExposureMeteringPointFactory.Builder().build()
val exposureState = cameraProvider.bindToLifecycle(
this, camera, preview, videoCapture
).cameraInfo.exposureState
// Configure exposure settings
if (exposureState.isExposureCompensationSupported) {
exposureState.exposureCompensationIndex = 3 // Adjust for brighter video
}
// Configure video capture settings
val videoCaptureConfig = VideoCaptureConfigBuilder()
.setTargetResolution(ResolutionSelector.Builder()
.setResolutionStrategy(ResolutionStrategy(HIGH, FLEXIBLE))
.build())
.setAudioBitRate(128000) // 128 kbps
.build()
}
Manual Exposure Compensation
If locking exposure doesn’t work, try manually adjusting exposure compensation:
// In your camera initialization
await _cameraController!.setExposureMode(ExposureMode.locked);
await _cameraController!.setExposureOffset(0.5); // Adjust this value (typically -1.0 to 1.0)
Values closer to 1.0 will produce brighter videos, while values closer to -1.0 will produce darker videos. You may need to experiment with different values based on lighting conditions.
Video-Specific Resolution and Format
Sometimes the issue is related to the video format or resolution. Try configuring your camera controller specifically for video:
// Initialize camera with video-specific settings
_cameraController = CameraController(
selectedCamera,
ResolutionPreset.ultraHigh, // Higher resolution might help
enableAudio: true,
);
await _cameraController!.initialize();
// Set video-specific parameters
await _cameraController!.startVideoRecording();
Alternative Camera Packages
If the official camera package continues to cause issues, consider these alternatives:
1. CameraX Plugin
The CameraX plugin provides more direct access to Android’s CameraX library:
# pubspec.yaml
camera_x: ^0.1.0
CameraX handles many of the exposure consistency issues internally and provides more granular control over camera settings.
2. Native Camera Implementation
For maximum control, implement the camera natively and communicate with Flutter through platform channels:
// In your Flutter code
Future<Uint8List> startNativeVideoRecording() async {
final Uint8List videoPath = await platform.invokeMethod('startVideoRecording');
return videoPath;
}
// In your Android code
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startVideoRecording() {
val videoFile = File(getExternalFilesDir(null), "video_${System.currentTimeMillis()}.mp4")
val videoCapture = VideoCaptureConfigBuilder()
.setTargetFile(videoFile)
.build()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture
)
return videoFile.absolutePath
}
3. Camera 2 Plugin
For older Android versions or more advanced features:
# pubspec.yaml
camera2: ^0.8.2
This plugin provides lower-level access to the Android Camera2 API, allowing more precise control over camera parameters.
Testing and Verification
After implementing any of these solutions, thoroughly test in different lighting conditions:
- Bright Outdoor Lighting: Verify that videos aren’t overexposed
- Indoor Lighting: Check consistency across different rooms
- Low Light Conditions: Ensure adequate brightness in dim environments
- Backlit Scenes: Test with strong light sources behind subjects
Create a checklist to verify each solution:
- [ ] Preview brightness matches recorded video brightness
- [ ] Flash mode changes don’t disrupt exposure
- [ ] Video quality is acceptable in all lighting conditions
- [ ] Performance remains good during recording
- [ ] No crashes or errors occur during camera operations
Record test videos with metadata to track improvements:
// Add metadata to your recorded videos
Future<void> _stopVideoRecording() async {
// ... existing code
// Add metadata
final video = await _cameraController!.stopVideoRecording();
final metadata = {
'timestamp': DateTime.now().toIso8601String(),
'exposureMode': _cameraController!.value.exposureMode.toString(),
'exposureOffset': _cameraController!.value.exposureOffset,
'flashMode': _cameraController!.value.flashMode.toString(),
'deviceInfo': '${Platform.isAndroid ? 'Android ' : 'iOS'} ${Platform.operatingSystemVersion}',
};
// Save metadata with video file
// ...
}
Conclusion
The dark video recording issue in Flutter’s camera plugin is primarily an exposure metering problem that can be resolved through several approaches:
- Lock exposure before recording and unlock afterward to maintain consistent settings
- Adjust exposure compensation values to brighten recorded videos
- Use platform channels for Android-specific camera configuration
- Consider alternative packages like CameraX or native implementations if the official package continues to cause issues
Start with the exposure locking approach, as it addresses the root cause of the problem with minimal code changes. If that doesn’t resolve the issue, experiment with exposure compensation values and consider implementing platform-specific configurations for more control.
Remember to test thoroughly across different lighting conditions and device types, as camera behavior can vary significantly between Android manufacturers. With these solutions, you should be able to achieve consistent brightness between your camera preview and recorded videos.