package com.example.mini_tias import android.content.Context import android.graphics.ImageFormat import android.hardware.camera2.* import android.media.ImageReader import android.media.MediaScannerConnection import android.os.Handler import android.os.HandlerThread import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel /** * Camera2 API を使用してフロントカメラから YUV_420_888 フォーマットで * フル解像度の画像を 1 フレームキャプチャする. */ class RawCapturePlugin(private val context: Context) : MethodChannel.MethodCallHandler { private var backgroundThread: HandlerThread? = null private var backgroundHandler: Handler? = null private var cameraDevice: CameraDevice? = null override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "captureFullResolutionYuv" -> captureFullResolutionYuv(result) "scanFile" -> { val path = call.argument("path") if (path != null) { MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf("image/png")) { _, _ -> result.success(null) } } else { result.error("INVALID_PATH", "パスが指定されていません", null) } } else -> result.notImplemented() } } private fun captureFullResolutionYuv(result: MethodChannel.Result) { startBackgroundThread() val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraId = cameraManager.cameraIdList.firstOrNull { id -> val characteristics = cameraManager.getCameraCharacteristics(id) characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT } if (cameraId == null) { returnError(result, "NO_CAMERA", "フロントカメラが見つかりません") return } val characteristics = cameraManager.getCameraCharacteristics(cameraId) val streamConfigMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) if (streamConfigMap == null) { returnError(result, "NO_CONFIG", "カメラの設定を取得できません") return } val yuvSizes = streamConfigMap.getOutputSizes(ImageFormat.YUV_420_888) val maxSize = yuvSizes.maxByOrNull { it.width * it.height } if (maxSize == null) { returnError(result, "NO_SIZE", "YUV の解像度を取得できません") return } val imageReader = ImageReader.newInstance( maxSize.width, maxSize.height, ImageFormat.YUV_420_888, 2 ) try { cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { cameraDevice = camera try { camera.createCaptureSession( listOf(imageReader.surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { try { // Phase 1: プレビューを流して AE/AF を安定させる // この間のフレームは捨てる(リスナー未設定) val previewRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(imageReader.surface) set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF) } session.setRepeatingRequest(previewRequest.build(), null, backgroundHandler) // Phase 2: 1 秒後にリスナーを設定してから本番キャプチャ backgroundHandler?.postDelayed({ try { session.stopRepeating() // プレビュー中のバッファを捨てる while (true) { val stale = imageReader.acquireLatestImage() if (stale != null) { stale.close() } else { break } } // ここでリスナーを設定(本番フレームのみ受信) imageReader.setOnImageAvailableListener({ reader -> val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener try { val yPlane = image.planes[0] val uPlane = image.planes[1] val vPlane = image.planes[2] val yBytes = ByteArray(yPlane.buffer.remaining()) val uBytes = ByteArray(uPlane.buffer.remaining()) val vBytes = ByteArray(vPlane.buffer.remaining()) yPlane.buffer.get(yBytes) uPlane.buffer.get(uBytes) vPlane.buffer.get(vBytes) val data = mapOf( "width" to image.width, "height" to image.height, "yPlane" to yBytes, "uPlane" to uBytes, "vPlane" to vBytes, "yRowStride" to yPlane.rowStride, "uvRowStride" to uPlane.rowStride, "uvPixelStride" to uPlane.pixelStride ) image.close() reader.close() camera.close() cameraDevice = null Handler(context.mainLooper).post { result.success(data) stopBackgroundThread() } } catch (e: Exception) { image.close() reader.close() cleanup() Handler(context.mainLooper).post { result.error("PROCESS_ERROR", "画像データの処理に失敗: ${e.message}", null) } } }, backgroundHandler) // 本番キャプチャ実行 val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF) } session.capture(captureRequest.build(), null, backgroundHandler) } catch (e: Exception) { cleanup() imageReader.close() Handler(context.mainLooper).post { result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null) } } }, 1000) } catch (e: Exception) { cleanup() imageReader.close() Handler(context.mainLooper).post { result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null) } } } override fun onConfigureFailed(session: CameraCaptureSession) { cleanup() imageReader.close() Handler(context.mainLooper).post { result.error("SESSION_ERROR", "カメラセッションの設定に失敗", null) } } }, backgroundHandler ) } catch (e: Exception) { cleanup() imageReader.close() Handler(context.mainLooper).post { result.error("CAPTURE_ERROR", "キャプチャに失敗: ${e.message}", null) } } } override fun onDisconnected(camera: CameraDevice) { camera.close() cameraDevice = null imageReader.close() stopBackgroundThread() } override fun onError(camera: CameraDevice, error: Int) { camera.close() cameraDevice = null imageReader.close() Handler(context.mainLooper).post { result.error("CAMERA_ERROR", "カメラエラー: $error", null) stopBackgroundThread() } } }, backgroundHandler) } catch (e: SecurityException) { imageReader.close() returnError(result, "PERMISSION_ERROR", "カメラの権限がありません") } } private fun cleanup() { cameraDevice?.close() cameraDevice = null stopBackgroundThread() } private fun returnError(result: MethodChannel.Result, code: String, message: String) { result.error(code, message, null) stopBackgroundThread() } private fun startBackgroundThread() { backgroundThread = HandlerThread("RawCaptureThread").also { it.start() } backgroundHandler = Handler(backgroundThread!!.looper) } private fun stopBackgroundThread() { backgroundThread?.quitSafely() try { backgroundThread?.join(3000) } catch (_: InterruptedException) { } backgroundThread = null backgroundHandler = null } }