package com.example.mini_tias import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.ImageFormat import android.hardware.camera2.* import android.hardware.camera2.params.ColorSpaceTransform import android.hardware.camera2.params.RggbChannelVector import android.hardware.camera2.params.TonemapCurve import android.media.ImageReader import android.media.MediaActionSound import android.media.MediaScannerConnection import android.media.RingtoneManager import android.os.Handler import android.os.HandlerThread import android.util.Log import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.io.File import java.io.FileOutputStream import kotlin.math.roundToInt /** フレーム取得後に処理するコールバックの型エイリアス.*/ private typealias FrameProcessor = ( yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray, width: Int, height: Int, yRowStride: Int, uvRowStride: Int, uvPixelStride: Int, reader: ImageReader, camera: CameraDevice, result: MethodChannel.Result ) -> Unit /** * 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 private val mediaActionSound: MediaActionSound by lazy { MediaActionSound() } private companion object { const val DEFAULT_EXPOSURE_TIME_NS = 8_333_333L // 1/120s const val DEFAULT_SENSITIVITY = 40 const val DEFAULT_FRAME_DURATION_NS = 33_333_333L const val RAW_WIDTH = 3264 const val RAW_HEIGHT = 2448 const val FRAMES_TO_DISCARD = 2 } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "captureFullResolutionPng" -> captureFullResolutionPng(result) "captureQuantitativeDng" -> { val baseName = call.argument("baseName") val directoryPath = call.argument("directoryPath") if (baseName == null || directoryPath == null) { result.error("INVALID_ARG", "baseName と directoryPath が必要です", null) } else { captureQuantitativeDng(baseName, directoryPath, 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) } } "playShutterSound" -> { try { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) result.success(null) } catch (e: Exception) { result.error("SOUND_ERROR", "シャッター音の再生に失敗: ${e.message}", null) } } "playSaveCompleteSound" -> { try { val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val ringtone = RingtoneManager.getRingtone(context, notification) ringtone?.play() result.success(null) } catch (e: Exception) { result.error("SOUND_ERROR", "保存完了音の再生に失敗: ${e.message}", null) } } "getCameraDiagnostics" -> getCameraDiagnostics(result) else -> result.notImplemented() } } /// YUV_420_888 を NV21 経由で JPEG 化して返す(実処理は [YuvJpegConverter]). private fun convertYuvToJpeg(call: MethodCall, result: MethodChannel.Result) { try { val jpegBytes = YuvJpegConverter.convert( width = call.argument("width")!!, height = call.argument("height")!!, yPlane = call.argument("yPlane")!!, uPlane = call.argument("uPlane")!!, vPlane = call.argument("vPlane")!!, yRowStride = call.argument("yRowStride")!!, uvRowStride = call.argument("uvRowStride")!!, uvPixelStride = call.argument("uvPixelStride")!!, rotation = call.argument("rotation") ?: 0, mirror = call.argument("mirror") ?: false, quality = call.argument("quality") ?: 85, ) result.success(jpegBytes) } catch (e: Exception) { result.error("CONVERT_ERROR", "YUV to JPEG 変換に失敗: ${e.message}", null) } } private fun captureFullResolutionPng(result: MethodChannel.Result) { openCameraAndCapture(result) { yBytes, uBytes, vBytes, width, height, yRowStride, uvRowStride, uvPixelStride, reader, camera, res -> // YUV → RGB 変換 + 90° 時計回り回転を統合 // 元の(x,y) → 回転後の(height-1-y, x) // 出力サイズ: width_out=height, height_out=width val rotatedPixels = IntArray(width * height) for (y in 0 until height) { for (x in 0 until width) { val yIndex = y * yRowStride + x val uvIndex = (y / 2) * uvRowStride + (x / 2) * uvPixelStride val yValue = (yBytes[yIndex].toInt() and 0xFF).toDouble() val uValue = (uBytes[uvIndex].toInt() and 0xFF).toDouble() val vValue = (vBytes[uvIndex].toInt() and 0xFF).toDouble() val r = (yValue + 1.402 * (vValue - 128)).roundToInt().coerceIn(0, 255) val g = (yValue - 0.344 * (uValue - 128) - 0.714 * (vValue - 128)).roundToInt().coerceIn(0, 255) val b = (yValue + 1.772 * (uValue - 128)).roundToInt().coerceIn(0, 255) rotatedPixels[x * height + (height - 1 - y)] = Color.rgb(r, g, b) } } val rotatedWidth = height val rotatedHeight = width val bitmap = Bitmap.createBitmap(rotatedWidth, rotatedHeight, Bitmap.Config.ARGB_8888) bitmap.setPixels(rotatedPixels, 0, rotatedWidth, 0, 0, rotatedWidth, rotatedHeight) // 非圧縮 PNG エンコード(Deflater.NO_COMPRESSION で高速化) val pngBytes = PngEncoder.encodeUncompressed(bitmap) bitmap.recycle() reader.close() camera.close() cameraDevice = null Handler(context.mainLooper).post { res.success(pngBytes) stopBackgroundThread() } } } /** * 定量モード撮影.RAW_SENSOR (DNG) で保存し,メタデータと LSC マップを返す. * * CONTROL_MODE=OFF など完全マニュアル制御で連続 3 フレームを投げ, * 最初の 2 フレーム(設定反映待ち)を捨てて 3 枚目を DNG 化する. * DNG ファイルは Kotlin 側で [directoryPath]/[baseName].dng に直接書き込む. */ private fun captureQuantitativeDng( baseName: String, directoryPath: String, result: MethodChannel.Result, ) { startBackgroundThread() val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraId = findFrontCameraId(cameraManager) if (cameraId == null) { returnError(result, "NO_CAMERA", "フロントカメラが見つかりません") return } val characteristics = cameraManager.getCameraCharacteristics(cameraId) // RAW_SENSOR サポート確認 val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) val supportsRaw = capabilities?.contains( CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW ) == true if (!supportsRaw) { returnError(result, "NOT_SUPPORTED", "このカメラは RAW_SENSOR をサポートしていません") return } val imageReader = ImageReader.newInstance( RAW_WIDTH, RAW_HEIGHT, ImageFormat.RAW_SENSOR, 3 ) 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 { openCameraAndCaptureRaw( camera, session, imageReader, characteristics, cameraId, baseName, directoryPath, result, { sent -> resultSent = sent } ) } catch (e: Exception) { if (!resultSent) { resultSent = true cleanup() imageReader.close() Handler(context.mainLooper).post { result.error( "CAPTURE_ERROR", "定量キャプチャに失敗: ${e.message}", null ) } } } } override fun onConfigureFailed(session: CameraCaptureSession) { if (!resultSent) { resultSent = true cleanup() imageReader.close() Handler(context.mainLooper).post { result.error( "SESSION_ERROR", "カメラセッションの設定に失敗", null ) } } } }, backgroundHandler ) } catch (e: Exception) { if (!resultSent) { resultSent = true 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() if (!resultSent) { resultSent = true Handler(context.mainLooper).post { result.error("CAMERA_ERROR", "カメラエラー: $error", null) stopBackgroundThread() } } } }, backgroundHandler) } catch (e: SecurityException) { imageReader.close() returnError(result, "PERMISSION_ERROR", "カメラの権限がありません") } } /** * 定量モード RAW キャプチャの実処理. * * session が onConfigured 後に呼ばれる.FRAMES_TO_DISCARD 枚を捨てた後, * 3 枚目の Image と TotalCaptureResult を DngCreator に渡して DNG バイト列を生成する. */ private fun openCameraAndCaptureRaw( camera: CameraDevice, session: CameraCaptureSession, imageReader: ImageReader, characteristics: CameraCharacteristics, cameraId: String, baseName: String, directoryPath: String, result: MethodChannel.Result, setResultSent: (Boolean) -> Unit, ) { var capturedImage: android.media.Image? = null var captureResult: TotalCaptureResult? = null // 画像到着カウント(ImageAvailableListener 側) var imageArrivalCount = 0 // CaptureResult 到着カウント(CaptureCallback 側) var resultArrivalCount = 0 var resultSentFlag = false fun sendResultSafe(action: () -> Unit) { if (!resultSentFlag) { resultSentFlag = true setResultSent(true) action() } } // 完全マニュアル制御の CaptureRequest を構築 val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF) set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF) set(CaptureRequest.SENSOR_EXPOSURE_TIME, DEFAULT_EXPOSURE_TIME_NS) set(CaptureRequest.SENSOR_SENSITIVITY, DEFAULT_SENSITIVITY) set(CaptureRequest.SENSOR_FRAME_DURATION, DEFAULT_FRAME_DURATION_NS) set(CaptureRequest.BLACK_LEVEL_LOCK, true) set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF) set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX) set(CaptureRequest.COLOR_CORRECTION_GAINS, RggbChannelVector(1.0f, 1.0f, 1.0f, 1.0f)) // 単位行列(3×3,Rational 型) set( CaptureRequest.COLOR_CORRECTION_TRANSFORM, ColorSpaceTransform( intArrayOf( 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, ) ) ) set(CaptureRequest.NOISE_REDUCTION_MODE, CaptureRequest.NOISE_REDUCTION_MODE_OFF) set(CaptureRequest.EDGE_MODE, CaptureRequest.EDGE_MODE_OFF) set(CaptureRequest.TONEMAP_MODE, CaptureRequest.TONEMAP_MODE_CONTRAST_CURVE) // 線形トーンマップカーブ(各チャネル: (0,0)→(1,1)) set( CaptureRequest.TONEMAP_CURVE, TonemapCurve( floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f), floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f), floatArrayOf(0.0f, 0.0f, 1.0f, 1.0f), ) ) set(CaptureRequest.SHADING_MODE, CaptureRequest.SHADING_MODE_HIGH_QUALITY) set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE, CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON) // Hot pixel map を有効化(STATISTICS_HOT_PIXEL_MAP_MODE は Boolean 型) set(CaptureRequest.STATISTICS_HOT_PIXEL_MAP_MODE, true) set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF) set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF) set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF) }.build() val captureCallback = object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, totalResult: TotalCaptureResult, ) { resultArrivalCount++ // FRAMES_TO_DISCARD + 1 枚目(最後)だけ CaptureResult を保持 if (resultArrivalCount == FRAMES_TO_DISCARD + 1) { captureResult = totalResult tryProcessRaw( capturedImage, captureResult, imageReader, camera, characteristics, cameraId, baseName, directoryPath, result, ::sendResultSafe ) } } override fun onCaptureFailed( session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure, ) { sendResultSafe { capturedImage?.close() imageReader.close() cleanup() Handler(context.mainLooper).post { result.error("CAPTURE_FAILED", "キャプチャに失敗しました: ${failure.reason}", null) } } } } imageReader.setOnImageAvailableListener({ reader -> val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener imageArrivalCount++ // 最初の FRAMES_TO_DISCARD 枚は捨てる(設定反映待ち) if (imageArrivalCount <= FRAMES_TO_DISCARD) { image.close() return@setOnImageAvailableListener } // FRAMES_TO_DISCARD + 1 枚目だけ保持 if (capturedImage == null) { capturedImage = image tryProcessRaw( capturedImage, captureResult, imageReader, camera, characteristics, cameraId, baseName, directoryPath, result, ::sendResultSafe ) } else { image.close() } }, backgroundHandler) // 連続 3 回キャプチャを投げる repeat(FRAMES_TO_DISCARD + 1) { session.capture(captureRequest, captureCallback, backgroundHandler) } } /** * 画像と CaptureResult の両方が揃ったら DNG を生成して result に返す. * * 片方がまだない場合は何もしない(もう一方の到着を待つ). */ private fun tryProcessRaw( image: android.media.Image?, captureResult: TotalCaptureResult?, imageReader: ImageReader, camera: CameraDevice, characteristics: CameraCharacteristics, cameraId: String, baseName: String, directoryPath: String, result: MethodChannel.Result, sendResultSafe: (() -> Unit) -> Unit, ) { if (image == null || captureResult == null) return sendResultSafe { try { // DNG をファイルに直接書き込む(MethodChannel 越しの巨大バイト転送を回避) val dir = File(directoryPath) if (!dir.exists()) dir.mkdirs() val dngFile = File(dir, "$baseName.dng") FileOutputStream(dngFile).use { fos -> DngCreator(characteristics, captureResult).use { dngCreator -> dngCreator.writeImage(fos, image) } } Log.d("RawCapturePlugin", "DNG written: ${dngFile.length()} bytes to ${dngFile.absolutePath}") // 画素統計を計算(DNG 書き込み後・image.close() 前) val imageStatistics: Map? = try { ImageStatistics.compute(image) } catch (e: Exception) { Log.w("RawCapturePlugin", "Image statistics computation failed", e) null } // Hot pixel map を CaptureResult から取得 val hotPixelMapData: Map = try { val hotPixelArray = captureResult.get(CaptureResult.STATISTICS_HOT_PIXEL_MAP) mapOf( "mode" to "ON", "count" to (hotPixelArray?.size ?: 0), "coordinates" to (hotPixelArray?.map { listOf(it.x, it.y) } ?: emptyList>()), ) } catch (e: Exception) { Log.w("RawCapturePlugin", "Hot pixel map retrieval failed", e) mapOf("mode" to "ON", "count" to 0, "coordinates" to emptyList>()) } Log.d("RawCapturePlugin", "Image stats: $imageStatistics, hot pixels: ${hotPixelMapData["count"]}") // LSC マップを取得 val lscMap = captureResult.get(CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP) val lscData: List>? = lscMap?.let { map -> val rows = map.rowCount val cols = map.columnCount val gainFactors = FloatArray(rows * cols * 4) map.copyGainFactors(gainFactors, 0) (0 until rows).map { row -> gainFactors.slice(row * cols * 4 until (row + 1) * cols * 4) } } // メタデータを構築 val metadataMap = CaptureMetadata.build( cameraId = cameraId, characteristics = characteristics, captureResult = captureResult, image = image, exposureTimeNs = DEFAULT_EXPOSURE_TIME_NS, sensitivity = DEFAULT_SENSITIVITY, frameDurationNs = DEFAULT_FRAME_DURATION_NS, imageStatistics = imageStatistics, hotPixelMap = hotPixelMapData, ) image.close() imageReader.close() camera.close() cameraDevice = null val resultMap = mapOf( "dngPath" to dngFile.absolutePath, "dngFileSize" to dngFile.length(), "metadata" to metadataMap, "lscMap" to lscData, "lscMapRowCount" to lscMap?.rowCount, "lscMapColumnCount" to lscMap?.columnCount, ) Handler(context.mainLooper).post { result.success(resultMap) stopBackgroundThread() } } catch (e: Exception) { image.close() imageReader.close() cleanup() Handler(context.mainLooper).post { result.error("DNG_ERROR", "DNG 生成に失敗: ${e.message}", null) stopBackgroundThread() } } } } /** フロントカメラの ID を返す.見つからない場合は null を返す.*/ private fun findFrontCameraId(cameraManager: CameraManager): String? = cameraManager.cameraIdList.firstOrNull { id -> val characteristics = cameraManager.getCameraCharacteristics(id) characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT } /** * フロントカメラを開いてフル解像度の YUV フレームを 1 枚取得し,[onFrame] を呼び出す. * * カメラのオープン・セッション作成・AE/AF 安定待機(1 秒)・本番キャプチャの * 共通フローを担う.フレーム取得後の処理は [onFrame] で差異化する. * [onFrame] 内で reader と camera のクローズおよび result への応答を行うこと. */ private fun openCameraAndCapture( result: MethodChannel.Result, onFrame: FrameProcessor, ) { startBackgroundThread() val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraId = findFrontCameraId(cameraManager) 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 width = image.width val height = image.height 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 yRowStride = yPlane.rowStride val uvRowStride = uPlane.rowStride val uvPixelStride = uPlane.pixelStride image.close() onFrame( yBytes, uBytes, vBytes, width, height, yRowStride, uvRowStride, uvPixelStride, reader, camera, result ) } 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", "カメラの権限がありません") } } /** * フロントカメラの Camera2 API 能力を読み出して Map で返す. * * CameraCharacteristics から各種パラメータを取得し,null セーフに処理する. * フロントカメラが見つからない場合は error を返す. */ private fun getCameraDiagnostics(result: MethodChannel.Result) { val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraId = findFrontCameraId(cameraManager) if (cameraId == null) { result.error("NO_CAMERA", "フロントカメラが見つかりません", null) return } val c = cameraManager.getCameraCharacteristics(cameraId) val streamConfigMap = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) // rawOutputSizes val rawOutputSizes = streamConfigMap ?.getOutputSizes(ImageFormat.RAW_SENSOR) ?.map { "${it.width}x${it.height}" } ?: emptyList() // yuvOutputSizes val yuvOutputSizes = streamConfigMap ?.getOutputSizes(ImageFormat.YUV_420_888) ?.map { "${it.width}x${it.height}" } ?: emptyList() // jpegOutputSizes val jpegOutputSizes = streamConfigMap ?.getOutputSizes(ImageFormat.JPEG) ?.map { "${it.width}x${it.height}" } ?: emptyList() val diagnostics = mapOf( "cameraId" to cameraId, "hardwareLevel" to c.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL), "capabilities" to c.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)?.toList(), "exposureTimeRangeNs" to c.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE) ?.let { listOf(it.lower, it.upper) }, "sensitivityRange" to c.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) ?.let { listOf(it.lower, it.upper) }, "lscMapSize" to null, // 実キャプチャ後に CaptureResult から取得する必要があるため診断段階では null "availableLensShadingMapModes" to c.get( CameraCharacteristics.STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES )?.map { it }, "availableNoiseReductionModes" to c.get( CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES )?.toList(), "availableEdgeModes" to c.get(CameraCharacteristics.EDGE_AVAILABLE_EDGE_MODES)?.toList(), "availableTonemapModes" to c.get( CameraCharacteristics.TONEMAP_AVAILABLE_TONE_MAP_MODES )?.toList(), "availableShadingModes" to c.get(CameraCharacteristics.SHADING_AVAILABLE_MODES)?.toList(), "availableAwbModes" to c.get( CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES )?.toList(), "availableAfModes" to c.get( CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES )?.toList(), "minFocusDistance" to c.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE), "hyperfocalDistance" to c.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE), "rawOutputSizes" to rawOutputSizes, "yuvOutputSizes" to yuvOutputSizes, "jpegOutputSizes" to jpegOutputSizes, "colorFilterArrangement" to c.get( CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT ), "blackLevelPattern" to c.get(CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN)?.toString(), "whiteLevel" to c.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL), "physicalSize" to c.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE) ?.let { listOf(it.width, it.height) }, "pixelArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE) ?.let { listOf(it.width, it.height) }, "activeArraySize" to c.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)?.toString(), ) result.success(diagnostics) } 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 } }