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