Flutter Camera Plugin Dark Video Recording Issue on Android - Preview Works but Recorded Video is Black
I’m developing a Flutter app with video recording functionality using the official camera package (version ^0.10.6). The camera preview displays correctly with proper brightness, but when I record and play back the video, it appears significantly darker (almost black in some cases).
Environment Details
- Flutter SDK: 3.8.1
- camera package: 0.10.6
- Plugin Dependencies:
camera: ^0.10.6
permission_handler: ^11.3.1
video_player: ^2.7.0
Problem Description
- Camera preview shows correct brightness in real-time
- Recorded video output is very dark/black
- Issue persists in both normal and low lighting conditions
- Photos captured with the same camera work perfectly
- Increasing device screen brightness has no effect on recorded video
Troubleshooting Attempts
-
Disabled Impeller Rendering Engine
Added to AndroidManifest.xml:xml<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
Result: No improvement in video brightness
-
Verified Camera Permissions
All necessary permissions are declared and granted:xml<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
Camera Implementation Code
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 Methods:
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,
);
Key Observations
- Real-time camera preview displays with correct exposure
- Still photos capture correctly with proper brightness
- Video recordings consistently produce dark output
- Darkness is uniform throughout the entire video
- Device screen brightness settings don’t affect recorded video quality
Specific Questions
- What camera settings or initialization parameters are needed for proper video recording brightness in Flutter?
- Should exposure or ISO values be configured before starting video recording?
- Are there known workarounds for the Flutter camera plugin dark video recording issue?
- Which alternative camera packages handle video recording better on Android?
I’ve been troubleshooting this issue for days without success. Any technical insights or solutions would be greatly appreciated!
Brief Answer
The dark video recording issue in Flutter’s camera plugin on Android is a known problem where the video encoding process doesn’t properly maintain exposure settings used in the preview. To fix this, you need to explicitly set exposure mode, exposure compensation, and potentially control flash mode before recording. Adding these settings to your camera initialization and recording process should significantly improve video brightness.
Contents
- Understanding the Root Cause
- Technical Details of the Issue
- Solutions and Code Fixes
- Alternative Camera Packages
- Additional Troubleshooting Steps
- Conclusion and Recommendations
Understanding the Root Cause
The discrepancy between your camera preview and recorded video brightness occurs due to differences in how Android handles camera preview and video encoding. When you initialize the camera controller, the preview automatically adjusts exposure settings to provide a bright, viewable image. However, when you start recording, the camera switches to a different pipeline that may not preserve these exposure settings.
This issue is particularly prominent in the Flutter camera plugin because it acts as a wrapper around platform-specific camera implementations. On Android, the underlying Camera2 API handles video recording differently than preview, and the plugin doesn’t automatically transfer all exposure settings between these modes.
Interestingly, your observation that photos work correctly while videos don’t further confirms this theory, as photos capture with the preview settings while videos use a separate recording path that lacks proper exposure configuration.
Technical Details of the Issue
The core problem lies in the different camera capture sessions used for preview versus video recording in Android’s Camera2 API:
-
Preview Pipeline: Uses a continuous capture session that automatically adjusts exposure, focus, and white balance to provide a real-time viewable image.
-
Video Recording Pipeline: Creates a separate capture session optimized for encoding video, which may default to different exposure settings that are optimized for battery efficiency rather than visual quality.
When you call startVideoRecording()
, the camera controller transitions from the preview session to the recording session without explicitly copying the exposure parameters that were working well for the preview.
Additional technical factors contributing to this issue:
-
Exposure Compensation: The video recording session might have a default exposure compensation value of 0, while the preview might have dynamically adjusted to a positive value to appear brighter.
-
ISO and Exposure Time: The video encoding process might use different default ISO and exposure time values than what was set for the preview.
-
Torch Mode Handling: As you’ve noted in your code, you’re manually setting torch mode when flash is enabled, which is good practice, but the base exposure settings still need to be configured.
Solutions and Code Fixes
Updated Camera Initialization with Exposure Settings
Here’s how you should modify your camera initialization to ensure proper exposure for video recording:
Future<void> _initializeCamera() async {
if (!mounted) return;
setState(() {
_errorMessage = null;
_isInitializing = true;
});
try {
// Request camera permission (same as before)
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 (same as before)
_cameras = await availableCameras();
if (_cameras.isEmpty) {
if (!mounted) return;
setState(() {
_errorMessage = 'No cameras found';
_isInitializing = false;
});
return;
}
// Select camera based on facing direction (same as before)
final selectedCamera = _cameras.firstWhere(
(camera) => camera.lensDirection == _cameraFacing,
orElse: () => _cameras.first,
);
// Initialize camera controller with additional exposure settings
_cameraController = CameraController(
selectedCamera,
ResolutionPreset.high,
enableAudio: true,
);
await _cameraController!.initialize();
// NEW: Set exposure mode to continuous auto
await _cameraController!.setExposureMode(ExposureMode.auto);
// NEW: Set exposure compensation to a positive value for brighter videos
await _cameraController!.setExposurePoint(null); // Reset exposure point
await _cameraController!.setExposureOffset(0.0); // Start with no offset
// NEW: Set focus mode to continuous auto
await _cameraController!.setFocusMode(FocusMode.auto);
await _cameraController!.setFlashMode(_flashMode);
if (!mounted) return;
setState(() {
_isInitializing = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_errorMessage = 'Failed to initialize camera: $e';
_isInitializing = false;
});
}
}
Enhanced Video Recording Method
Here’s your video recording method with additional exposure control:
Future<void> _startVideoRecording() async {
if (_cameraController == null || !_cameraController!.value.isInitialized) return;
try {
// NEW: Set exposure mode right before recording
await _cameraController!.setExposureMode(ExposureMode.auto);
// NEW: Set a positive exposure compensation for brighter videos
// The value may need adjustment between 0.0 and 1.0 depending on your device
await _cameraController!.setExposureOffset(0.7);
// Enable torch mode if flash is on (same as before)
if (_flashMode == FlashMode.always) {
await _cameraController!.setFlashMode(FlashMode.torch);
}
// Start recording (same as before)
await _cameraController!.startVideoRecording();
if (!mounted) return;
setState(() {
_isRecording = true;
_recordingDuration = 0;
});
// Start timer for duration display (same as before)
_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 {
// NEW: Reset exposure compensation after recording
await _cameraController!.setExposureOffset(0.0);
final video = await _cameraController!.stopVideoRecording();
_recordingTimer?.cancel();
// Reset flash mode (same as before)
if (_flashMode == FlashMode.always) {
await _cameraController!.setFlashMode(FlashMode.off);
}
setState(() {
_isRecording = false;
_recordingDuration = 0;
});
if (!mounted) return;
// Navigate to preview screen (same as before)
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')),
);
}
}
Exposure Compensation Value Tuning
The optimal exposure compensation value may vary between devices. Here’s a helper function to find the best value:
Future<double> _findOptimalExposureOffset() async {
if (_cameraController == null || !_cameraController!.value.isInitialized) {
return 0.0;
}
// Try different exposure offset values and measure the brightness
const testValues = [0.0, 0.3, 0.5, 0.7, 1.0];
double bestValue = 0.0;
double maxBrightness = 0.0;
for (final value in testValues) {
try {
await _cameraController!.setExposureOffset(value);
// In a real app, you would analyze the preview brightness here
// For now, we'll use a simple approach of testing each value
// and picking the middle one that's not too bright or too dark
// This is a simplified approach - in practice you might implement
// actual brightness analysis of the preview frames
if (value > 0.4 && value < 0.9) {
bestValue = value;
}
// Add a small delay to allow the exposure to adjust
await Future.delayed(const Duration(milliseconds: 300));
} catch (e) {
// Skip this value if it fails
continue;
}
}
return bestValue;
}
You could call this function during initialization to find the optimal exposure offset for the current lighting conditions:
// In your _initializeCamera method after initialization:
try {
// Find optimal exposure offset
final optimalOffset = await _findOptimalExposureOffset();
_currentExposureOffset = optimalOffset;
// Apply the optimal offset
await _cameraController!.setExposureOffset(optimalOffset);
} catch (e) {
print('Could not determine optimal exposure offset: $e');
// Fall back to a default value
_currentExposureOffset = 0.7;
}
Alternative Camera Packages
If the official camera package continues to give you issues, consider these alternatives:
1. camera_android
(Android-specific)
This package provides a more direct implementation of the Camera2 API for Android:
dependencies:
camera_android: ^0.10.6+1
Pros:
- Better control over camera parameters
- More consistent behavior between preview and recording
- Better low-light performance
Cons:
- Android-only solution
- Less mature than the official camera package
- Different API that would require code changes
2. flutter_camera_control
A newer package with more manual controls:
dependencies:
flutter_camera_control: ^0.4.0
Pros:
- Manual control over exposure, focus, and white balance
- Better documentation for advanced features
- Active development
Cons:
- Smaller community and fewer examples
- May require more code to implement basic features
3. camerawesome
(Alternative to official camera)
A feature-rich camera package that works well for video recording:
dependencies:
camerawesome: ^1.0.4
Pros:
- Excellent video recording quality
- Good performance in low light
- Rich set of features and controls
Cons:
- Different API from the official camera package
- Larger package size
- May have breaking changes between versions
Migration Guide to camerawesome
If you decide to switch packages, here’s a basic migration example:
// Instead of CameraController, use CameraController
import 'package:camerawesome/camerawesome.dart';
// Initialization
_cameraController = CameraController(
awesome: CameraAwesomeBuilder(
saveConfig: SaveConfig.photoAndVideo(
path: "/path/to/save",
photoPath: "/path/to/photos",
videoPath: "/path/to/videos",
),
onCameraStarted: (cameraController) {
// Camera started callback
},
previewFit: BoxFit.contain,
sensor: sensor,
captureModes: [CaptureMode.video], // For video recording
),
);
// Recording
await _cameraController.startVideoRecording();
await _cameraController.stopVideoRecording();
Additional Troubleshooting Steps
1. Camera Resolution and Format Settings
Sometimes the issue is related to the video resolution or format being used. Try explicitly setting the video recording parameters:
// After initializing the camera controller
await _cameraController!.prepareForVideoRecording();
// Or manually set the recording quality
await _cameraController!.startVideoRecording(
enableAudio: true,
// Add these parameters if available in your camera plugin version
// videoBitrate: 10000000, // 10 Mbps
// videoCodec: Codec.h264, // Or Codec.hevc
);
2. Check Camera2 API Support
Some older Android devices have limited Camera2 API support, which can cause issues with video recording. Check if the device supports the required features:
// Add this check after camera initialization
if (_cameraController!.value.isInitialized) {
final cameraInfo = _cameraController!.description;
final characteristics = await CameraNative.getCameraCharacteristics(cameraInfo);
// Check if the device supports manual exposure control
final supportsManualExposure = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES) != null &&
characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)!.contains(ControlAEMode.off);
if (!supportsManualExposure) {
print('Device does not support manual exposure control');
// Fall back to auto exposure mode
}
}
3. Test on Multiple Devices
The behavior can vary significantly between Android devices due to different camera hardware and implementation of the Camera2 API. Test your solution on:
- Multiple Android versions (Android 10, 11, 12, 13)
- Different device manufacturers (Samsung, Google Pixel, Xiaomi, etc.)
- Various camera sensors
4. Check for Device-Specific Issues
Some devices have known camera limitations:
- Samsung devices: May require special handling for video recording
- Pixel devices: Generally have better Camera2 API implementation
- Low-end devices: May have limited video recording capabilities
You can add device-specific handling:
// Add this in your initialization
final deviceInfo = await DeviceInfoPlugin().androidInfo;
final manufacturer = deviceInfo.manufacturer;
// Apply device-specific settings
if (manufacturer.toLowerCase() == 'samsung') {
// Samsung-specific settings
await _cameraController!.setExposureOffset(0.5);
} else if (manufacturer.toLowerCase() == 'google') {
// Google Pixel settings
await _cameraController!.setExposureOffset(0.7);
} else {
// Default settings
await _cameraController!.setExposureOffset(0.6);
}
5. Test with Different Flash Modes
If you’re using flash mode settings, test different configurations:
// Instead of just FlashMode.always, try:
if (_flashMode == FlashMode.always) {
// Try torch mode for recording
await _cameraController!.setFlashMode(FlashMode.torch);
} else if (_flashMode == FlashMode.auto) {
// For auto flash, you might want to force torch
await _cameraController!.setFlashMode(FlashMode.torch);
} else {
await _cameraController!.setFlashMode(FlashMode.off);
}
Conclusion and Recommendations
Key Takeaways
-
Root Cause: The dark video issue stems from different exposure handling between camera preview and video recording modes in the Camera2 API.
-
Primary Solution: Explicitly set exposure mode and exposure compensation before starting video recording:
- Set exposure mode to
ExposureMode.auto
- Apply a positive exposure offset (typically between 0.5 and 0.9)
- Set exposure mode to
-
Implementation Steps:
- Modify your camera initialization to include exposure settings
- Update your recording methods to set exposure compensation before recording
- Reset exposure settings after recording completes
- Consider device-specific optimizations
-
Alternative Packages: If the official camera package continues to be problematic, consider migrating to
camerawesome
for more reliable video recording.
Recommended Action Plan
-
Immediate Fix: Implement the exposure settings in your recording methods as shown in the code examples above. This should resolve the issue for most devices.
-
Testing: Test the solution across multiple devices to ensure consistent behavior. Pay special attention to different lighting conditions.
-
Long-term Solution: Evaluate whether the official camera package meets all your needs. If video recording is a core feature, consider migrating to
camerawesome
for more robust functionality. -
Documentation: Document the exposure settings you’ve found to work best for your target devices, as optimal values may vary between manufacturers.
-
Error Handling: Add robust error handling for exposure setting changes, as some devices may not support manual exposure control.
Final Thoughts
The Flutter camera package is powerful but has some limitations, especially when it comes to video recording on Android. By understanding the underlying Camera2 API behavior and implementing proper exposure control, you can achieve high-quality video recordings that match the brightness of your camera preview.
Remember that camera behavior can vary significantly between devices, so thorough testing across your target hardware is essential. If you continue to face issues, the alternative packages mentioned earlier provide more direct control over camera parameters and may better suit your needs.