赞
踩
在前两章中,我们已经对如何查询 Codec 和 Codec 的支持特性有了深入的理解,这是通过学习 MediaCodecList 和 MediaCodecInfo.CodecCapabilities 实现的。在确认设备的 Codec 支持特定视频后,我们可以创建相应的 MediaCodec 进行视频解码。
本章,我们将探讨如何使用 MediaCodec 进行视频解码。MediaCodec 支持同步和异步两种模式,同时也支持使用 Surface 或 ByteBuffers 进行数据处理。尽管官方推荐使用 Surface,但考虑到这是一个入门教程,我们将从简单的开始。使用 Surface 的复杂度比使用 ByteBuffers 更高,因此,我们将在本文中讨论如何在同步和异步模式下,使用 MediaCodec 将视频解码到 ByteBuffers。
本文所有代码你可以在 learnmediacodec 找到。
Android MediaCodec 是 Android 提供的一个 API,用于访问底层的多媒体硬件组件,如视频和音频编解码器。这个 API 提供了一个标准的方式来处理多媒体数据,使得开发者可以更容易地在 Android 平台上进行音视频开发。
MediaCodec 可以处理原始的音视频数据,包括编码(将原始数据转换为压缩格式)和解码(将压缩格式转换为原始数据)。这使得开发者可以更方便地进行音视频流的处理,比如实现音视频的播放、录制、编辑等功能。
上图是 Android 官网中对 MediaCodec 工作原理的描述,简单来说,编解码器(codec)是一种处理输入数据以生成输出数据的工具。它异步地处理数据,并使用一组输入和输出缓冲区。你需要请求(或接收)一个空的输入缓冲区(input buffer),将其填充数据并发送给编解码器进行处理。编解码器使用这些数据,并将其转换为一个空的输出缓冲区。最后,你需要请求(或接收)一个已填充的输出缓冲区(output buffer),消耗其内容并将其释放回编解码器。
MediaCodec可以被类比为一个工厂的生产线。输入缓冲区就像是原材料仓库,输出缓冲区就像是成品仓库。原材料(即待编解码的数据)首先被送入原材料仓库(即输入缓冲区),然后工厂(即MediaCodec)根据生产需求,从原材料仓库中取出原材料进行加工处理(即编解码操作)。加工处理完成后,成品(即编解码后的数据)被放入成品仓库(即输出缓冲区)。最后,消费者(即应用程序)从成品仓库中取出成品进行使用。
在这个过程中,原材料仓库和成品仓库都不止一个,这样可以保证工厂的连续生产,提高生产效率。同时,工厂的生产过程是异步的,也就是说,工厂在加工处理原材料的同时,消费者可以从成品仓库中取出成品进行使用,这样可以提高整体的效率。
写到这里,有个问题涌入脑中: MediaCodec 中有几个 input buffer 和 output buffer 呢?Android MediaCodec的输入缓冲区和输出缓冲区的数量并没有固定的值,它们的数量取决于MediaCodec的实现和设备的性能。但是,通常情况下,MediaCodec至少会有一个输入缓冲区和一个输出缓冲区。
在实际使用中,MediaCodec通常会有多个输入缓冲区和输出缓冲区。这是因为,通过使用多个缓冲区,MediaCodec可以在一个缓冲区正在被处理(例如,正在进行编解码操作)的同时,另一个缓冲区可以被填充或消耗数据,这样可以提高处理效率。
具体的数量可以通过调用MediaCodec的getInputBuffers()和getOutputBuffers()方法来获取,这两个方法都会返回一个ByteBuffer数组,数组的长度就是缓冲区的数量。例如在笔者的测试机上有 5 个 input buffer 和 20 个 output buffer。
上图是 MediaCodec 状态的流转图(同步模式)。
在其生命周期中,编解码器(codec)在概念上存在于三种状态之一:停止(Stopped)、执行(Executing)或释放(Released)。停止状态实际上是三种状态的集合:未初始化(Uninitialized)、已配置(Configured)和错误(Error),而执行状态在概念上经历三个子状态:刷新(Flushed)、运行(Running)和流结束(End-of-Stream)。
当你使用工厂方法之一创建编解码器时,编解码器处于未初始化状态。首先,你需要通过configure(…)方法配置它,这会将其转移到已配置状态,然后调用start()方法将其转移到执行状态。在此状态下,你可以通过上述的缓冲区队列操作处理数据。
执行状态有三个子状态:刷新、运行和流结束。在start()方法后,编解码器立即处于刷新子状态,此时它持有所有的缓冲区。一旦第一个输入缓冲区被出队,编解码器就转移到运行子状态,它在这个状态下度过了大部分的生命周期。当你将带有流结束标记的输入缓冲区入队时,编解码器转移到流结束子状态。在此状态下,编解码器不再接受更多的输入缓冲区,但仍然生成输出缓冲区,直到输出上达到流结束。对于解码器,你可以在执行状态下的任何时候使用flush()方法返回到刷新子状态。
调用stop()方法将编解码器返回到未初始化状态,然后它可以再次被配置。当你完成编解码器的使用后,你必须通过调用release()方法释放它。
在极少数情况下,编解码器可能会遇到错误并转移到错误状态。这通过从队列操作的无效返回值,或有时通过异常来通知。调用reset()方法可以使编解码器再次可用。你可以从任何状态调用它,将编解码器返回到未初始化状态。否则,调用release()方法转移到终止的已释放状态。
在不同的状态下,能够进行的操作是不同的,如果转态不匹配 MediaCodec 会抛出异常。在使用MediaCodec的过程中,你需要根据其当前的状态来执行相应的操作。例如,只有在已配置状态下,你才能启动MediaCodec;只有在执行状态下,你才能处理数据;只有在停止或执行状态下,你才能释放MediaCodec。如果在错误状态下,你需要调用reset()方法来重置MediaCodec,使其回到未初始化状态。
MediaCodec 的解码使用起来并不麻烦,但它的使用比较灵活,提供了多种方案,有两个问题需要你进行回答:
本章将使用 MediaCodec 解码到 ByteBuffers,给出同步和异步两种实现。而 Surface 的解码等下个博客再详细聊。
同步模式的基本框架:
MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B codec.start(); for (;;) { int inputBufferId = codec.dequeueInputBuffer(timeoutUs); if (inputBufferId >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(…); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is identical to outputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) outputFormat = codec.getOutputFormat(); // option B } } codec.stop(); codec.release();
MediaCodec codec = MediaCodec.createByCodecName(name); MediaFormat mOutputFormat; // member variable codec.setCallback(new MediaCodec.Callback() { @Override void onInputBufferAvailable(MediaCodec mc, int inputBufferId) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } @Override void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is equivalent to mOutputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } @Override void onOutputFormatChanged(MediaCodec mc, MediaFormat format) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) mOutputFormat = format; // option B } @Override void onError(…) { … } @Override void onCryptoError(…) { … } }); codec.configure(format, …); mOutputFormat = codec.getOutputFormat(); // option B codec.start(); // wait for processing to complete codec.stop(); codec.release();
异步模式下涉及至少两个线程:
另外,异步模式下转态转移与同步模式略有不同:start 后直接到 running 状态。
当你到达输入数据的末尾时,你必须通过在调用queueInputBuffer时指定BUFFER_FLAG_END_OF_STREAM标志来向编解码器发出信号。你可以在最后一个有效的输入缓冲区上做这个操作,或者提交一个额外的带有结束流标志的空输入缓冲区。如果使用空缓冲区,时间戳将被忽略。
编解码器将继续返回输出缓冲区,直到最终通过在dequeueOutputBuffer中设置的BufferInfo或通过onOutputBufferAvailable返回的BufferInfo中指定相同的结束流标志来标志输出流的结束。这可以在最后一个有效的输出缓冲区上设置,或者在最后一个有效的输出缓冲区之后的空缓冲区上设置。这样的空缓冲区的时间戳应该被忽略。
在标志输入流结束后,除非编解码器已经被刷新,或者停止并重新启动,否则不要提交额外的输入缓冲区。
在输入数据流结束时,需要通过指定特定的标志(BUFFER_FLAG_END_OF_STREAM)来通知编解码器。编解码器在处理完所有输入数据后,也会通过同样的方式标志输出数据流的结束。在数据流结束标志后,不应再提交新的输入数据,除非编解码器已经被刷新或重启。这是为了确保数据的完整性和编解码器的正确运行。
private fun decodeToBitmap() { // create and configure media extractor val mediaExtractor = MediaExtractor() resources.openRawResourceFd(R.raw.h264_720p).use { mediaExtractor.setDataSource(it) } val videoTrackIndex = 0 mediaExtractor.selectTrack(videoTrackIndex) val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex) // create and configure media codec val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) val codecName = codecList.findDecoderForFormat(videoFormat) val codec = MediaCodec.createByCodecName(codecName) // configure with null surface so that we can get decoded bitmap easily codec.configure(videoFormat, null, null, 0) // start decoding val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) val inputBuffer = ByteBuffer.allocate(maxInputSize) val bufferInfo = MediaCodec.BufferInfo() val timeoutUs = 10000L // 10ms var inputEnd = false var outputEnd = false codec.start() while (!outputEnd && !stopDecoding) { val isExtractorReadEnd = getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo) if (isExtractorReadEnd) { inputEnd = true } // get codec input buffer and fill it with data from extractor // timeoutUs is -1L means wait forever val inputBufferId = codec.dequeueInputBuffer(-1L) if (inputBufferId >= 0) { if (inputEnd) { codec.queueInputBuffer(inputBufferId, 0, 0, 0, BUFFER_FLAG_END_OF_STREAM) } else { val codecInputBuffer = codec.getInputBuffer(inputBufferId) codecInputBuffer!!.put(inputBuffer) codec.queueInputBuffer( inputBufferId, 0, bufferInfo.size, bufferInfo.presentationTimeUs, 0 ) } } // get output buffer from codec and render it to image view // NOTE! dequeueOutputBuffer with -1L is will stuck here, so wait 10ms here val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, timeoutUs) if (outputBufferId >= 0) { if (bufferInfo.flags and BUFFER_FLAG_END_OF_STREAM != 0) { outputEnd = true } if (bufferInfo.size > 0) { // get output image from codec, is a YUV image val outputImage = codec.getOutputImage(outputBufferId) // convert YUV image to bitmap so that we can render it to image view val bitmap = yuvImage2Bitmap(outputImage!!) // post to main thread to update image view imageView.post { imageView.setImageBitmap(bitmap) } // remember to release output buffer after rendering codec.releaseOutputBuffer(outputBufferId, false) // sleep 30ms to simulate 30fps Thread.sleep(30) } } mediaExtractor.advance() } mediaExtractor.release() codec.stop() codec.release() }
private fun decodeToBitmapAsync() { // create and configure media extractor val mediaExtractor = MediaExtractor() resources.openRawResourceFd(R.raw.h264_4k_30).use { mediaExtractor.setDataSource(it) } val videoTrackIndex = 0 mediaExtractor.selectTrack(videoTrackIndex) val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex) // create and configure media codec val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) val codecName = codecList.findDecoderForFormat(videoFormat) val codec = MediaCodec.createByCodecName(codecName) val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) val inputBuffer = ByteBuffer.allocate(maxInputSize) val bufferInfo = MediaCodec.BufferInfo() val inputEnd = AtomicBoolean(false) val outputEnd = AtomicBoolean(false) // set codec callback in async mode codec.setCallback(object : MediaCodec.Callback() { override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) { val isExtractorReadEnd = getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo) if (isExtractorReadEnd) { inputEnd.set(true) codec.queueInputBuffer(inputBufferId, 0, 0, 0, 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, outputBufferId: Int, info: MediaCodec.BufferInfo ) { if (info.flags and BUFFER_FLAG_END_OF_STREAM != 0) { outputEnd.set(true) } if(info.size > 0){ val outputImage = codec.getOutputImage(outputBufferId) val bitmap = yuvImage2Bitmap(outputImage!!) runOnUiThread{ imageView.setImageBitmap(bitmap) } codec.releaseOutputBuffer(outputBufferId, false) Thread.sleep(30) } } override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { e.printStackTrace() } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { // do nothing } }) codec.configure(videoFormat, null, null, 0) codec.start() // wait for processing to complete while (!outputEnd.get() && !stopDecoding) { Thread.sleep(10) } mediaExtractor.release() codec.stop() codec.release() }
本文介绍了 Android MediaCodec 相关知识,并给出了 MediaCodec 同步和异步的解码流程和示例代码,所有代码你可以在 learnmediacodec 找到。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。