Android CameraX学习记录(二) kotlin mjepg服务

获取相机原数据

通过ImageAnalysis[ 图片分析 ]来获取相机的缓冲数据

// 创建图像分析
val imageAnalysis = ImageAnalysis.Builder()
   // 设置输出图像格式
   .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
   // 设置策略 只保持最新
   .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
   // 设置启用输出图像旋转
   .setOutputImageRotationEnabled(true)
   // 设置旋转角度
   // .setTargetRotation(Surface.ROTATION_0)
   //  设置图像纵横比
   .setTargetAspectRatio(AspectRatio.RATIO_16_9)
   //  设置此配置中预期目标的分辨率。目标分辨率尝试为图像分辨率建立最小界限。
   //  实际图像分辨率将是大小中最接近的可用分辨率,不小于目标分辨率,由相机实现确定。
   //  但是,如果不存在等于或大于目标分辨率的分辨率,则将选择小于目标分辨率的最接近的可用分辨率。
// .setTargetResolution(Size(640, 480))
   .build()
val cameraExecutor = Executors.newSingleThreadExecutor()
imageAnalysis.setAnalyzer(cameraExecutor,
   ImageAnalysis.Analyzer { imageProxy ->
   		// 返回的图像数据
        imageProxy.close()
   })

imageAnalysis绑定到相机上

// 将用例绑定到相机 Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageAnalysis)

MJPEG

MJPEG(Motion Joint Photographic Experts Group)是视频压缩格式,其中每一帧图像都分别使用JPEG编码,不使用帧间编码,压缩率通常在20:1-50:1范围内。【百度百科

由百度百科得知MJPEG的每一帧是jpeg编码的图像,因此咋们需要将imageAnalysis输出的YUV_420_888格式转为JPEG,jpeg编码请看下一节。

MJPEG 协议简介

mjpeg服务端是基于Http/1.1协议的长连接实现的视频传输协议
浏览器抓包得知响应头的Content-Type为:
multipart/x-mixed-replace;boundary=***

注意:boundary的值必须和mjpeg流里面的 长连接响应头“--”后的字符串相同,具体参考下面mjpeg协议结构

mjpeg协议结构:

http长连接格式
http响应头:
	Content-Type:multipart/x-mixed-replace;boundary=mjpeg
http响应体:
	长连接响应头:
		\r\n(换行符)
		--mjpeg
		\r\n(换行符)
		Content-Type: image/jpeg
		Content-Length:  204800
		\r\n(换行符)
		\r\n(换行符)
	长连接响应体:
   		(JPEG的byte数据)
--mjpeg							长连接分隔符
Content-Type: image/jpeg		长连接消息体格式
Content-Length:  204800			长连接消息体长度

生成mjpeg的代码如下,更详细业务代码请参考下面 jpeg编码 部分代码

// 输出的jpeg byte数组
 val buffer: ByteArray = bos.toByteArray()
 // 长连接消息头
 val head = ("\r\n--androidMjpeg\r\n" +
         "Content-Type: image/jpeg\r\nContent-Length: ${buffer.size}\r\n\r\n").toByteArray()
 // 创建一个byte数组保存拼接 长连接消息头 和 jpeg byte数组
 var outByte: ByteArray = ByteArray(head.size+buffer.size)
 for (i in head.indices){
     outByte[i] = head[i]
 }
 for (i in buffer.indices){
     outByte[i+head.size] = buffer[i]
 }
 // 将这个长连接的消息头和jpeg祖成的消息体添加到ByteBufferList中
 val bflist = ByteBufferList()
 bflist.add(ByteBuffer.wrap(outByte))

JPEG编码

调用android.graphics.YuvImage函数可以实现将相机原始数据编码为jpeg格式。

但是YuvImage函数只能输入YUY2NV21格式的数据,但是咋们cameraX的ImageAnalysis输出的是YUV_420_888,因此我们需要将YUY420转为NV21

YUY420转NV21

创建工具类ImageUtils

object ImageUtils {
    /**
     * 将来自 CameraX API 的 YUV_420_888 图像转换为NV21 的 ByteBuffer。
     */
    @RequiresApi(VERSION_CODES.LOLLIPOP)
    @ExperimentalGetImage
    fun getNv21ByteBuffer(imageProxy: ImageProxy): ByteBuffer? {
        if (imageProxy.image == null) return null
        return yuv420ThreePlanesToNV21(imageProxy.image!!.planes, imageProxy.width, imageProxy.height)
    }

    /**
     * YUV_420_888格式转换成NV21.
     *
     * NV21 格式由一个包含 Y、U 和 V 值的单字节数组组成。
     * 对于大小为 S 的图像,数组的前 S 个位置包含所有 Y 值。其余位置包含交错的 V 和 U 值。
     * U 和 V 在两个维度上都进行了 2 倍的二次采样,因此有 S/4 U 值和 S/4 V 值。
     * 总之,NV21 数组将包含 S 个 Y 值,后跟 S/4 + S/4 VU 值: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
     *
     * YUV_420_888 是一种通用格式,可以描述任何 YUV 图像,其中 U 和 V 在两个维度上都以 2 倍的因子进行二次采样。
     * [Image.getPlanes] 返回一个包含 Y、U 和 V 平面的数组
     * Y 平面保证不会交错,因此我们可以将其值复制到 NV21 数组的第一部分。U 和 V 平面可能已经具有 NV21 格式的表示。
     * 如果平面共享相同的缓冲区,则会发生这种情况,V 缓冲区位于 U 缓冲区之前的一个位置,并且平面的 pixelStride 为 2。
     * 如果是这种情况,我们可以将它们复制到 NV21 阵列中。
     */
    @RequiresApi(VERSION_CODES.KITKAT)
    private fun yuv420ThreePlanesToNV21(
        yuv420888planes: Array<Plane>, width: Int, height: Int
    ): ByteBuffer {
        val imageSize = width * height
        val out = ByteArray(imageSize + 2 * (imageSize / 4))
        if (areUVPlanesNV21(yuv420888planes, width, height)) {
            // 复制 Y 的值
            yuv420888planes[0].buffer[out, 0, imageSize]
            // 从 V 缓冲区获取第一个 V 值,因为 U 缓冲区不包含它。
            yuv420888planes[2].buffer[out, imageSize, 1]
            // 从 U 缓冲区复制第一个 U 值和剩余的 VU 值。
            yuv420888planes[1].buffer[out, imageSize + 1, 2 * imageSize / 4 - 1]
        } else {
            // 回退到一个一个地复制 UV 值,这更慢但也有效。
            // 取 Y.
            unpackPlane(yuv420888planes[0], width, height, out, 0, 1)
            // 取 U.
            unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2)
            // 取 V.
            unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2)
        }
        return ByteBuffer.wrap(out)
    }

    /**
     * 检查 YUV_420_888 图像的 UV 平面缓冲区是否为 NV21 格式。
     */
    @RequiresApi(VERSION_CODES.KITKAT)
    private fun areUVPlanesNV21(planes: Array<Plane>, width: Int, height: Int): Boolean {
        val imageSize = width * height
        val uBuffer: ByteBuffer = planes[1].buffer
        val vBuffer: ByteBuffer = planes[2].buffer

        // 备份缓冲区属性。
        val vBufferPosition: Int = vBuffer.position()
        val uBufferLimit: Int = uBuffer.limit()

        // 将 V 缓冲区推进 1 个字节,因为 U 缓冲区将不包含第一个 V 值。
        vBuffer.position(vBufferPosition + 1)
        // 切掉 U 缓冲区的最后一个字节,因为 V 缓冲区将不包含最后一个 U 值。
        uBuffer.limit(uBufferLimit - 1)

        // 检查缓冲区是否相等并具有预期的元素数量。
        val areNV21 =
            vBuffer.remaining() === 2 * imageSize / 4 - 2 && vBuffer.compareTo(uBuffer) === 0

        // 将缓冲区恢复到初始状态。
        vBuffer.position(vBufferPosition)
        uBuffer.limit(uBufferLimit)
        return areNV21
    }

    /**
     * 将图像平面解压缩为字节数组。
     *
     * 输入平面数据将被复制到“out”中,从“offset”开始,每个像素将被“pixelStride”隔开。 请注意,输出上没有行填充。
     */
    @TargetApi(VERSION_CODES.KITKAT)
    private fun unpackPlane(
        plane: Plane,
        width: Int,
        height: Int,
        out: ByteArray,
        offset: Int,
        pixelStride: Int
    ) {
        val buffer: ByteBuffer = plane.buffer
        buffer.rewind()

        // 计算当前平面的大小。假设它的纵横比与原始图像相同。
        val numRow: Int = (buffer.limit() + plane.rowStride - 1) / plane.rowStride
        if (numRow == 0) {
            return
        }
        val scaleFactor = height / numRow
        val numCol = width / scaleFactor

        // 提取输出缓冲区中的数据。
        var outputPos = offset
        var rowStart = 0
        for (row in 0 until numRow) {
            var inputPos = rowStart
            for (col in 0 until numCol) {
                out[outputPos] = buffer.get(inputPos)
                outputPos += pixelStride
                inputPos += plane.pixelStride
            }
            rowStart += plane.rowStride
        }
    }
}

Jpeg编码

调用安卓库android.graphics.YuvImage将nv21编码为jpeg,并添加mjpeg长连接的消息头

private fun nv21ToJpeg(data: ByteArray, mWidth: Int, mHeight: Int) {
        val yuvImage = YuvImage(data, ImageFormat.NV21, mWidth, mHeight, null)
        val bos = ByteArrayOutputStream(data.size)
        val result = yuvImage.compressToJpeg(Rect(0, 0, mWidth, mHeight), 70, bos)
        if (result) {
            // 输出的jpeg byte数组
            val buffer: ByteArray = bos.toByteArray()
            // 长连接消息头
            val head = ("\r\n--androidMjpeg\r\n" +
                    "Content-Type: image/jpeg\r\nContent-Length: ${buffer.size}\r\n\r\n").toByteArray()
            // 创建一个byte数组保存拼接 长连接消息头 和 jpeg byte数组
            var outByte: ByteArray = ByteArray(head.size+buffer.size)
            for (i in head.indices){
                outByte[i] = head[i]
            }
            for (i in buffer.indices){
                outByte[i+head.size] = buffer[i]
            }
            // 将这个长连接的消息头和jpeg祖成的消息体添加到ByteBufferList中
            val bflist = ByteBufferList()
            bflist.add(ByteBuffer.wrap(outByte))
            // responseMain http服务响应流,http服务创建看下一节
            if (responseMain != null){
                responseMain?.write(bflist)
            }
            try {
                bos.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

Android HTTP服务端

现在已经得到了mjpeg的编码数据了,现在我们就要创建Http服务端把他发送出去

AndroidAsync

使用Github项目:com.koushikdutta.async:androidasync
AndroidAsync可以轻松的创建 Socket、Http,WebSocket 的 客户端 服务端

Mjpeg服务

onCreate通过 AndroidAsync 创建Mjpeg服务

// Http服务端
val server = AsyncHttpServer()
server.get("/", HttpServerRequestCallback { request, response ->
    response.setContentType("multipart/x-mixed-replace;boundary=androidMjpeg")
    responseMain = response
    response.headers

})
server.get("/test", HttpServerRequestCallback { request, response ->
    response.send("Hello World")
})
// 设置服务端口
server.listen(5000)

效果展示

在这里插入图片描述

完整代码

完整参考代码请看 Gitee

参考资料:
Github AndroidAsync
Camera YUV数据转换Jpeg和Bitmap图片格式并保存到本地
Android CameraX 预览以及图片分析(YUV转Bitmap)
CameraX 入门指南-使用 ImageAnalysis 用例
Google Developers

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 CameraX 调用系统相机需要使用以下步骤: 1. 在 build.gradle 文件中添加以下依赖项: ```groovy dependencies { def camerax_version = "1.0.0-beta06" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:1.0.0-alpha20" } ``` 2. 在布局文件中添加 CameraView: ```xml <androidx.camera.view.CameraView android:id="@+id/camera_view" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 3. 在 Activity 或 Fragment 中实例化 CameraX: ```kotlin class MainActivity : AppCompatActivity() { private lateinit var cameraView: CameraView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) cameraView = findViewById(R.id.camera_view) startCamera() } private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() val preview = Preview.Builder().build().also { it.setSurfaceProvider(cameraView.surfaceProvider) } val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle(this, cameraSelector, preview) } catch (e: Exception) { Log.e(TAG, "Use case binding failed", e) } }, ContextCompat.getMainExecutor(this)) } companion object { private const val TAG = "MainActivity" } } ``` 这样就可以使用 CameraX 调用系统相机了。注意,在 AndroidManifest.xml 文件中要添加相机权限: ```xml <uses-permission android:name="android.permission.CAMERA" /> ``` 另外,CameraX 还提供了其他功能,比如拍照、录视频等,可以根据需要进行配置和使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值