import 'dart:async'; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // import 'package:video_player/video_player.dart'; /// Camera example home widget. class CameraTest extends StatefulWidget { /// Default Constructor const CameraTest({super.key}); @override State createState() { return _CameraTestState(); } } /// Returns a suitable camera icon for [direction]. IconData getCameraLensIcon(CameraLensDirection direction) { switch (direction) { case CameraLensDirection.back: return Icons.camera_rear; case CameraLensDirection.front: return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; } // This enum is from a different package, so a new value could be added at // any time. The example should keep working if that happens. // ignore: dead_code return Icons.camera; } void _logError(String code, String? message) { // ignore: avoid_print print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } class _CameraTestState extends State with WidgetsBindingObserver, TickerProviderStateMixin { CameraController? controller; XFile? imageFile; XFile? videoFile; // VideoPlayerController? videoController; VoidCallback? videoPlayerListener; bool enableAudio = true; double _minAvailableExposureOffset = 0.0; double _maxAvailableExposureOffset = 0.0; double _currentExposureOffset = 0.0; late AnimationController _flashModeControlRowAnimationController; late Animation _flashModeControlRowAnimation; late AnimationController _exposureModeControlRowAnimationController; late Animation _exposureModeControlRowAnimation; late AnimationController _focusModeControlRowAnimationController; late Animation _focusModeControlRowAnimation; double _minAvailableZoom = 1.0; double _maxAvailableZoom = 1.0; double _currentScale = 1.0; double _baseScale = 1.0; // Counting pointers (number of user fingers on screen) int _pointers = 0; List _cameras = []; void findCameras() async { try { _cameras = await availableCameras(); setState(() {}); // if (_cameras.isEmpty) { // showInSnackBar('No camera found'); // } else { // onNewCameraSelected(_cameras.first); // } } on CameraException catch (e) { _logError(e.code, e.description); } } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); findCameras(); _flashModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _flashModeControlRowAnimation = CurvedAnimation( parent: _flashModeControlRowAnimationController, curve: Curves.easeInCubic, ); _exposureModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _exposureModeControlRowAnimation = CurvedAnimation( parent: _exposureModeControlRowAnimationController, curve: Curves.easeInCubic, ); _focusModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _focusModeControlRowAnimation = CurvedAnimation( parent: _focusModeControlRowAnimationController, curve: Curves.easeInCubic, ); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _flashModeControlRowAnimationController.dispose(); _exposureModeControlRowAnimationController.dispose(); super.dispose(); } // #docregion AppLifecycle @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = controller; // App state changed before we got the chance to initialize. if (cameraController == null || !cameraController.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { onNewCameraSelected(cameraController.description); } } // #enddocregion AppLifecycle @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Camera example'), ), body: Column( children: [ Expanded( child: Container( decoration: BoxDecoration( color: Colors.black, border: Border.all( color: controller != null && controller!.value.isRecordingVideo ? Colors.redAccent : Colors.grey, width: 3.0, ), ), child: Padding( padding: const EdgeInsets.all(1.0), child: Center( child: _cameraPreviewWidget(), ), ), ), ), _captureControlRowWidget(), _modeControlRowWidget(), Padding( padding: const EdgeInsets.all(5.0), child: Row( children: [ _cameraTogglesRowWidget(), // _thumbnailWidget(), ], ), ), ], ), ); } /// Display the preview from the camera (or a message if the preview is not available). Widget _cameraPreviewWidget() { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { return GestureDetector( onTap: () { findCameras(); }, child: const Text( 'Find cameras', style: TextStyle( color: Colors.white, fontSize: 24.0, fontWeight: FontWeight.w900, ), ), ); } else { return Listener( onPointerDown: (_) => _pointers++, onPointerUp: (_) => _pointers--, child: CameraPreview( controller!, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return GestureDetector( behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, onTapDown: (TapDownDetails details) => onViewFinderTap(details, constraints), ); }), ), ); } } void _handleScaleStart(ScaleStartDetails details) { _baseScale = _currentScale; } Future _handleScaleUpdate(ScaleUpdateDetails details) async { // When there are not exactly two fingers on screen don't scale if (controller == null || _pointers != 2) { return; } _currentScale = (_baseScale * details.scale) .clamp(_minAvailableZoom, _maxAvailableZoom); await controller!.setZoomLevel(_currentScale); } /// Display a bar with buttons to change the flash and exposure modes Widget _modeControlRowWidget() { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( icon: const Icon(Icons.flash_on), color: Colors.blue, onPressed: controller != null ? onFlashModeButtonPressed : null, ), // The exposure and focus mode are currently not supported on the web. ...!kIsWeb ? [ IconButton( icon: const Icon(Icons.exposure), color: Colors.blue, onPressed: controller != null ? onExposureModeButtonPressed : null, ), IconButton( icon: const Icon(Icons.filter_center_focus), color: Colors.blue, onPressed: controller != null ? onFocusModeButtonPressed : null, ) ] : [], IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), color: Colors.blue, onPressed: controller != null ? onAudioModeButtonPressed : null, ), IconButton( icon: Icon(controller?.value.isCaptureOrientationLocked ?? false ? Icons.screen_lock_rotation : Icons.screen_rotation), color: Colors.blue, onPressed: controller != null ? onCaptureOrientationLockButtonPressed : null, ), ], ), _flashModeControlRowWidget(), _exposureModeControlRowWidget(), _focusModeControlRowWidget(), ], ); } Widget _flashModeControlRowWidget() { return SizeTransition( sizeFactor: _flashModeControlRowAnimation, child: ClipRect( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( icon: const Icon(Icons.flash_off), color: controller?.value.flashMode == FlashMode.off ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.off) : null, ), IconButton( icon: const Icon(Icons.flash_auto), color: controller?.value.flashMode == FlashMode.auto ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.auto) : null, ), IconButton( icon: const Icon(Icons.flash_on), color: controller?.value.flashMode == FlashMode.always ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.always) : null, ), IconButton( icon: const Icon(Icons.highlight), color: controller?.value.flashMode == FlashMode.torch ? Colors.orange : Colors.blue, onPressed: controller != null ? () => onSetFlashModeButtonPressed(FlashMode.torch) : null, ), ], ), ), ); } Widget _exposureModeControlRowWidget() { final ButtonStyle styleAuto = TextButton.styleFrom( foregroundColor: controller?.value.exposureMode == ExposureMode.auto ? Colors.orange : Colors.blue, ); final ButtonStyle styleLocked = TextButton.styleFrom( foregroundColor: controller?.value.exposureMode == ExposureMode.locked ? Colors.orange : Colors.blue, ); return SizeTransition( sizeFactor: _exposureModeControlRowAnimation, child: ClipRect( child: ColoredBox( color: Colors.grey.shade50, child: Column( children: [ const Center( child: Text('Exposure Mode'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( style: styleAuto, onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.auto) : null, onLongPress: () { if (controller != null) { CameraPlatform.instance .setExposurePoint(controller!.cameraId, null); showInSnackBar('Resetting exposure point'); } }, child: const Text('AUTO'), ), TextButton( style: styleLocked, onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.locked) : null, child: const Text('LOCKED'), ), TextButton( style: styleLocked, onPressed: controller != null ? () => controller!.setExposureOffset(0.0) : null, child: const Text('RESET OFFSET'), ), ], ), const Center( child: Text('Exposure Offset'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text(_minAvailableExposureOffset.toString()), Slider( value: _currentExposureOffset, min: _minAvailableExposureOffset, max: _maxAvailableExposureOffset, label: _currentExposureOffset.toString(), onChanged: _minAvailableExposureOffset == _maxAvailableExposureOffset ? null : setExposureOffset, ), Text(_maxAvailableExposureOffset.toString()), ], ), ], ), ), ), ); } Widget _focusModeControlRowWidget() { final ButtonStyle styleAuto = TextButton.styleFrom( foregroundColor: controller?.value.focusMode == FocusMode.auto ? Colors.orange : Colors.blue, ); final ButtonStyle styleLocked = TextButton.styleFrom( foregroundColor: controller?.value.focusMode == FocusMode.locked ? Colors.orange : Colors.blue, ); return SizeTransition( sizeFactor: _focusModeControlRowAnimation, child: ClipRect( child: ColoredBox( color: Colors.grey.shade50, child: Column( children: [ const Center( child: Text('Focus Mode'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( style: styleAuto, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.auto) : null, onLongPress: () { if (controller != null) { CameraPlatform.instance .setFocusPoint(controller!.cameraId, null); } showInSnackBar('Resetting focus point'); }, child: const Text('AUTO'), ), TextButton( style: styleLocked, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.locked) : null, child: const Text('LOCKED'), ), ], ), ], ), ), ), ); } /// Display the control bar with buttons to take pictures and record videos. Widget _captureControlRowWidget() { final CameraController? cameraController = controller; return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( icon: const Icon(Icons.camera_alt), color: Colors.blue, onPressed: cameraController != null && cameraController.value.isInitialized && !cameraController.value.isRecordingVideo ? onTakePictureButtonPressed : null, ), IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, onPressed: cameraController == null ? null : onVideoRecordButtonPressed, ), IconButton( icon: cameraController != null && cameraController.value.isRecordingPaused ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), color: Colors.blue, onPressed: () { if (cameraController == null) { return; } else if (cameraController.value.isRecordingPaused) { return onResumeButtonPressed(); } else { return onPauseButtonPressed(); } }, ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, onPressed: cameraController == null ? null : onStopButtonPressed, ), IconButton( icon: const Icon(Icons.pause_presentation), color: cameraController != null && cameraController.value.isPreviewPaused ? Colors.red : Colors.blue, onPressed: cameraController == null ? null : onPausePreviewButtonPressed, ), ], ); } /// Display a row of toggle to select the camera (or a message if no camera is available). Widget _cameraTogglesRowWidget() { final List toggles = []; void onChanged(CameraDescription? description) { if (description == null) { return; } onNewCameraSelected(description); } if (_cameras.isEmpty) { SchedulerBinding.instance.addPostFrameCallback((_) async { showInSnackBar('No camera found.'); }); return const Text('None'); } else { for (final CameraDescription cameraDescription in _cameras) { toggles.add( SizedBox( width: 90.0, child: RadioListTile( title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), groupValue: controller?.description, value: cameraDescription, onChanged: controller != null && controller!.value.isRecordingVideo ? null : onChanged, ), ), ); } } return Row(children: toggles); } String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); void showInSnackBar(String message) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { if (controller == null) { return; } final CameraController cameraController = controller!; final Offset offset = Offset( details.localPosition.dx / constraints.maxWidth, details.localPosition.dy / constraints.maxHeight, ); cameraController.setExposurePoint(offset); cameraController.setFocusPoint(offset); } Future onNewCameraSelected(CameraDescription cameraDescription) async { final CameraController? oldController = controller; 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; await oldController.dispose(); } final CameraController cameraController = CameraController( cameraDescription, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); controller = cameraController; // 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(); await Future.wait(>[ // The exposure mode is currently not supported on the web. ...!kIsWeb ? >[ cameraController.getMinExposureOffset().then( (double value) => _minAvailableExposureOffset = value), cameraController .getMaxExposureOffset() .then((double value) => _maxAvailableExposureOffset = value) ] : >[], cameraController .getMaxZoomLevel() .then((double value) => _maxAvailableZoom = value), cameraController .getMinZoomLevel() .then((double value) => _minAvailableZoom = value), ]); } on CameraException catch (e) { // switch (e.code) { // case 'CameraAccessDenied': // showInSnackBar('You have denied camera access.'); // case 'CameraAccessDeniedWithoutPrompt': // // iOS only // showInSnackBar('Please go to Settings app to enable camera access.'); // case 'CameraAccessRestricted': // // iOS only // showInSnackBar('Camera access is restricted.'); // case 'AudioAccessDenied': // showInSnackBar('You have denied audio access.'); // case 'AudioAccessDeniedWithoutPrompt': // // iOS only // showInSnackBar('Please go to Settings app to enable audio access.'); // case 'AudioAccessRestricted': // // iOS only // showInSnackBar('Audio access is restricted.'); // default: // _showCameraException(e); // break; // } } if (mounted) { setState(() {}); } } void onTakePictureButtonPressed() { takePicture().then((XFile? file) { if (mounted) { setState(() { imageFile = file; // videoController?.dispose(); // videoController = null; }); if (file != null) { showInSnackBar('Picture saved to ${file.path}'); } } }); } void onFlashModeButtonPressed() { if (_flashModeControlRowAnimationController.value == 1) { _flashModeControlRowAnimationController.reverse(); } else { _flashModeControlRowAnimationController.forward(); _exposureModeControlRowAnimationController.reverse(); _focusModeControlRowAnimationController.reverse(); } } void onExposureModeButtonPressed() { if (_exposureModeControlRowAnimationController.value == 1) { _exposureModeControlRowAnimationController.reverse(); } else { _exposureModeControlRowAnimationController.forward(); _flashModeControlRowAnimationController.reverse(); _focusModeControlRowAnimationController.reverse(); } } void onFocusModeButtonPressed() { if (_focusModeControlRowAnimationController.value == 1) { _focusModeControlRowAnimationController.reverse(); } else { _focusModeControlRowAnimationController.forward(); _flashModeControlRowAnimationController.reverse(); _exposureModeControlRowAnimationController.reverse(); } } void onAudioModeButtonPressed() { enableAudio = !enableAudio; if (controller != null) { onNewCameraSelected(controller!.description); } } Future onCaptureOrientationLockButtonPressed() async { try { if (controller != null) { final CameraController cameraController = controller!; if (cameraController.value.isCaptureOrientationLocked) { await cameraController.unlockCaptureOrientation(); showInSnackBar('Capture orientation unlocked'); } else { await cameraController.lockCaptureOrientation(); showInSnackBar( 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); } } } on CameraException catch (e) { _showCameraException(e); } } void onSetFlashModeButtonPressed(FlashMode mode) { setFlashMode(mode).then((_) { if (mounted) { setState(() {}); } showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); }); } void onSetExposureModeButtonPressed(ExposureMode mode) { setExposureMode(mode).then((_) { if (mounted) { setState(() {}); } showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); }); } void onSetFocusModeButtonPressed(FocusMode mode) { setFocusMode(mode).then((_) { if (mounted) { setState(() {}); } showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); }); } void onVideoRecordButtonPressed() { startVideoRecording().then((_) { if (mounted) { setState(() {}); } }); } void onStopButtonPressed() { stopVideoRecording().then((XFile? file) { if (mounted) { setState(() {}); } if (file != null) { showInSnackBar('Video recorded to ${file.path}'); videoFile = file; // _startVideoPlayer(); } }); } Future onPausePreviewButtonPressed() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return; } if (cameraController.value.isPreviewPaused) { await cameraController.resumePreview(); } else { await cameraController.pausePreview(); } if (mounted) { setState(() {}); } } void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) { setState(() {}); } showInSnackBar('Video recording paused'); }); } void onResumeButtonPressed() { resumeVideoRecording().then((_) { if (mounted) { setState(() {}); } showInSnackBar('Video recording resumed'); }); } Future startVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return; } if (cameraController.value.isRecordingVideo) { // A recording is already started, do nothing. return; } try { await cameraController.startVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return; } } Future stopVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { return null; } try { return cameraController.stopVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return null; } } Future pauseVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { return; } try { await cameraController.pauseVideoRecording(); } on CameraException catch (e) { _showCameraException(e); rethrow; } } Future resumeVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { return; } try { await cameraController.resumeVideoRecording(); } on CameraException catch (e) { _showCameraException(e); rethrow; } } Future setFlashMode(FlashMode mode) async { if (controller == null) { return; } try { await controller!.setFlashMode(mode); } on CameraException catch (e) { _showCameraException(e); rethrow; } } Future setExposureMode(ExposureMode mode) async { if (controller == null) { return; } try { await controller!.setExposureMode(mode); } on CameraException catch (e) { _showCameraException(e); rethrow; } } Future setExposureOffset(double offset) async { if (controller == null) { return; } setState(() { _currentExposureOffset = offset; }); try { offset = await controller!.setExposureOffset(offset); } on CameraException catch (e) { _showCameraException(e); rethrow; } } Future setFocusMode(FocusMode mode) async { if (controller == null) { return; } try { await controller!.setFocusMode(mode); } on CameraException catch (e) { _showCameraException(e); rethrow; } } // Future _startVideoPlayer() async { // if (videoFile == null) { // return; // } // final VideoPlayerController vController = kIsWeb // ? VideoPlayerController.networkUrl(Uri.parse(videoFile!.path)) // : VideoPlayerController.file(File(videoFile!.path)); // videoPlayerListener = () { // if (videoController != null) { // // Refreshing the state to update video player with the correct ratio. // if (mounted) { // setState(() {}); // } // videoController!.removeListener(videoPlayerListener!); // } // }; // vController.addListener(videoPlayerListener!); // await vController.setLooping(true); // await vController.initialize(); // await videoController?.dispose(); // if (mounted) { // setState(() { // imageFile = null; // videoController = vController; // }); // } // await vController.play(); // } Future takePicture() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return null; } if (cameraController.value.isTakingPicture) { // A capture is already pending, do nothing. return null; } try { final XFile file = await cameraController.takePicture(); return file; } on CameraException catch (e) { _showCameraException(e); return null; } } void _showCameraException(CameraException e) { _logError(e.code, e.description); showInSnackBar('Error: ${e.code}\n${e.description}'); } }