采集camera数据

数据采集部分使用的是Camera2,CameraHolder是对camera2的简单封装。 Camera2有个显著的优势,他可以同时添加多个surface用于接收camer数据。 下面是通过CameraHolder启动camera的流程:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ......
    cameraHolder = CameraHolder(this)
}
override fun onStart() {
    super.onStart()
    cameraHolder.startPreview().invalidate()
}

override fun onStop() {
    super.onStop()
    cameraHolder.stopPreview().invalidate()
}

override fun onDestroy() {
    super.onDestroy()
    cameraHolder.release().invalidate()
    recorder.stopVideoEncoder()
}

在启动camera和预览的时候可以不用考虑camera的当前状态,这是CameraHolder的优势。有的朋友会问启动预览肯定要依赖surface,这里启动预览时怎么保证surface已经设置? 在封装Camera2的时候就想到了这种依赖问题,这种依赖导致我们每次接口调用都要判断依赖对象是否创建,代码写起来繁琐也难看。看下CameraHolder是如何解决的这个问题:

fun invalidate() = runInCameraThread {
    if (cameraPermissionInProcess) {
        Log.d(TAG, "invalidate cameraPermissionInProcess $cameraPermissionInProcess")
        return@runInCameraThread
    }
    if (cameraCaptureSession != null && (!requestPreview || requestRestartPreview || !requestOpen || requestRestartOpen)) {
        cameraCaptureSession?.close()
        cameraCaptureSession = null
        requestRestartPreview = false
        Log.d(TAG, "invalidate cameraCaptureSession?.close()")
    }

    if (cameraDevice != null && (!requestOpen || requestRestartOpen)) {
        cameraDevice?.close()
        cameraDevice = null
        requestRestartOpen = false
        Log.d(TAG, "invalidate cameraDevice?.close()")
    }

    if (cameraDevice == null && requestOpen) {
        Log.d(TAG, "invalidate openCamera()")
        openCameraInternal()
    }

    if (cameraDevice != null && cameraCaptureSession == null && requestPreview) {
        Log.d(TAG, "invalidate startPreview()")
        startPreviewInternal()
    }

    if (requestRelease) {
        Log.d(TAG, "invalidate release()")
        handler = null
        handlerThread.quitSafely()
    }
}

我只贴出了CameraHodler的一个invalidate方法,这个方法应该算是CameraHodler最重要的部分。invalidate方法定义了camera启动、预览、停止预览和关闭camera的流程模板。方法中定义了许多的状态flag,整个的流程就是通过flag来控制的。

requestOpen 为true代表用户要求打开camera。
requestPreview 为true代表用户要求进行preview。
requestRestartPreview为true代表用户更新了preview依赖对象请求重启preview
requestRestartOpen为true代表用户要求重启camera。
流程模板中的每一步都有失败的可能,比如用户请求开启preview,但是surface没有准备好。用户请求启动camera但是权限验证没通过等等。这时CameraHolder会等待下一次invalidate方法被调用,触发下次的流程模板执行,这样就做到依赖满足时唤醒原来终止的流程。

预览数据

为了更方便地对camera采集数据进行修改,这里构建了一个opengl环境,用户可以根据自己的需求添加新的eglSurface到opengl渲染系统。RenderScope维护了egl环境和与之对应的thread。

class RenderScope(private val render: Render) : BackgroundScope by HandlerThreadScope() {

private var eglCore: EGLCore? = null

init {
    runInBackground {
        eglCore = EGLCore()
        render.onCreate()
    }
}

fun addSurfaceHolder(eglSurfaceHolder: EGLCore.EGLSurfaceHolder?) = runInBackground {
    eglCore?.addSurfaceHolder(eglSurfaceHolder)
}

fun removeSurfaceHolder(eglSurfaceHolder: EGLCore.EGLSurfaceHolder?) = runInBackground {
    eglCore?.removeSurfaceHolder(eglSurfaceHolder)
}

fun removeSurfaceHolder(surface: Surface?) = runInBackground {
    eglCore?.removeSurfaceHolder(surface)
}

fun release() = runInBackground {
    render.onDestroy()
    eglCore?.releaseEGLContext()
    eglCore = null
    quit()
}

fun requestRender() = runInBackground {
    eglCore?.render { render.onDrawFrame(it) }
}

interface Render {
    fun onCreate()
    fun onDestroy()
    fun onDrawFrame(eglSurfaceHolder: EGLCore.EGLSurfaceHolder)
}

}
RenderScope的代码并不多,直观的就能看到两个主要部分render线程和egl环境。HandlerThreadScope封装了HandlerThread并提供了runInBackground方法扩展,我们可以方便的把代码执行切换到render线程。eglCore则构建了egl环境,他做了一些opengl的初始化工作,比如egldisplay、eglcontext、eglsurface。 在渲染的流程中,我们可以选择同时渲染内容到多个不同的eglsurface。在这里多个不同的eglsurface指的是preview surface和encode surface。eglSurface是通过eglMakeCurrent方法切换的。

fun render(block: (EGLSurfaceHolder) -> Unit) {
    if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
        log("render mEGLDisplay == EGL14.EGL_NO_DISPLAY")
        return
    }
    mEGLSurfaces.forEach { _, holder ->
        if (!holder.available) {
            return@forEach
        }
        EGL14.eglMakeCurrent(mEGLDisplay, holder.eglSurface, holder.eglSurface, mEGLContext)
        checkEglError("makeCurrent")
        block.invoke(holder)
        val result = EGL14.eglSwapBuffers(mEGLDisplay, holder.eglSurface)
        when (val error = EGL14.eglGetError()) {
            EGL14.EGL_SUCCESS -> result
            EGL14.EGL_BAD_NATIVE_WINDOW, EGL14.EGL_BAD_SURFACE -> throw IllegalStateException(
                "swapBuffers: EGL error: 0x" + Integer.toHexString(error)
            )
            else -> throw IllegalStateException(
                "swapBuffers: EGL error: 0x" + Integer.toHexString(error)
            )
        }
    }
}

在渲染的流程中我们可以看到遍历eglsurface的过程,每个eglsurface都可以调用eglMakeCurrent方法切换输出对象。这里的输出对象包括preview surface和encode surface。通过这种方法可以替代共享context和texture方式,流程也更简单明了。

使用MediaCodec编码数据

视频编码部分这里使用的是MediaCodec,SurfaceEncodeCodec类是对MediaCodec的封装。

abstract class SurfaceEncodeCodec(mediaFormat: MediaFormat) : BaseCodec("Encode surface", mediaFormat) {

private var inputSurface: Surface? = null
override fun onCreateMediaCodec(mediaFormat: MediaFormat): MediaCodec {
    val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)
    val codecName = mediaCodecList.findEncoderForFormat(mediaFormat)
    check(!codecName.isNullOrEmpty()) { throw RuntimeException("not find the matched codec!!!!!!!") }
    return MediaCodec.createByCodecName(codecName)
}

override fun onConfigMediaCodec(mediaCodec: MediaCodec) {
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    inputSurface = mediaCodec.createInputSurface()
    inputSurface?.let {
        onCreateInputSurface(it)
    }
}

override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
    //do nothing.because we set the input surface for encoder.
}

protected abstract fun onCreateInputSurface(surface: Surface)

/**
 * we need to release surface ourselves.
 */
protected abstract fun onDestroyInputSurface(surface: Surface)

override fun releaseInternal() {
    inputSurface?.let {
        onDestroyInputSurface(it)
    }
    super.releaseInternal()
}

}
MediaCodec可以通过surface的方式输入视频数据,在配置好MediaCodec后,我们通过MediaCodec的createInputSurface方法得到surface,在preview部分已经讲过,opengl会将内容渲染到preview surface和encode surface。encode surface就是MediaCodec创建的surface。输入数据的问题解决了,那么来看下编码后的数据如何处理。

override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {

            val buffer = codec.getOutputBuffer(index) ?: return
            when {
                info.flags.and(MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> {
                    log { "video config frame +++++++++++++" }
                }
                info.flags.and(MediaCodec.BUFFER_FLAG_KEY_FRAME) == MediaCodec.BUFFER_FLAG_KEY_FRAME -> {
                    countIFrame++
                    if (countIFrame.rem(3) == 0) {
                        log { "video ssspppssspppsss frame +++++++++++++" }
                        videoRtpWrapper?.sendData(ppsByteArray, ppsByteArraySize, videoPayloadType, true, 0)
                        videoRtpWrapper?.sendData(spsByteArray, spsByteArraySize, videoPayloadType, true, 0)
                    }
                    naluData.split2FU(buffer, info.offset, info.size) { b, o, s, m, increase ->
                        videoRtpWrapper?.sendData(b, s, videoPayloadType, m, if (increase) videoTimeIncrease else 0)
                    }
                }
                info.flags.and(MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM -> {
                    log { "video end frame -------------" }
                }
                info.flags.and(MediaCodec.BUFFER_FLAG_PARTIAL_FRAME) == MediaCodec.BUFFER_FLAG_PARTIAL_FRAME -> {
                    log { "video partial frame -------------" }
                }
                else -> {
                    naluData.split2FU(buffer, info.offset, info.size) { b, o, s, m, increase ->
                        videoRtpWrapper?.sendData(b, s, videoPayloadType, m, if (increase) videoTimeIncrease else 0)
                    }
                }
            }
            codec.releaseOutputBuffer(index, false)
        }

        override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
            format.getByteBuffer("csd-0")?.apply {
                position(4)
                spsByteArraySize = limit() - 4
                get(spsByteArray, 0, spsByteArraySize)
            }
            format.getByteBuffer("csd-1")?.apply {
                position(4)
                ppsByteArraySize = limit() - 4
                get(ppsByteArray, 0, ppsByteArraySize)
            }
            videoRtpWrapper = RtpWrapper()
            videoRtpWrapper?.open(videoRtpPort, videoPayloadType, videoSampleRate)
            videoRtpWrapper?.addDestinationIp(ip)

            videoRtpWrapper?.sendData(ppsByteArray, ppsByteArraySize, videoPayloadType, true, 0)
            videoRtpWrapper?.sendData(spsByteArray, spsByteArraySize, videoPayloadType, true, 0)
        }

我们要关心下MediaCodec的两类输出数据,格式数据与视频帧数据。格式数据中对我们比较重要的是sps和pps数据,这俩个数据描述视频的基本信息。视频接收端需要根据sps和pps数据进行视频解码。从代码上可以看到他们保存在"csd-0"和"csd-1"两个buffer中。sps数据和pps数据在后面的rtp发送的过程当中要间断性地重复发送,因为在半路连接的远端对象需要在接收到他们后才可以播放。 帧数据发送的过程当中涉及到拆包的问题,因为rtp包的大小是有限制的。

fun split2FU(byteBuffer: ByteBuffer, offset: Int, size: Int, sender: (ByteArray, Int, Int, Boolean, Boolean) -> Unit) {

    if (size < maxFragmentSize) {
        byteBuffer.position(offset + 4)//skip 0001
        byteBuffer.get(data, 0, size - 4)
        sender.invoke(data, 0, size - 4, true, true)
        return
    }
    val naluHeader = byteBuffer.get(4)//read nalu header
    byteBuffer.position(offset + 5)//skip 0001 and nalu header
    var leftSize = size - 5
    var started = false
    var fuIndicator: Byte
    var fuHeader: Byte
    while (leftSize > 0) {
        val readSize = if (leftSize > maxFragmentSize) maxFragmentSize else leftSize;
        byteBuffer.get(data, 2, readSize)
        leftSize -= readSize
        fuIndicator = naluHeader and 0b11100000.toByte() or 0b00011100.toByte()
        fuHeader = when {
            !started -> {
                0b10000000.toByte()
            }
            leftSize <= 0 -> {
                0b01000000.toByte()
            }
            else -> {
                0b00000000.toByte()
            }
        }
        data[0] = fuIndicator
        data[1] = fuHeader or (naluHeader and 0b00011111.toByte())
        sender.invoke(data, 0, readSize + 2, leftSize <= 0, !started)
        started = true
    }
}

从代码上看分片规则还是不太容易理解,并且分片过程中提到了两个type,我们不太容易理解对应关系。看下面这个图就好理解了。

MediaCodec编码后的nalu数据帧的前四个字节是0001,它用于表示帧数据的开始。我们跳过这四个字节后再取的一个字节就是NALU header了。从图中可以简单了解下nalu header 的构成。在分片后的每一帧数据都会拥有两个字节的头,头的构成情况可以参考图中的第二个部分。fu indicator数据和fu header数据包含了原来nalu header的完整数据和分片帧信息。在这里nalu header的原始数据被拆开后放到fu indicator和 fu header中。如果不通过图的方式讲解,大家很难弄清分片type和nalutype到底如何区分。理清了type问题后就变得简单了,根据分片是开始、中间或是结束来设置分片控制信息就可以了。

使用RTP lib发送数据

rtp可以简单的把拆好的分片包发送出去,但是还要注意rtp发送的时候需要指定payload type、时间戳、是否传达结束mark。按照rtp传输视频的规则,payload type是96。时间戳的控制需要根据帧率来计算每一帧的时间增量,然后以固定的时间戳增量发送。分片的情况下,各个分片之间增量为0,在发送结束分片时,时间增量为固定时间戳增量。 这里还有按照固定的时间间隔发送sps和pps数据出去,这样半路开始接入播放的远端才可以正常的解码观看。

使用vlc播放器播放

vlc播放rtp视频时需要打开sdp文件,sdp文件中有传送协议信息和视频合适信息。sdp文件包含下面的内容:

m=video 40018 RTP/AVP 96
a=rtpmap:96 H264
a=framerate:30
c=IN IP4 192.168.31.1
player_video.sdp

rtp端口:40018

payload type:96

视频编码:H264

Git

VideoRecorder


mjlong123
4 声望3 粉丝