乐趣区

关于android:Android-recorder-录制h264通过rtp发送

采集 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

退出移动版