package com.example.mini_tias
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.ImageFormat
import android.graphics.Matrix
import android.graphics.Rect
import android.graphics.YuvImage
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.Build
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.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.time.Instant
import java.util.zip.CRC32
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
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 = 16_666_666L
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 に変換し,YuvImage で JPEG 化して返す.
private fun convertYuvToJpeg(call: MethodCall, result: MethodChannel.Result) {
try {
val width = call.argument<Int>("width")!!
val height = call.argument<Int>("height")!!
val yPlane = call.argument<ByteArray>("yPlane")!!
val uPlane = call.argument<ByteArray>("uPlane")!!
val vPlane = call.argument<ByteArray>("vPlane")!!
val yRowStride = call.argument<Int>("yRowStride")!!
val uvRowStride = call.argument<Int>("uvRowStride")!!
val uvPixelStride = call.argument<Int>("uvPixelStride")!!
val rotation = call.argument<Int>("rotation") ?: 0
val quality = call.argument<Int>("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<Boolean>("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 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 = encodePngUncompressed(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)
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}")
// 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 = buildMetadataMap(
cameraId, characteristics, captureResult, image
)
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()
}
}
}
}
/// 撮影メタデータ Map を構築する.
private fun buildMetadataMap(
cameraId: String,
characteristics: CameraCharacteristics,
captureResult: TotalCaptureResult,
image: android.media.Image,
): Map<String, Any?> {
val settingsMap = mapOf<String, Any?>(
"control_mode" to "OFF",
"ae_mode" to "OFF",
"exposure_time_ns" to DEFAULT_EXPOSURE_TIME_NS,
"sensor_sensitivity_iso" to DEFAULT_SENSITIVITY,
"frame_duration_ns" to DEFAULT_FRAME_DURATION_NS,
"awb_mode" to "OFF",
"color_correction_gains" to listOf(1.0, 1.0, 1.0, 1.0),
"color_correction_transform" to "identity",
"noise_reduction_mode" to "OFF",
"edge_mode" to "OFF",
"tonemap_mode" to "CONTRAST_CURVE_LINEAR",
"shading_mode" to "HIGH_QUALITY",
"statistics_lens_shading_map_mode" to "ON",
"af_mode" to "OFF",
"black_level_lock" to true,
)
val actualMap = mapOf<String, Any?>(
"exposure_time_ns" to captureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME),
"sensor_sensitivity" to captureResult.get(CaptureResult.SENSOR_SENSITIVITY),
"frame_duration_ns" to captureResult.get(CaptureResult.SENSOR_FRAME_DURATION),
"sensor_timestamp" to captureResult.get(CaptureResult.SENSOR_TIMESTAMP),
"dynamic_black_level" to
captureResult.get(CaptureResult.SENSOR_DYNAMIC_BLACK_LEVEL)?.toList(),
"dynamic_white_level" to
captureResult.get(CaptureResult.SENSOR_DYNAMIC_WHITE_LEVEL),
"neutral_color_point" to
captureResult.get(CaptureResult.SENSOR_NEUTRAL_COLOR_POINT)?.map {
"${it.numerator}/${it.denominator}"
},
"color_gains" to captureResult.get(CaptureResult.COLOR_CORRECTION_GAINS)?.let {
listOf(
it.red.toDouble(),
it.greenEven.toDouble(),
it.greenOdd.toDouble(),
it.blue.toDouble()
)
},
"color_transform" to
captureResult.get(CaptureResult.COLOR_CORRECTION_TRANSFORM)?.let { transform ->
(0 until 3).map { row ->
(0 until 3).map { col ->
val r = transform.getElement(col, row)
"${r.numerator}/${r.denominator}"
}
}
},
)
val sensorCharacteristicsMap = mapOf<String, Any?>(
"color_filter_arrangement" to
characteristics.get(
CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
),
"black_level_pattern" to
characteristics.get(
CameraCharacteristics.SENSOR_BLACK_LEVEL_PATTERN
)?.toString(),
"white_level" to characteristics.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL),
"physical_size" to
characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)?.let {
listOf(it.width, it.height)
},
"pixel_array_size" to
characteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)?.let {
listOf(it.width, it.height)
},
"active_array_size" to
characteristics.get(
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE
)?.toString(),
)
return mapOf(
"captureTimestampUtc" to Instant.now().toString(),
"deviceModel" to Build.MODEL,
"androidVersion" to Build.VERSION.RELEASE,
"cameraId" to cameraId,
"hardwareLevel" to
characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
"captureMode" to "quantitative",
"imageFormat" to "RAW_SENSOR",
"imageSize" to listOf(image.width, image.height),
"settings" to settingsMap,
"actual" to actualMap,
"sensorCharacteristics" to sensorCharacteristicsMap,
)
}
/** フロントカメラの 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", "カメラの権限がありません")
}
}
/// Bitmap を非圧縮 PNG(Deflater.NO_COMPRESSION)としてエンコードする.
private fun encodePngUncompressed(bitmap: Bitmap): ByteArray {
val w = bitmap.width
val h = bitmap.height
val out = ByteArrayOutputStream(w * h * 3 + 1024)
// PNG シグネチャ
out.write(byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A))
// IHDR チャンク
val ihdrData = ByteBuffer.allocate(13)
.putInt(w)
.putInt(h)
.put(8) // bit depth
.put(2) // color type: RGB
.put(0) // compression method
.put(0) // filter method
.put(0) // interlace method
.array()
writeChunk(out, "IHDR", ihdrData)
// IDAT チャンク: 非圧縮 zlib でラップした RGB データ
val idatStream = ByteArrayOutputStream(w * h * 3 + h + 64)
val deflater = Deflater(Deflater.NO_COMPRESSION)
val deflaterOut = DeflaterOutputStream(idatStream, deflater, 65536)
val pixels = IntArray(w)
val rowBytes = ByteArray(1 + w * 3) // フィルタバイト(0) + RGB
rowBytes[0] = 0 // フィルタ: None
for (y in 0 until h) {
bitmap.getPixels(pixels, 0, w, 0, y, w, 1)
for (x in 0 until w) {
val pixel = pixels[x]
rowBytes[1 + x * 3] = ((pixel shr 16) and 0xFF).toByte() // R
rowBytes[1 + x * 3 + 1] = ((pixel shr 8) and 0xFF).toByte() // G
rowBytes[1 + x * 3 + 2] = (pixel and 0xFF).toByte() // B
}
deflaterOut.write(rowBytes)
}
deflaterOut.finish()
deflaterOut.close()
deflater.end()
writeChunk(out, "IDAT", idatStream.toByteArray())
// IEND チャンク
writeChunk(out, "IEND", byteArrayOf())
return out.toByteArray()
}
/// PNG チャンクを書き込む(length + type + data + CRC).
private fun writeChunk(out: ByteArrayOutputStream, type: String, data: ByteArray) {
val typeBytes = type.toByteArray(Charsets.US_ASCII)
// length (4 bytes, big-endian)
out.write(ByteBuffer.allocate(4).putInt(data.size).array())
// type (4 bytes)
out.write(typeBytes)
// data
out.write(data)
// CRC (type + data)
val crc = CRC32()
crc.update(typeBytes)
crc.update(data)
out.write(ByteBuffer.allocate(4).putInt(crc.value.toInt()).array())
}
/**
* フロントカメラの 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
}
}