您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

使用缓冲的方式采集视频

时间:02-09来源:作者:点击数:

摄像头权限申请什么的就不说了,直接上关键代码:

class MainActivity : AppCompatActivity() {

    private val camera = Camera.open()
    private val surfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        camera.setPreviewTexture(surfaceTexture)
        camera.setPreviewCallback { data, camera ->
            println("摄像头正在采集图像")
        }
        camera.startPreview()
    }

    override fun onDestroy() {
        super.onDestroy()
        camera.setPreviewCallback(null)
        camera.stopPreview()
        camera.release()
    }

}

这是一个无预览摄像头视频采集,只是一个非常简单的代码,camera.setPreviewCallback中的data即为图像数据,我们没有做保存处理,每次得到的data都会是一个新的数组对象,试验如下:

class MainActivity : AppCompatActivity() {

    private val camera = Camera.open()
    private val surfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
    private var oldData = ByteArray(0)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        camera.setPreviewTexture(surfaceTexture)
        camera.setPreviewCallback { data, camera ->
            if (oldData !== data) {
                oldData = data
                println("新data")
            } else {
                println("旧data")
            }
        }
        camera.startPreview()
    }

    override fun onDestroy() {
        super.onDestroy()
        camera.setPreviewCallback(null)
        camera.stopPreview()
        camera.release()
    }

}

运行代码,打印的都是"新data"。如果是25帧/秒,则每秒要创建25个新的Byte数组,不停地创建新的对象性能是比较低的,所以可以使用缓冲对象,每次都用同一个数组,代码如下:

camera.setPreviewCallbackWithBuffer { data, camera ->
    if (oldData !== data) {
        oldData = data
        println("新data")
    } else {
        println("旧data")
    }
}

这里把setPreviewCallback函数替换成了setPreviewCallbackWithBuffer,运行代码,会发现没有任何输出,这是因为还需要我们提供一个缓冲数组,如下:

class MainActivity : AppCompatActivity() {

    private val camera = Camera.open()
    private val surfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
    private val previewSize = camera.parameters.previewSize
    private val width = previewSize.width
    private val height = previewSize.height
    private val callbackBuffer = ByteArray((width * height * 3) shr 1)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        camera.setPreviewTexture(surfaceTexture)
        camera.addCallbackBuffer(callbackBuffer)
        camera.setPreviewCallbackWithBuffer { data, camera ->
            if (callbackBuffer !== data) {
                println("新data")
            } else {
                println("旧data")
            }
        }
        camera.startPreview()
    }

    override fun onDestroy() {
        super.onDestroy()
        camera.setPreviewCallback(null)
        camera.stopPreview()
        camera.release()
    }

}

运行代码,会发现只打印了一次"旧data",只是因为系统只会在我们调用了camera.addCallbackBuffer(callbackBuffer)之后,才使用我们给的buffer对象装一帧的图像给我们,所以,在我们需要数据的时候就需要调用camera.addCallbackBuffer(callbackBuffer),如果一直需要,就需要一直调用camera.addCallbackBuffer(callbackBuffer),示例如下:

class MainActivity : AppCompatActivity() {

    private val camera = Camera.open()
    private val surfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
    private val previewSize = camera.parameters.previewSize
    private val width = previewSize.width
    private val height = previewSize.height
    private val callbackBuffer = ByteArray((width * height * 3) shr 1)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        camera.setPreviewTexture(surfaceTexture)
        camera.addCallbackBuffer(callbackBuffer)
        camera.setPreviewCallbackWithBuffer { data, camera ->
            if (callbackBuffer !== data) {
                println("新data")
            } else {
                println("旧data")
            }
            // TODO 处理data
            // data处理完了,再把缓冲对象给到框架,让其再填充图像数据在里面
            //camera.addCallbackBuffer(data) // 这个方式也可以,反正都是同一个对象
            camera.addCallbackBuffer(callbackBuffer)
        }
        camera.startPreview()
    }

    override fun onDestroy() {
        super.onDestroy()
        camera.setPreviewCallback(null)
        camera.stopPreview()
        camera.release()
    }

}

再次运行代码,会看到一直在输出"旧data",这样就避免了每一帧都创建新的data对象,但是需要注意,我们在处理data数据的时候一定要快,假设需要25帧/秒,则每一帧的处理时间为40毫秒,我们必须在40毫秒内处理完data,然后再把data设置到addCallbackBuffer中,如果处理的时间慢,就会导致丢帧。比如,我们加入帧率的统计代码,如下:

class MainActivity : AppCompatActivity() {

    private val camera = Camera.open()
    private val surfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
    private val previewSize = camera.parameters.previewSize
    private val width = previewSize.width
    private val height = previewSize.height
    private val callbackBuffer = ByteArray((width * height * 3) shr 1)
    private var fps = 0
    private var start = 0L

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("width = $width height = $height")
        camera.setPreviewTexture(surfaceTexture)
        camera.addCallbackBuffer(callbackBuffer)
        camera.setPreviewCallbackWithBuffer { data, camera ->
            fps++
            val current = System.currentTimeMillis()
            if (start == 0L) {
                start = current
            }
            if (current - start >= 1000) {
                println("帧速:${fps}帧/秒")
                fps = 0
                start = current
            }
            camera.addCallbackBuffer(data)
        }
        camera.startPreview()
    }

    override fun onDestroy() {
        super.onDestroy()
        camera.setPreviewCallback(null)
        camera.stopPreview()
        camera.release()
    }

}

在小米11 pro运行结果如下:

width = 1920 height = 1080
帧速:28帧/秒
帧速:24帧/秒
帧速:30帧/秒
帧速:30帧/秒
帧速:31帧/秒
帧速:30帧/秒
帧速:31帧/秒
帧速:30帧/秒
帧速:31帧/秒
帧速:30帧/秒

可以看到,帧速并不是稳定输出的,也会有偏差。

接下来,模拟一下对data数据的处理,假设这个处里需要80毫秒:

class MainActivity : AppCompatActivity() {

    private val camera = Camera.open()
    private val surfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
    private val previewSize = camera.parameters.previewSize
    private val width = previewSize.width
    private val height = previewSize.height
    private val callbackBuffer = ByteArray((width * height * 3) shr 1)
    private var fps = 0
    private var start = 0L
    private val handler = object: Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            camera.addCallbackBuffer(callbackBuffer)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        println("width = $width height = $height")
        camera.setPreviewTexture(surfaceTexture)
        camera.addCallbackBuffer(callbackBuffer)
        camera.setPreviewCallbackWithBuffer { data, camera ->
            fps++
            val current = System.currentTimeMillis()
            if (start == 0L) {
                start = current
            }
            if (current - start >= 1000) {
                println("帧速:${fps}帧/秒")
                fps = 0
                start = current
            }

            // 模拟一个80毫秒的耗时操作
            handler.sendEmptyMessageDelayed(0, 80L)
        }
        camera.startPreview()
    }

    override fun onDestroy() {
        super.onDestroy()
        camera.setPreviewCallback(null)
        camera.stopPreview()
        camera.release()
    }

}

运行效果如下:

width = 1920 height = 1080
帧速:7帧/秒
帧速:8帧/秒
帧速:6帧/秒
帧速:6帧/秒
帧速:8帧/秒
帧速:7帧/秒
帧速:8帧/秒
帧速:8帧/秒
帧速:9帧/秒
帧速:8帧/秒

可以看到,由于处理数据花费的时间太多,帧速极速下降,所以看到的视频就会很卡,不是卡住不动,而是视频不流畅,画面不连贯。

接下来,我们把耗时时间改成30毫秒,再次运行,结果如下:

width = 1920 height = 1080
帧速:13帧/秒
帧速:13帧/秒
帧速:14帧/秒
帧速:14帧/秒
帧速:14帧/秒
帧速:14帧/秒
帧速:14帧/秒
帧速:13帧/秒
帧速:14帧/秒

理论上1帧需要30毫秒,1秒(1000毫秒)可以处理33帧啊,但是结果显示只有14帧左右,那我们把延时再改小为10毫秒,再次运行,结果如下:

width = 1920 height = 1080
帧速:20帧/秒
帧速:22帧/秒
帧速:31帧/秒
帧速:30帧/秒
帧速:31帧/秒
帧速:31帧/秒
帧速:30帧/秒
帧速:30帧/秒
帧速:30帧/秒
帧速:31帧/秒
帧速:29帧/秒
帧速:31帧/秒
帧速:30帧/秒
帧速:31帧/秒

可以看到,帧速恢复到了30帧/秒,这样视频就不会卡了,但是10毫秒的处理时间,在真实开发中根本就不够用,每一帧图像要经过旋转、加水印、H264编码、网络发送等一系列操作,10毫秒根本就完成不了,所以解决方案就是多线程处理,可以使用双缓冲机制,比如视频采集、格式转换、旋转、加水印、编码、发送,每两个相连的步骤之间都可以加入双缓冲机制,这样就可以实现每一个步骤都是并行运行的。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门