GLM 4.5 Air

Fix Flutter Camera Dark Video Issue on Android

Solve Flutter camera plugin dark video recording problem on Android. Learn exposure settings, code fixes, and alternative packages for bright video recordings.

Question

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

  1. 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

  2. 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:

dart
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:

dart
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:

dart
_cameraController = CameraController(
  selectedCamera,
  ResolutionPreset.high,  // Using high resolution
  enableAudio: true,
);

Key Observations

  1. Real-time camera preview displays with correct exposure
  2. Still photos capture correctly with proper brightness
  3. Video recordings consistently produce dark output
  4. Darkness is uniform throughout the entire video
  5. Device screen brightness settings don’t affect recorded video quality

Specific Questions

  1. What camera settings or initialization parameters are needed for proper video recording brightness in Flutter?
  2. Should exposure or ISO values be configured before starting video recording?
  3. Are there known workarounds for the Flutter camera plugin dark video recording issue?
  4. 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!

GLM 4.5 Air

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

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:

  1. Preview Pipeline: Uses a continuous capture session that automatically adjusts exposure, focus, and white balance to provide a real-time viewable image.

  2. 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:

dart
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:

dart
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:

dart
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:

dart
// 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:

yaml
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:

yaml
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:

yaml
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:

dart
// 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:

dart
// 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:

dart
// 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:

dart
// 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:

dart
// 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

  1. Root Cause: The dark video issue stems from different exposure handling between camera preview and video recording modes in the Camera2 API.

  2. 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)
  3. 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
  4. Alternative Packages: If the official camera package continues to be problematic, consider migrating to camerawesome for more reliable video recording.

Recommended Action Plan

  1. 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.

  2. Testing: Test the solution across multiple devices to ensure consistent behavior. Pay special attention to different lighting conditions.

  3. 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.

  4. Documentation: Document the exposure settings you’ve found to work best for your target devices, as optimal values may vary between manufacturers.

  5. 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.