import 'package:camera/camera.dart'; import 'package:flutter/foundation.dart'; import 'package:mini_tias/services/file_service.dart'; import 'package:mini_tias/services/permission_service.dart'; import 'package:mini_tias/services/raw_capture_service.dart'; import 'package:mini_tias/services/sound_service.dart'; /// カメラの初期化・プレビュー制御・撮影実行・ライフサイクルを管理する. class CameraProvider extends ChangeNotifier { final PermissionService _permissionService = PermissionService(); final FileService _fileService = FileService(); final RawCaptureService _rawCaptureService = RawCaptureService(); final SoundService _soundService = SoundService(); CameraController? _controller; bool _isInitialized = false; String? _errorMessage; bool _permissionDenied = false; bool _permissionPermanentlyDenied = false; String? _lastSavedFileName; bool _isSaving = false; bool _imageStreamActive = false; Map? _latestFrame; CameraController? get controller => _controller; bool get isInitialized => _isInitialized; String? get errorMessage => _errorMessage; bool get permissionDenied => _permissionDenied; bool get permissionPermanentlyDenied => _permissionPermanentlyDenied; String? get lastSavedFileName => _lastSavedFileName; bool get isSaving => _isSaving; bool get isSoundEnabled => _soundService.isSoundEnabled; /// サウンド設定を切り替える. Future toggleSound() async { await _soundService.toggleSound(); notifyListeners(); } /// シャッター音を再生する. Future playShutterSound() async { await _soundService.playShutterSound(); } /// カメラを初期化する. Future initialize() async { await _soundService.init(); final granted = await _permissionService.requestCamera(); if (!granted) { _permissionDenied = true; _permissionPermanentlyDenied = await _permissionService .isCameraPermanentlyDenied(); _errorMessage = 'カメラの権限が必要です'; notifyListeners(); return; } _permissionDenied = false; _permissionPermanentlyDenied = false; try { final cameras = await availableCameras(); final frontCamera = cameras.firstWhere( (c) => c.lensDirection == CameraLensDirection.front, ); _controller = CameraController( frontCamera, ResolutionPreset.max, enableAudio: false, ); await _controller!.initialize(); await _controller!.setFlashMode(FlashMode.off); _isInitialized = true; _errorMessage = null; _startImageStream(); } catch (e) { _errorMessage = 'カメラの初期化に失敗しました: $e'; _isInitialized = false; } notifyListeners(); } /// 画像ストリームを開始して最新フレームを保持する. void _startImageStream() { if (_controller == null || _imageStreamActive) return; try { _controller!.startImageStream((CameraImage image) { _latestFrame = { 'width': image.width, 'height': image.height, 'yPlane': Uint8List.fromList(image.planes[0].bytes), 'uPlane': Uint8List.fromList(image.planes[1].bytes), 'vPlane': Uint8List.fromList(image.planes[2].bytes), 'yRowStride': image.planes[0].bytesPerRow, 'uvRowStride': image.planes[1].bytesPerRow, 'uvPixelStride': image.planes[1].bytesPerPixel ?? 1, }; }); _imageStreamActive = true; } catch (e) { debugPrint('画像ストリーム開始失敗: $e'); } } /// 画像ストリームを停止する. Future _stopImageStream() async { if (!_imageStreamActive || _controller == null) return; try { await _controller!.stopImageStream(); } catch (_) { // 既に停止済みの場合等は無視 } _imageStreamActive = false; } /// 最新のプレビューフレームを JPEG として取得する(オーバーレイなし). Future capturePreviewSnapshot() async { if (_latestFrame == null) return null; final frame = _latestFrame!; return _rawCaptureService.convertYuvToJpeg( width: frame['width'] as int, height: frame['height'] as int, yPlane: frame['yPlane'] as Uint8List, uPlane: frame['uPlane'] as Uint8List, vPlane: frame['vPlane'] as Uint8List, yRowStride: frame['yRowStride'] as int, uvRowStride: frame['uvRowStride'] as int, uvPixelStride: frame['uvPixelStride'] as int, rotation: 90, mirror: true, ); } /// Camera2 API でフル解像度 YUV キャプチャし,PNG で保存する. Future takePicture() async { if (!_isInitialized || _isSaving) return; _isSaving = true; _isInitialized = false; notifyListeners(); try { // ストレージ権限の確認 final storageGranted = await _permissionService.requestStorage(); if (!storageGranted) { _errorMessage = 'ストレージの権限が必要です'; _isSaving = false; notifyListeners(); return; } // 画像ストリームを停止してカメラを解放 await _stopImageStream(); await _controller?.dispose(); _controller = null; // カメラが完全に解放されるまで待つ await Future.delayed(const Duration(milliseconds: 500)); // 定量モード撮影(DNG 経路) // baseName を先に確定し,Kotlin 側で DNG を直接書き込む final baseName = await _fileService.generateQuantitativeBaseName(); final capture = await _rawCaptureService.captureQuantitativeDng( baseName: baseName, directoryPath: _fileService.directoryPath, ); debugPrint( 'DNG saved: ${capture.dngFileSize} bytes at ${capture.dngPath}', ); if (capture.previewPath != null) { debugPrint( 'Preview saved: ${capture.previewFileSize} bytes at ${capture.previewPath}', ); } // メタデータ JSON は引き続き Dart 側で保存 final metaPath = await _fileService.saveQuantitativeMetadata( baseName, capture.metadata, lscMap: capture.lscMap, lscMapRowCount: capture.lscMapRowCount, lscMapColumnCount: capture.lscMapColumnCount, ); // MediaStore に登録(PC から MTP で見えるようにする) await _rawCaptureService.scanFile(capture.dngPath); await _rawCaptureService.scanFile(metaPath); if (capture.previewPath != null) { await _rawCaptureService.scanFile(capture.previewPath!); } _lastSavedFileName = capture.dngPath.split('/').last; _errorMessage = null; // 保存完了音を再生(エラーで撮影フロー全体は止めない) await _soundService.playSaveCompleteSound(); } catch (e) { _errorMessage = '撮影に失敗しました: $e'; _lastSavedFileName = null; } _isSaving = false; notifyListeners(); // プレビューを再開 await initialize(); } /// カメラリソースを解放する. Future disposeCamera() async { await _stopImageStream(); _latestFrame = null; _controller?.dispose(); _controller = null; _isInitialized = false; notifyListeners(); } /// アプリの設定画面を開く(権限が永久拒否された場合). Future openSettings() async { await _permissionService.openSettings(); } @override void dispose() { _controller?.dispose(); super.dispose(); } }