camera_service.dart 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import 'dart:io';
  2. import 'package:camera/camera.dart' as camera_pkg;
  3. import 'package:sino_med_cloud/core/utils/logger.dart';
  4. import 'package:sino_med_cloud/core/utils/toast_utils.dart';
  5. import 'package:sino_med_cloud/permission/permission_service.dart';
  6. import 'media_exception.dart';
  7. // 使用别名避免命名冲突
  8. typedef CameraController = camera_pkg.CameraController;
  9. typedef ResolutionPreset = camera_pkg.ResolutionPreset;
  10. typedef CameraLensDirection = camera_pkg.CameraLensDirection;
  11. typedef XFile = camera_pkg.XFile;
  12. /// 专业相机服务
  13. ///
  14. /// 适合医疗影像、证件采集、高质量拍照、连拍/自定义UI
  15. ///
  16. /// 使用示例:
  17. /// ```dart
  18. /// final cameraService = CameraService();
  19. /// await cameraService.init();
  20. /// final photo = await cameraService.takePicture();
  21. /// cameraService.dispose();
  22. /// ```
  23. class CameraService {
  24. CameraController? _controller;
  25. bool _isInitialized = false;
  26. bool _isDisposed = false;
  27. /// 初始化相机
  28. ///
  29. /// - [resolutionPreset] 分辨率预设,默认 veryHigh
  30. /// - [enableAudio] 是否启用音频,默认 false
  31. /// - [lensDirection] 镜头方向,默认后置摄像头
  32. ///
  33. /// 抛出 [MediaCameraException] 如果初始化失败
  34. Future<void> init({
  35. ResolutionPreset resolutionPreset = camera_pkg.ResolutionPreset.veryHigh,
  36. bool enableAudio = false,
  37. CameraLensDirection lensDirection = camera_pkg.CameraLensDirection.back,
  38. }) async {
  39. try {
  40. if (_isDisposed) {
  41. throw MediaCameraException('相机服务已释放,无法初始化');
  42. }
  43. if (_isInitialized) {
  44. AppLogger.d('相机已初始化,跳过重复初始化');
  45. return;
  46. }
  47. // 检查相机权限
  48. final permissionResult = await PermissionService.request(PermissionType.camera);
  49. if (permissionResult != PermissionResult.granted) {
  50. if (permissionResult == PermissionResult.permanentlyDenied) {
  51. ToastUtils.showError('相机权限被永久拒绝,请在设置中开启相机权限');
  52. // 自动打开设置页面
  53. await PermissionService.openSetting();
  54. throw MediaCameraException('相机权限被永久拒绝,已引导用户前往设置');
  55. } else {
  56. ToastUtils.showError('需要相机权限才能使用相机');
  57. throw MediaCameraException('相机权限被拒绝');
  58. }
  59. }
  60. AppLogger.d('开始初始化相机...');
  61. final cameras = await camera_pkg.availableCameras();
  62. if (cameras.isEmpty) {
  63. throw MediaCameraException('未找到可用相机');
  64. }
  65. final camera = cameras.firstWhere(
  66. (c) => c.lensDirection == lensDirection,
  67. orElse: () => cameras.first,
  68. );
  69. _controller = camera_pkg.CameraController(
  70. camera,
  71. resolutionPreset,
  72. enableAudio: enableAudio,
  73. );
  74. await _controller!.initialize();
  75. _isInitialized = true;
  76. AppLogger.d('相机初始化成功: ${camera.name}');
  77. } catch (e) {
  78. _isInitialized = false;
  79. if (e is camera_pkg.CameraException) {
  80. AppLogger.e('相机初始化失败(原生异常)', e);
  81. throw MediaCameraException(
  82. '相机初始化失败: ${e.toString()}',
  83. originalError: e,
  84. );
  85. }
  86. if (e is MediaCameraException) {
  87. rethrow;
  88. }
  89. AppLogger.e('相机初始化失败', e);
  90. throw MediaCameraException(
  91. '相机初始化失败: ${e.toString()}',
  92. originalError: e,
  93. );
  94. }
  95. }
  96. /// 获取相机控制器
  97. ///
  98. /// 抛出 [MediaCameraException] 如果相机未初始化
  99. CameraController get controller {
  100. if (!_isInitialized || _controller == null) {
  101. throw MediaCameraException('相机未初始化,请先调用 init()');
  102. }
  103. return _controller!;
  104. }
  105. /// 检查相机是否已初始化
  106. bool get isInitialized => _isInitialized && _controller?.value.isInitialized == true;
  107. /// 拍照
  108. ///
  109. /// 返回拍摄的照片文件,如果失败返回 null
  110. ///
  111. /// 抛出 [MediaCameraException] 如果拍照失败
  112. Future<File?> takePicture() async {
  113. try {
  114. if (!isInitialized) {
  115. throw MediaCameraException('相机未初始化,无法拍照');
  116. }
  117. AppLogger.d('开始拍照...');
  118. final camera_pkg.XFile file = await _controller!.takePicture();
  119. final photoFile = File(file.path);
  120. if (!await photoFile.exists()) {
  121. throw MediaCameraException('拍照失败:文件不存在');
  122. }
  123. AppLogger.d('拍照成功: ${photoFile.path}');
  124. return photoFile;
  125. } catch (e) {
  126. if (e is camera_pkg.CameraException) {
  127. AppLogger.e('拍照失败(原生异常)', e);
  128. throw MediaCameraException(
  129. '拍照失败: ${e.toString()}',
  130. originalError: e,
  131. );
  132. }
  133. if (e is MediaCameraException) {
  134. rethrow;
  135. }
  136. AppLogger.e('拍照失败', e);
  137. throw MediaCameraException(
  138. '拍照失败: ${e.toString()}',
  139. originalError: e,
  140. );
  141. }
  142. }
  143. /// 释放相机资源
  144. ///
  145. /// 必须在不再使用相机时调用,否则可能导致资源泄漏
  146. Future<void> dispose() async {
  147. if (_isDisposed) {
  148. return;
  149. }
  150. try {
  151. if (_controller != null) {
  152. await _controller!.dispose();
  153. _controller = null;
  154. }
  155. _isInitialized = false;
  156. _isDisposed = true;
  157. AppLogger.d('相机资源已释放');
  158. } catch (e) {
  159. AppLogger.e('释放相机资源失败', e);
  160. }
  161. }
  162. }