{"id":465,"date":"2025-10-25T01:32:39","date_gmt":"2025-10-24T17:32:39","guid":{"rendered":"https:\/\/yin.nnneri.me\/?p=465"},"modified":"2025-10-25T01:36:57","modified_gmt":"2025-10-24T17:36:57","slug":"android-camera%e5%ba%93%e6%89%93%e5%8c%85","status":"publish","type":"post","link":"https:\/\/yin.nnneri.me\/?p=465","title":{"rendered":"Android Camera\u5e93\u6253\u5305"},"content":{"rendered":"<h2>\u7b80\u4ecb<\/h2>\n<p>\u4e3a\u4e86\u5feb\u901f\u4f7f\u7528Android\u8bbe\u5907\u7684\u76f8\u673a \u6253\u5305\u4e86camera2\u5e93<br \/>\n\u8be5\u5e93\u53ea\u9700\u8981\u5f88\u5c11\u7684\u914d\u7f6e\u5373\u53ef\u83b7\u53d6\u76f8\u673a\u7684\u90e8\u5206\u91cd\u8981\u53c2\u6570\uff0c\u5e76\u542f\u7528\u5176\u4e2d\u4e00\u4e2a\u9009\u4e2d\u7684\u76f8\u673a<\/p>\n<h2>\u7528\u6cd5<\/h2>\n<ol>\n<li>\u4f7f\u7528<code>init(applicationContext)<\/code>\u83b7\u53d6\u4e0a\u4e0b\u6587<\/li>\n<li>\u4f7f\u7528<code>getCamerasList()<\/code>\u83b7\u53d6\u76f8\u673a\u53c2\u6570<\/li>\n<li>\u4f7f\u7528<code>setFrameCallback()<\/code>\u8bbe\u7f6e\u56de\u8c03<\/li>\n<li>\u4f7f\u7528<code>setCameraParameters()<\/code>\u9009\u62e9\u76ee\u6807\u76f8\u673a<\/li>\n<li>\u4f7f\u7528<code>closeCamera()<\/code>\u5173\u95ed\u76f8\u673a<\/li>\n<\/ol>\n<h2>\u9644\u4ef6<\/h2>\n<p>\u6e90\u7801\uff1a<\/p>\n<pre><code class=\"language-kotlin \">import android.Manifest\nimport android.content.Context\nimport android.graphics.ImageFormat\nimport android.hardware.camera2.CameraAccessException\nimport android.hardware.camera2.CameraCaptureSession\nimport android.hardware.camera2.CameraCharacteristics\nimport android.hardware.camera2.CameraDevice\nimport android.hardware.camera2.CameraManager\nimport android.hardware.camera2.CaptureRequest\nimport android.media.Image\nimport android.media.ImageReader\nimport android.media.MediaRecorder\nimport android.os.Handler\nimport android.os.HandlerThread\nimport android.util.Log\nimport android.util.Range\nimport android.util.Size\nimport androidx.annotation.RequiresPermission\n\n\/\/ \u5206\u8fa8\u7387\u4e0e\u5e27\u7387\u4fe1\u606f\u7c7b\ndata class ResInfo(\n    val width: Int,\n    val height: Int,\n    val fpsRanges: List&lt;Range&lt;Int&gt;&gt; \/\/ \u5e27\u7387\u8303\u56f4\u5217\u8868\uff08\u5982[30,30]\u6216[60,120]\uff09\n)\n\ndata class CameraInfo_t (\n    var id: Int = 0,\n    var focalLength: Float = 0.0f,\n    var equalsFocalLength: Float = 0.0f,\n    var resolutions: List&lt;ResInfo&gt; = emptyList()\n)\n\nobject CameraController {\n\n    private lateinit var applicationContext: Context\n\n    fun init(applicationContext: Context) {\n        this.applicationContext = applicationContext.applicationContext\n    }\n\n    private var CameraInfos: MutableList&lt;CameraInfo_t&gt; = mutableListOf()\n    private lateinit var cameraManager: CameraManager\n    private lateinit var cameraDevice: CameraDevice\n    private lateinit var imageReader: ImageReader\n\n    private lateinit var backgroundThread: HandlerThread\n    private lateinit var backgroundHandler: Handler\n\n    private lateinit var captureSession: CameraCaptureSession\n\n    private var frameCallback: FrameCallback? = null\n\n    private var fpsRange: Range&lt;Int&gt;? = null\n\n\n    fun interface FrameCallback {\n        fun onFrameAvailable(imageData: Image, width: Int, height: Int)\n    }\n\n    fun setFrameCallback(callback: FrameCallback) {\n        this.frameCallback = callback\n    }\n\n    \/\/ \u542f\u52a8\u540e\u53f0\u7ebf\u7a0b\n    private fun startBackgroundThread() {\n        Log.d(\"startBackgroundThread\",\"Active\")\n        backgroundThread = HandlerThread(\"CameraBackground\")\n        backgroundThread.start()\n        backgroundHandler = Handler(backgroundThread.looper)\n    }\n\n    \/\/ \u505c\u6b62\u540e\u53f0\u7ebf\u7a0b\n    private fun stopBackgroundThread() {\n        backgroundThread.quitSafely()\n        try {\n            backgroundThread.join()\n        } catch (e: InterruptedException) {\n            e.printStackTrace()\n        }\n    }\n\n    \/\/ \u83b7\u53d6camera\u4fe1\u606f\u5217\u8868\n    public fun getCamerasList():List&lt;CameraInfo_t&gt; {\n        cameraManager = applicationContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager\n        try {\n            val cameraIds = cameraManager.cameraIdList\n            for (cameraId in cameraIds) {\n                var cameraInfo = CameraInfo_t()\n                Log.d(\"ID\",cameraId)\n                cameraInfo.id = cameraId.toInt()\n                val characteristics = cameraManager.getCameraCharacteristics(cameraId)\n\n                \/\/ 1. \u83b7\u53d6\u6240\u6709\u652f\u6301\u7684\u5206\u8fa8\u7387\u53ca\u5e27\u7387\u8303\u56f4\n                val streamMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)\n                streamMap?.let { map -&gt;\n                    val videoSizes = map.getOutputSizes(MediaRecorder::class.java)\n                    cameraInfo.resolutions = videoSizes?.map { size -&gt;\n                        val fpsRanges = if (map.highSpeedVideoSizes.contains(size)) {\n                            \/\/ \u4ec5\u9ad8\u5e27\u7387\u5206\u8fa8\u7387\u53ef\u8c03\u7528\u6b64\u65b9\u6cd5\n                            map.getHighSpeedVideoFpsRangesFor(size)?.toList() ?: emptyList()\n                        } else {\n                            \/\/ \u666e\u901a\u5e27\u7387\uff1a\u83b7\u53d6\u6807\u51c6\u5e27\u7387\u8303\u56f4\n                            map.getOutputMinFrameDuration(MediaRecorder::class.java, size)?.let {\n                                val fps = (1_000_000_000.0 \/ it).toInt()\n                                listOf(Range(fps, fps)) \/\/ \u6784\u9020\u56fa\u5b9a\u5e27\u7387\u8303\u56f4\n                            } ?: emptyList()\n                        }\n                        ResInfo(size.width, size.height, fpsRanges)\n                    } ?: emptyList()\n                }\n\n                cameraInfo.resolutions.forEach { i -&gt;\n                    Log.d(\"CameraInfo\", \"\u5206\u8fa8\u7387: ${i.width} * ${i.height} \u5e27\u7387: ${i.fpsRanges} \" )\n                }\n\n\n                val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)\n                val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)\n\n                val diagonal35mm = 43.27f\n                val sensorDiagonal = kotlin.math.hypot(sensorSize!!.width, sensorSize.height)\n\n                focalLengths?.let { lengths -&gt;\n                    if(lengths.isNotEmpty()) {\n                        val mainFocalLength = lengths[0]\n                        val equivalentFocalLength = mainFocalLength * (diagonal35mm \/ sensorDiagonal)\n                        Log.d(\"CameraInfo\", \"\u76f8\u673a\u7126\u8ddd: $mainFocalLength mm, \u7b49\u6548\u7126\u8ddd: $equivalentFocalLength mm\")\n                        cameraInfo.focalLength = mainFocalLength\n                        cameraInfo.equalsFocalLength = equivalentFocalLength\n                    }\n                }\n                CameraInfos.add(cameraInfo)\n            }\n        } catch (e: CameraAccessException) {\n            e.printStackTrace()\n        }\n        return CameraInfos\n    }\n\n    \/\/\u8bbe\u7f6ecamera\u5e76\u542f\u52a8\n    @RequiresPermission(Manifest.permission.CAMERA)\n    public fun setCameraParameters(id:Int, width:Int, height:Int, fps:Int) {\n        val characteristics = cameraManager.getCameraCharacteristics(id.toString())\n        fpsRange = chooseOptimalFpsRange(\n            characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)\n                ?: arrayOf(),\n            fps\n        )\n        Log.d(\"fpsRange\",\"$fpsRange\")\n        try {\n            startBackgroundThread() \/\/\u542f\u52a8\u540e\u53f0\u8fdb\u7a0b\n            imageReader = ImageReader.newInstance( \/\/\u914d\u7f6eImageReader\n                width,\n                height,\n                ImageFormat.YUV_420_888,\n                2\n            ).apply {\n                setOnImageAvailableListener({ reader -&gt;\n                    \/\/Log.d(\"setOnImageAvailableListener\",\"Active\")\n                    val image = reader.acquireLatestImage()\n                    image?.let {\n                        val buffer = it\n                        frameCallback?.onFrameAvailable(buffer,it.width,it.height)\n                        it.close()\n                    }\n                }, backgroundHandler)\n            }\n            cameraManager!!.openCamera(\n                id.toString(),\n                object : CameraDevice.StateCallback() {\n                    override fun onOpened(camera: CameraDevice) {\n                        Log.d(\"cameraManager\",\"onOpened\")\n                        cameraDevice = camera\n                        startPreview(width,height,fps)\n                    }\n\n                    override fun onDisconnected(camera: CameraDevice) {\n                        Log.d(\"cameraManager\",\"onDisconnected\")\n                        closeCamera()\n                    }\n\n                    override fun onError(\n                        camera: CameraDevice,\n                        error: Int\n                    ) {\n                        Log.e(\"cameraManager\",\"onError:$error\")\n                        closeCamera()\n                    }\n                },\n                backgroundHandler\n            )\n        } catch (e:CameraAccessException) {\n            Log.e(\"setCameraParameters\",e.toString())\n        }\n    }\n\n    private fun startPreview(width:Int, height:Int, fps:Int) {\n        try {\n            val imageSurface = imageReader.surface\n            val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)\n            captureRequestBuilder.addTarget(imageSurface)\n            fpsRange?.let {\n                captureRequestBuilder.set(\n                    CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,\n                    it\n                )\n            }\n            cameraDevice.createCaptureSession(\n                listOf(imageSurface),\n                object : CameraCaptureSession.StateCallback() {\n                    override fun onConfigured(session: CameraCaptureSession) {\n                        captureSession = session\n                        updatePreview(captureRequestBuilder)\n\n                    }\n\n                    override fun onConfigureFailed(session: CameraCaptureSession) {\n                        \/\/TODO(\"Not yet implemented\")\n                    }\n\n                },\n                backgroundHandler\n            )\n        } catch (e:CameraAccessException) {\n            Log.e(\"startPreview\",e.toString())\n        }\n\n    }\n    private fun updatePreview(requestBuilder: CaptureRequest.Builder) {\n        try {\n            captureSession?.setRepeatingRequest(\n                requestBuilder.build(),\n                null,\n                backgroundHandler\n            )\n        } catch (e: CameraAccessException) {\n            e.printStackTrace()\n        }\n    }\n    \/\/\u5173\u95edcamera\n    public fun closeCamera() {\n        if (::cameraDevice.isInitialized) {\n            cameraDevice.close()\n            stopBackgroundThread()\n        }\n    }\n    private fun convertImageToByteArray(image: Image): ByteArray {\n        val planes = image.planes\n        val yBuffer = planes[0].buffer\n        val uBuffer = planes[1].buffer\n        val vBuffer = planes[2].buffer\n        val ySize = yBuffer.remaining()\n        val uSize = uBuffer.remaining()\n        val vSize = vBuffer.remaining()\n\n        val nv21 = ByteArray(ySize + uSize + vSize)\n        \/\/ \u590d\u5236 Y \u6570\u636e\n        yBuffer.get(nv21, 0, ySize)\n        \/\/ \u590d\u5236 UV \u6570\u636e\n        vBuffer.get(nv21, ySize, vSize)\n        uBuffer.get(nv21, ySize + vSize, uSize)\n\n        return nv21\n    }\n    \/\/ \u9009\u62e9\u6700\u4f73\u5c3a\u5bf8\n    private fun chooseOptimalSize(\n        choices: Array&lt;Size&gt;,\n        width: Int,\n        height: Int\n    ): Size {\n        val desiredRatio = width.toFloat() \/ height\n        var optimalSize = choices[0]\n        var minDiff = Float.MAX_VALUE\n\n        for (size in choices) {\n            val ratio = size.width.toFloat() \/ size.height\n            val diff = Math.abs(ratio - desiredRatio)\n            if (diff &lt; minDiff) {\n                optimalSize = size\n                minDiff = diff\n            }\n        }\n\n        return optimalSize\n    }\n\n    \/\/ \u9009\u62e9\u6700\u4f73 FPS \u8303\u56f4\n    private fun chooseOptimalFpsRange(\n        ranges: Array&lt;Range&lt;Int&gt;&gt;,\n        desiredFps: Int\n    ): Range&lt;Int&gt;? {\n        if (ranges.isEmpty()) return null\n\n        \/\/ \u627e\u5230\u6700\u63a5\u8fd1 desiredFps \u7684\u8303\u56f4\n        var optimalRange: Range&lt;Int&gt;? = null\n        var minDiff = Int.MAX_VALUE\n\n        for (range in ranges) {\n            \/\/ \u68c0\u67e5\u8303\u56f4\u662f\u5426\u5305\u542b desiredFps\n            if (range.lower &lt;= desiredFps &amp;&amp; range.upper &gt;= desiredFps) {\n                \/\/ \u8ba1\u7b97\u4e0e desiredFps \u7684\u5dee\u5f02\n                val diff = (range.upper - desiredFps) + (desiredFps - range.lower)\n                if (diff &lt; minDiff) {\n                    minDiff = diff\n                    optimalRange = range\n                }\n            }\n        }\n\n        \/\/ \u5982\u679c\u6ca1\u6709\u627e\u5230\u5305\u542b desiredFps \u7684\u8303\u56f4\uff0c\u8fd4\u56de\u6700\u5927\u8303\u56f4\n        return optimalRange ?: ranges.maxByOrNull { it.upper }\n    }\n}\n\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>\u4e3a\u4e86\u5feb\u901f\u4f7f\u7528Android\u8bbe\u5907\u7684\u76f8\u673a \u6253\u5305\u4e86camera2\u5e93<br \/>\n\u8be5\u5e93\u53ea\u9700\u8981\u5f88\u5c11\u7684\u914d\u7f6e\u5373\u53ef\u83b7\u53d6\u76f8\u673a\u7684\u90e8\u5206\u91cd\u8981\u53c2\u6570\uff0c\u5e76\u542f\u7528\u5176\u4e2d\u4e00\u4e2a\u9009\u4e2d\u7684\u76f8\u673a<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"emotion":"","emotion_color":"","title_style":"","license":"","footnotes":""},"categories":[3],"tags":[],"class_list":["post-465","post","type-post","status-publish","format-standard","hentry","category-code"],"_links":{"self":[{"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=\/wp\/v2\/posts\/465","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=465"}],"version-history":[{"count":3,"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=\/wp\/v2\/posts\/465\/revisions"}],"predecessor-version":[{"id":468,"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=\/wp\/v2\/posts\/465\/revisions\/468"}],"wp:attachment":[{"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=465"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=465"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/yin.nnneri.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=465"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}