import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:smtias_capture/models/display_orientation.dart';
import 'package:smtias_capture/models/preview_frame.dart';
import 'package:smtias_capture/models/preview_region.dart';
import 'package:smtias_capture/services/file_service.dart';
import 'package:smtias_capture/services/permission_service.dart';
import 'package:smtias_capture/services/preview_region_service.dart';
import 'package:smtias_capture/services/raw_capture_service.dart';
import 'package:smtias_capture/services/sound_service.dart';
/// カメラの初期化・プレビュー制御・撮影実行・ライフサイクルを管理する.
class CameraProvider extends ChangeNotifier {
/// 依存サービスはコンストラクタで差し替え可能(省略時は既定実装を生成).
CameraProvider({
PermissionService? permissionService,
FileService? fileService,
RawCaptureService? rawCaptureService,
SoundService? soundService,
PreviewRegionService? previewRegionService,
}) : _permissionService = permissionService ?? PermissionService(),
_fileService = fileService ?? FileService(),
_rawCaptureService = rawCaptureService ?? RawCaptureService(),
_soundService = soundService ?? SoundService(),
_previewRegionService = previewRegionService ?? PreviewRegionService();
final PermissionService _permissionService;
final FileService _fileService;
final RawCaptureService _rawCaptureService;
final SoundService _soundService;
final PreviewRegionService _previewRegionService;
CameraController? _controller;
bool _isInitialized = false;
bool _initializing = false;
String? _errorMessage;
bool _permissionDenied = false;
bool _permissionPermanentlyDenied = false;
String? _lastSavedFileName;
bool _isSaving = false;
bool _imageStreamActive = false;
PreviewFrame? _latestFrame;
PreviewRegion _previewRegion = PreviewRegion.full;
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;
/// プレビュー表示のクロップ領域(撮影画像には影響しない).
PreviewRegion get previewRegion => _previewRegion;
/// サウンド設定を切り替える.
Future<void> toggleSound() async {
await _soundService.toggleSound();
notifyListeners();
}
/// シャッター音を再生する.
Future<void> playShutterSound() async {
await _soundService.playShutterSound();
}
/// テスト専用: 実カメラ初期化を経ずに「初期化済み」状態を作る.
///
/// `_controller` は null のままにする(`takePicture()` は `_controller?.` と
/// null 安全に書かれているため、保存フローはそのまま駆動できる).
/// 本番コードからは呼ばないこと.
@visibleForTesting
void debugSetInitializedForTest({bool initialized = true}) {
_isInitialized = initialized;
}
/// カメラを初期化する.
///
/// 初期化処理が進行中の場合は即座に return して二重実行を防ぐ.
/// すでに初期化済み([isInitialized] が true かつ [controller] が非 null)の
/// 場合も何もしない.
Future<void> initialize() async {
// 二重実行防止: 初期化処理が進行中なら早期 return
if (_initializing) return;
// 既に初期化済みなら再初期化不要
if (_isInitialized && _controller != null) return;
_initializing = true;
try {
await _initializeInternal();
} finally {
_initializing = false;
}
}
Future<void> _initializeInternal() async {
await _soundService.init();
_previewRegion = await _previewRegionService.load();
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 = PreviewFrame(
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<void> _stopImageStream() async {
if (!_imageStreamActive || _controller == null) return;
try {
await _controller!.stopImageStream();
} catch (_) {
// 既に停止済みの場合等は無視
}
_imageStreamActive = false;
}
/// 最新のプレビューフレームを JPEG として取得する(オーバーレイなし).
Future<Uint8List?> capturePreviewSnapshot() async {
if (_latestFrame == null) return null;
final frame = _latestFrame!;
// 向き処理はライブ系と共通の単一定義(DisplayOrientation)から派生させる.
// スナップショット系は生 YUV(センサ向き未適用)を native で回転・ミラーする.
const orientation = DisplayOrientation.attachmentDownward;
return _rawCaptureService.convertYuvToJpeg(
width: frame.width,
height: frame.height,
yPlane: frame.yPlane,
uPlane: frame.uPlane,
vPlane: frame.vPlane,
yRowStride: frame.yRowStride,
uvRowStride: frame.uvRowStride,
uvPixelStride: frame.uvPixelStride,
rotation: orientation.snapshotRotationDegrees,
mirror: orientation.snapshotMirror,
);
}
/// Camera2 API で定量モード撮影を行い,DNG ファイルとメタデータ JSON を保存する.
///
/// [previewBytes] が指定された場合,`<baseName>.preview.jpg` として保存する.
/// 指定しない場合はプレビュー JPEG の保存をスキップする.
Future<void> takePicture({Uint8List? previewBytes}) async {
if (!_isInitialized || _isSaving) return;
_isSaving = true;
_isInitialized = false;
notifyListeners();
try {
if (!await _ensureStoragePermission()) return;
await _releaseCameraBeforeCapture();
await _captureAndSaveQuantitative(previewBytes);
} catch (e) {
_errorMessage = '撮影に失敗しました: $e';
_lastSavedFileName = null;
}
_isSaving = false;
notifyListeners();
// プレビューを再開
await initialize();
}
/// ストレージ権限を確認する.拒否時は errorMessage を設定して通知する.
///
/// 拒否された場合は [_isSaving] を false に戻して false を返す.
/// 呼び出し側は false を受けたら撮影フローを中断する.
Future<bool> _ensureStoragePermission() async {
final storageGranted = await _permissionService.requestStorage();
if (!storageGranted) {
_errorMessage = 'ストレージの権限が必要です';
_isSaving = false;
notifyListeners();
return false;
}
return true;
}
/// 撮影前に画像ストリームを停止しカメラを解放する.
///
/// 解放が完全に反映されるまで 500ms 待機してから戻る.
Future<void> _releaseCameraBeforeCapture() async {
await _stopImageStream();
await _controller?.dispose();
_controller = null;
// カメラが完全に解放されるまで待つ
await Future.delayed(const Duration(milliseconds: 500));
}
/// 定量モード撮影を行い,DNG・メタデータ・プレビューを保存して登録する.
///
/// [previewBytes] が非 null のときのみプレビュー JPEG を保存・登録する.
/// 保存した各ファイルを MediaStore へ登録し,DNG ファイル名を
/// [_lastSavedFileName] に反映してから保存完了音を再生する.
Future<void> _captureAndSaveQuantitative(Uint8List? previewBytes) async {
// 定量モード撮影(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}');
// メタデータ JSON は引き続き Dart 側で保存
final metaPath = await _fileService.saveQuantitativeMetadata(
baseName,
capture.metadata,
lscMap: capture.lscMap,
lscMapRowCount: capture.lscMapRowCount,
lscMapColumnCount: capture.lscMapColumnCount,
);
// プレビュー JPEG 保存(snapshot がある場合のみ)
String? previewPath;
if (previewBytes != null) {
previewPath = await _fileService.savePreviewJpeg(baseName, previewBytes);
debugPrint('Preview saved: ${previewBytes.length} bytes at $previewPath');
}
// MediaStore に登録(PC から MTP で見えるようにする)
await _rawCaptureService.scanFile(capture.dngPath);
await _rawCaptureService.scanFile(metaPath);
if (previewPath != null) {
await _rawCaptureService.scanFile(previewPath);
}
_lastSavedFileName = capture.dngPath.split('/').last;
_errorMessage = null;
// 保存完了音を再生(エラーで撮影フロー全体は止めない)
await _soundService.playSaveCompleteSound();
}
/// カメラリソースを解放する.
Future<void> disposeCamera() async {
await _stopImageStream();
_latestFrame = null;
_controller?.dispose();
_controller = null;
_isInitialized = false;
notifyListeners();
}
/// アプリの設定画面を開く(権限が永久拒否された場合).
Future<void> openSettings() async {
await _permissionService.openSettings();
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
}