package com.example.mini_tias import android.app.Activity import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageFormat import android.graphics.Matrix import android.graphics.Rect import android.graphics.YuvImage import android.hardware.camera2.* import android.media.ImageReader import android.media.MediaScannerConnection import android.os.Handler import android.os.HandlerThread import android.view.PixelCopy import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.io.ByteArrayOutputStream /** * Camera2 API を使用してフロントカメラから YUV_420_888 フォーマットで * フル解像度の画像を 1 フレームキャプチャする. */ class RawCapturePlugin(private val activity: Activity) : MethodChannel.MethodCallHandler { private val context: Context = activity.applicationContext 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) "capturePreviewFrame" -> capturePreviewFrame(call, result) "convertYuvToJpeg" -> convertYuvToJpeg(call, 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() } } /// PixelCopy で全画面をキャプチャし,指定領域を切り取って 180° 回転して返す. private fun capturePreviewFrame(call: MethodCall, result: MethodChannel.Result) { val x = call.argument("x") ?: 0 val y = call.argument("y") ?: 0 val cropWidth = call.argument("width") ?: 0 val cropHeight = call.argument("height") ?: 0 val window = activity.window val view = window.decorView val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) val handler = Handler(activity.mainLooper) PixelCopy.request(window, bitmap, { copyResult -> if (copyResult == PixelCopy.SUCCESS) { try { val output = if (cropWidth > 0 && cropHeight > 0) { // Dart 側で回転済み左上座標を計算済み val cx = x.coerceIn(0, bitmap.width - 1) val cy = y.coerceIn(0, bitmap.height - 1) val cw = cropWidth.coerceAtMost(bitmap.width - cx) val ch = cropHeight.coerceAtMost(bitmap.height - cy) // 切り取り val cropped = Bitmap.createBitmap(bitmap, cx, cy, cw, ch) bitmap.recycle() // 180° 回転(app.dart の回転を相殺) val matrix = Matrix() matrix.postRotate(180f) val rotated = Bitmap.createBitmap(cropped, 0, 0, cropped.width, cropped.height, matrix, true) cropped.recycle() rotated } else { bitmap } val stream = ByteArrayOutputStream() output.compress(Bitmap.CompressFormat.JPEG, 85, stream) output.recycle() result.success(stream.toByteArray()) } catch (e: Exception) { bitmap.recycle() result.error("PROCESS_ERROR", "スナップショット処理に失敗: ${e.message}", null) } } else { bitmap.recycle() result.error("PIXEL_COPY_FAILED", "PixelCopy に失敗: $copyResult", null) } }, handler) } /// YUV_420_888 を NV21 に変換し,YuvImage で JPEG 化して返す. private fun convertYuvToJpeg(call: MethodCall, result: MethodChannel.Result) { try { val width = call.argument("width")!! val height = call.argument("height")!! val yPlane = call.argument("yPlane")!! val uPlane = call.argument("uPlane")!! val vPlane = call.argument("vPlane")!! val yRowStride = call.argument("yRowStride")!! val uvRowStride = call.argument("uvRowStride")!! val uvPixelStride = call.argument("uvPixelStride")!! val rotation = call.argument("rotation") ?: 0 val quality = call.argument("quality") ?: 85 // YUV_420_888 → NV21 変換 val nv21 = ByteArray(width * height * 3 / 2) // Y プレーンをコピー for (row in 0 until height) { System.arraycopy(yPlane, row * yRowStride, nv21, row * width, width) } // UV プレーンを NV21 形式(VUVU...)にインターリーブ val uvOffset = width * height for (row in 0 until height / 2) { for (col in 0 until width / 2) { val uvIndex = row * uvRowStride + col * uvPixelStride nv21[uvOffset + row * width + col * 2] = vPlane[uvIndex] nv21[uvOffset + row * width + col * 2 + 1] = uPlane[uvIndex] } } // NV21 → JPEG val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null) val jpegStream = ByteArrayOutputStream() yuvImage.compressToJpeg(Rect(0, 0, width, height), quality, jpegStream) // 回転・反転が必要な場合 val mirror = call.argument("mirror") ?: false val jpegBytes = if (rotation != 0 || mirror) { val bitmap = BitmapFactory.decodeByteArray(jpegStream.toByteArray(), 0, jpegStream.size()) val matrix = Matrix() if (rotation != 0) matrix.postRotate(rotation.toFloat()) if (mirror) matrix.postScale(-1f, 1f) val transformed = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) bitmap.recycle() val outStream = ByteArrayOutputStream() transformed.compress(Bitmap.CompressFormat.JPEG, quality, outStream) transformed.recycle() outStream.toByteArray() } else { jpegStream.toByteArray() } result.success(jpegBytes) } catch (e: Exception) { result.error("CONVERT_ERROR", "YUV to JPEG 変換に失敗: ${e.message}", null) } } 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 ) var resultSent = false 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 -> if (resultSent) return@setOnImageAvailableListener val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener resultSent = true 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 } }