赞
踩
在上一章节,我们已经探讨了如何使用 OpenGL ES 处理解码后的纹理,将彩色画面转换为灰色画面,并在 GLSurfaceView 上展示。在本章节,我们将研究如何将处理后的视频帧保存为本地的 MP4 文件。
本文所有代码可以在 DecodeEditEncodeActivity.kt 找到
整体流程可以大致描述为: Demuxer -> MediaCodec Decoder -> Edit -> MediaCodec Encoder -> Muxer
我们选择 Surface 作为视频数据传递的介质,其中 Surface 中的 Buffer Queue 起着关键作用。在这个流程中,我们需要关注每个 Surface 的生产者和消费者,以便清晰地理解数据的流向。
通过以上流程,视频数据经过解封装、解码、编辑、编码和封装等步骤,最终生成了一个完整的视频文件。
我在编写本章代码时遇到了卡死的问题,线程卡在 glColor
或者 glDrawElements
等 OpenGL 绘制 API 上,并且在华为手机上是必现的,但在小米手机上却没能复现。经过排查,我找到了原因:编码器的 Surface Buffer Queue 满了,导致在调用绘制 api 时,阻塞了当前线程。
那么,问题一:为什么编码器的 Surface 满了?这是因为我们使用的是 MediaCodec 的异步模式,无论是编码还是解码;并且通过 Debug 你就会知道,编码器和解码器虽然是两个 MediaCodec 实例,但它们的回调函数却在同一个线程中执行。于是乎,当出现解码器任务比较多的时候,编码器的 Surface 就可能满,导致卡死。如下图。
问题二,为什么华为手机上必现,小米手机却是正常的。通过日志我发现华为手机上 Surface Buffer Queue 大小为 5,而小米手机是 15,这就导致了小米手机上比较难出现 Buffer Quque 满了导致卡死的问题,但实际上也只是概率比较小,在极限情况仍然可能出现卡死的问题。
知道卡死的原因后如何修复?其实也很简单,我们让编解码器的回调函数执行在不同线程下即可,这部分在代码中会有说明。
先看下整体流程的代码:
private fun decodeASync() { var done = AtomicBoolean(false) // setup extractor val mediaExtractor = MediaExtractor() resources.openRawResourceFd(R.raw.h264_720p).use { mediaExtractor.setDataSource(it) } val videoTrackIndex = 0 mediaExtractor.selectTrack(videoTrackIndex) val inputVideoFormat = mediaExtractor.getTrackFormat(videoTrackIndex) val videoWidth = inputVideoFormat.getInteger(MediaFormat.KEY_WIDTH) val videoHeight = inputVideoFormat.getInteger(MediaFormat.KEY_HEIGHT) Log.i(TAG, "get video width: $videoWidth, height: $videoHeight") // setup muxer val outputDir = externalCacheDir val outputName = "decode_edit_encode_test.mp4" val outputFile = File(outputDir, outputName) val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) var muxerSelectVideoTrackIndex = 0 // create encoder val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC val outputFormat = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight) val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface val videoBitrate = 2000000 val frameRate = 30 val iFrameInterval = 60 outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat) outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate) outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval) val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) val encodeCodecName = codecList.findEncoderForFormat(outputFormat) val encoder = MediaCodec.createByCodecName(encodeCodecName) Log.i(TAG, "create encoder with format: $outputFormat") // set encoder callback encoder.setCallback(...) encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) // create input surface and egl context for opengl rendering val inputSurface = InputSurface(encoder.createInputSurface()) inputSurface.makeCurrent() // create decoder val decodeCodecName = codecList.findDecoderForFormat(inputVideoFormat) val decoder = MediaCodec.createByCodecName(decodeCodecName) // create output surface texture val textureRenderer = TextureRenderer2() val surfaceTexture = SurfaceTexture(textureRenderer.texId) val outputSurface = Surface(surfaceTexture) inputSurface.releaseEGLContext() val thread = HandlerThread("FrameHandlerThread") thread.start() surfaceTexture.setOnFrameAvailableListener({ Log.d(TAG, "setOnFrameAvailableListener") synchronized(lock) { if (frameAvailable) Log.d( TAG, "Frame available before the last frame was process...we dropped some frames" ) frameAvailable = true lock.notifyAll() } }, Handler(thread.looper)) val texMatrix = FloatArray(16) // set callback val maxInputSize = inputVideoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) val inputBuffer = ByteBuffer.allocate(maxInputSize) val bufferInfo = MediaCodec.BufferInfo() val videoDecoderHandlerThread = HandlerThread("DecoderThread") videoDecoderHandlerThread.start() decoder.setCallback(..., Handler(videoDecoderHandlerThread.looper)) // config decoder decoder.configure(inputVideoFormat, outputSurface, null, 0) decoder.start() encoder.start() // wait for done while(!done.get()) { Thread.sleep(10) } Log.d(TAG, "finished") // release resources Log.d(TAG, "release resources...") mediaExtractor.release() decoder.stop() decoder.release() surfaceTexture.release() outputSurface.release() encoder.stop() encoder.release() muxer.stop() muxer.release() Log.d(TAG, "release resources end...") }
setOnFrameAvailableListener
回调函数,原因在上一章中我已经解释过了,不再赘述。上面的过程除了一些 GL Context、线程等细节外,整体上还是比较容易理解的。接下来,我们看解码器和编码器的回调函数,这才是真正干活的地方。
encoder.setCallback(object : MediaCodec.Callback() { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { } override fun onOutputBufferAvailable( codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo ) { val isEncodeDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 if (isEncodeDone) { info.size = 0 done.set(true) } // got encoded frame, write it to muxer if (info.size > 0) { val encodedData = codec.getOutputBuffer(index) muxer.writeSampleData(muxerSelectVideoTrackIndex, encodedData!!, info) codec.releaseOutputBuffer(index, info.presentationTimeUs * 1000) } } override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { muxerSelectVideoTrackIndex = muxer.addTrack(format) muxer.start() } });
编码器的回调函数逻辑比较简单:
onOutputBufferAvailable
,当编码器的输出缓冲区有数据可用时,此函数会被调用。在这个函数中,你可以从输出缓冲区获取编码后的数据。在这段代码中,首先检查是否已经到达流的结束,如果是,则设置done标志为true。然后,如果输出缓冲区的数据大小大于0,就将编码后的数据写入到muxer,然后释放输出缓冲区。onOutputFormatChanged
,当编码器的输出格式发生改变时,此函数会被调用。在这段代码中,当输出格式改变时,将新的格式添加到muxer,然后启动muxer。decoder.setCallback(object : MediaCodec.Callback() { override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) { val isExtractorReadEnd = getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo) if (isExtractorReadEnd) { codec.queueInputBuffer( inputBufferId, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM ) } else { val codecInputBuffer = codec.getInputBuffer(inputBufferId) codecInputBuffer!!.put(inputBuffer) codec.queueInputBuffer( inputBufferId, 0, bufferInfo.size, bufferInfo.presentationTimeUs, bufferInfo.flags ) mediaExtractor.advance() } } override fun onOutputBufferAvailable( codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo ) { if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) { codec.releaseOutputBuffer(index, false) return } val render = info.size > 0 codec.releaseOutputBuffer(index, render) if (render) { waitTillFrameAvailable() val ptsNs = info.presentationTimeUs * 1000 inputSurface.makeCurrent() surfaceTexture.updateTexImage() surfaceTexture.getTransformMatrix(texMatrix) // draw oes text to input surface textureRenderer.draw(videoWidth, videoWidth, texMatrix, getMvp()) inputSurface.setPresentationTime(ptsNs) inputSurface.swapBuffers() inputSurface.releaseEGLContext() } if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { encoder.signalEndOfInputStream() } } override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { } }, Handler(videoDecoderHandlerThread.looper))
onInputBufferAvailable
,当解码器需要输入数据时调用。在该回调函数中,首先通过调用getInputBufferFromExtractor()方法从MediaExtractor中获取输入数据,并将数据放入解码器的输入缓冲区中。如果已经读取到了Extractor的末尾,则向解码器的输入缓冲区发送结束标志。否则,将输入数据放入解码器的输入缓冲区,并调用advance()方法继续读取下一帧数据。onOutputBufferAvailable
,当解码器的输出缓冲区有数据可用时,此函数会被调用。在这个函数中,你可以从输出缓冲区获取解码后的数据。在这段代码中,首先检查输出缓冲区的数据是否是编解码器配置数据,如果是,则释放输出缓冲区并返回。然后,如果输出缓冲区的数据大小大于0,就将解码后的数据渲染到 Surface。最后,如果已经到达流的结束,就向编码器发送流结束的信号。注意,为了绘制数据到 Surface 上,我们要确保当前线程有 EGL Context 环境,因此调用了 inputSurface.makeCurrent()
;接着,inputSurface.setPresentationTime
设置 PTS,然后使用 inputSurface.swapBuffers()
来交换 Buffer,告诉编码器来了一帧数据;最后 inputSurface.releaseEGLContext
来解除当前的 EGL 环境。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。