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<String>("baseName")
val directoryPath = call.argument<String>("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<String>("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<Int>("width")!!,
height = call.argument<Int>("height")!!,
yPlane = call.argument<ByteArray>("yPlane")!!,
uPlane = call.argument<ByteArray>("uPlane")!!,
vPlane = call.argument<ByteArray>("vPlane")!!,
yRowStride = call.argument<Int>("yRowStride")!!,
uvRowStride = call.argument<Int>("uvRowStride")!!,
uvPixelStride = call.argument<Int>("uvPixelStride")!!,
rotation = call.argument<Int>("rotation") ?: 0,
mirror = call.argument<Boolean>("mirror") ?: false,
quality = call.argument<Int>("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<String, Any?>? = try {
ImageStatistics.compute(image)
} catch (e: Exception) {
Log.w("RawCapturePlugin", "Image statistics computation failed", e)
null
}
// Hot pixel map を CaptureResult から取得
val hotPixelMapData: Map<String, Any?> = 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<List<Int>>()),
)
} catch (e: Exception) {
Log.w("RawCapturePlugin", "Hot pixel map retrieval failed", e)
mapOf("mode" to "ON", "count" to 0,
"coordinates" to emptyList<List<Int>>())
}
Log.d("RawCapturePlugin",
"Image stats: $imageStatistics, hot pixels: ${hotPixelMapData["count"]}")
// LSC マップを取得
val lscMap =
captureResult.get(CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP)
val lscData: List<List<Float>>? = 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<String, Any?>(
"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<String>()
// yuvOutputSizes
val yuvOutputSizes = streamConfigMap
?.getOutputSizes(ImageFormat.YUV_420_888)
?.map { "${it.width}x${it.height}" }
?: emptyList<String>()
// jpegOutputSizes
val jpegOutputSizes = streamConfigMap
?.getOutputSizes(ImageFormat.JPEG)
?.map { "${it.width}x${it.height}" }
?: emptyList<String>()
val diagnostics = mapOf<String, Any?>(
"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
}
}