Newer
Older
MiniTias / android / app / src / main / kotlin / com / example / mini_tias / RawCapturePlugin.kt
package com.example.mini_tias

import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.graphics.Matrix
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)
            "scanFile" -> {
                val path = call.argument<String>("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<Int>("x") ?: 0
        val y = call.argument<Int>("y") ?: 0
        val cropWidth = call.argument<Int>("width") ?: 0
        val cropHeight = call.argument<Int>("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)
    }

    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
    }
}