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