MediaCodec概述

本篇文章是对官方的文档MediaCodec的翻译,一些地方使用了比较通俗易懂的叙述。算是对于音视频方面知识学习的开篇吧。
注:一些英文单词和翻译后的对照

  • codec:编解码器
  • input buffer:输入缓冲区
  • output buffer:输出缓冲区
缓冲区/缓冲区数组用代码表示更清晰:
//缓冲区 ByteBuffer byteBuffer; //缓冲区数组 ByteBuffer[] mCachedInputBuffers;

MediaCodec类可以用来访问低级别的媒体编解码器,即编码器/解码器组件。它是Android低水平的多媒体支持组件的一部分,通常和MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack 一起使用。
MediaCodec概述
文章图片
初始输入数据生成输出数据.png 广义上来说,一个编解码器处理输入数据生成输出数据。它异步的处理数据并使用一组输入和输出缓冲区。在一个简单的层面上,你请求(或接收)一个空的输入缓冲区,将其填充数据并将其交给编解码器进行处理。编解码器将输入的数据处理以后会将输出数据填充到一个空的输出缓冲区进行输出。最后,你请求(或接收)一个填充了数据的输出缓冲区,消费其中的内容,然后将这个输出缓冲区释放给编解码器。(上面的图片表达的更清晰)
数据类型 MediaCodec可以处理3中类型的数据:压缩的数据、原始的音频数据、原始的视频数据。三种类型的数据都可以使用ByteBuffers处理,但是你应该使用Surface来处理原始视频数据,这样可以提高编解码器的性能。Surface使用本地(native)视频缓冲区没有将其映射或者拷贝到ByteBuffers,因此会更高效。当使用Surface的时候,正常情况下你无法访问原始视频数据,但是你可以使用ImageReader来访问不安全的未编码的(就是原始类型)的视频帧(frame)。这肯能仍然比使用ByteBuffers高效,因为一些本地(native)的缓冲区可能被映射到了直接缓冲区(direct ByteBuffers)。当使用ByteBuffer模式的时候,你可以使用Image类和getInput/OutputImage(int)来访问原始的视频帧。
压缩的数据
输入缓冲区和输出缓冲区根据 format's type包含压缩数据。对于视频类型来说,通常是单个压缩过的视频帧。对音频数据来说通常是一个单独的访问单元(access unit),但是因为一个缓冲区可能包含多个编码的音频访问单元,这个要求可以稍微放松一点。无论哪种情况,缓冲区都不会在任意字节边界处开始或结束,而是在帧/访问单元边界处开始或结束,除非使用了BUFFER_FLAG_PARTIAL_FRAME标记。
原始的音频数据
原始的音频缓冲区包含PCM音频数据的整个帧,这是按照通道顺序的每个通道的一个样本。每个PCM音频样本都是16位带符号整数或浮点数,以本地字节顺序(in native byte order)。只有在MediaCodec在configure(…)期间将MediaFormat#KEY_PCM_ENCODING设置为AudioFormat#ENCODING_PCM_FLOAT并且在解码时由getOutputFormat()确认或者在编码时由getInputFormat()确认时,原始的音频数据才有可能是浮点型PCM编码。一个检查MediaFormat是不是浮点型PCM编码的简单方法如下所示:
static boolean isPcmFloat(MediaFormat format) { return format.getInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT) == AudioFormat.ENCODING_PCM_FLOAT; }

为了从一个包含16位带符号整数的音频数据缓冲区中提取一个信道(channel)的数据到一个short类型的数组中,下面的代码可能被用到:(这段英文原文如下:In order to extract, in a short array, one channel of a buffer containing 16 bit signed integer audio data, the following code may be used:)
// Assumes the buffer PCM encoding is 16 bit. short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) { ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId); MediaFormat format = codec.getOutputFormat(bufferId); ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer(); int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); if (channelIx < 0 || channelIx >= numChannels) { return null; } short[] res = new short[samples.remaining() / numChannels]; for (int i = 0; i < res.length; ++i) { res[i] = samples.get(i * numChannels + channelIx); } return res; }

原始的视频数据
在ByteBuffer的模式下,视频缓冲区根据其 color format进行存储(laid out)。你可以从getCodecInfo()、getCapabilitiesForType(…)、colorFormats获取支持的色彩格式(color format)作为一个数组。视频解码器可能支持3中色彩格式。
  • 本地原始视频格式:该类型由CodecCapabilities#COLOR_FormatSurface
    标记,可以和一个输入/输出Surface一起使用。
  • 灵活的YUV缓冲区(例如CodecCapabilities#COLOR_FormatYUV420Flexible):可以与一个输入/输出Surface一起使用,也可以在 ByteBuffer 模式下通过getInput/OutputImage(int)使用。
  • 其他,特定的格式:这些通常仅在 ByteBuffer 模式下使用。一些色彩格式基于特定的供应商。其他的格式定义在CodecCapabilities中。对于等同于灵活格式(是指灵活的YUV缓冲区吗?)的色彩格式,你仍然通过getInput/OutputImage(int)的方式进行使用。
从Build.VERSION_CODES.LOLLIPOP_MR1开始,所有的视频解码器支持灵活的 YUV 4:2:0 buffers 。
在老的设备上访问原始视频数据
在Build.VERSION_CODES.LOLLIPOP和Image
支持之前,你需要使用MediaFormat#KEY_STRIDE和MediaFormat#KEY_SLICE_HEIGHT输出格式的值来理解原始输出缓冲区的布局(layout)。这里的布局我的理解就是存储方式。
MediaFormat#KEY_WIDTH和MediaFormat#KEY_HEIGHT指定了视频帧的尺寸;但是大多数编码的视频(图像)只占视频帧的一部分。这由剪裁矩形(crop rectangle)表示。
你需要使用以下的键(key)从输出格式(output format)中获取原始输出图像的剪裁矩形。如果这些key没有对应的值(value),那么视频(图像)就占据整个视频帧。注意应该在应用任何 rotation之前获取输出帧的裁剪矩形(The crop rectangle is understood in the context of the output frame before applying any rotation)。
格式键 类型 描述
crop-left Integer 剪裁矩形的左坐标
crop-top Integer 剪裁矩形的顶部坐标
crop-right Integer 剪裁矩形的右坐标减1
crop-bottom Integer 剪裁矩形的底部坐标减1
右坐标和底坐标可以理解为裁剪后的输出图像的最右边有效列/最下面有效行的坐标。
视频帧的尺寸(在旋转之前)可以这样计算:
MediaFormat format = decoder.getOutputFormat(…); int width = format.getInteger(MediaFormat.KEY_WIDTH); if (format.containsKey("crop-left") && format.containsKey("crop-right")) { width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left"); } int height = format.getInteger(MediaFormat.KEY_HEIGHT); if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) { height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top"); }

另请注意,BufferInfo.offset在不同设备之间的含义不一致。在某些设备上,偏移量是指裁剪矩形的左上像素,而在大多数设备上,偏移量是指整个帧的左上像素。
状态
在整个生命周期中,编解码器理论上处于以下三个状态之一:停止(Stopped),执行(Executing),释放(Released)。停止的状态实际上是三个状态的集合:未初始化(Uninitialized),已配置(Configured)和错误(Error)。执行状态从概念上是在3个子状态之间变化:刷新(Flushed), 运行(Running) 和流结束(End-of-Stream)。
MediaCodec概述
文章图片
状态.png 当你使用某个工厂方法创建编解码器的时候,编解码器处于未初始化的状态。首先,你需要通过configure(…)来配置它,使它进入已配置的状态,然后调用start()方法使它进入到执行状态。当编解码器处于执行状态的时候,你可以通过上述缓冲区队列操作来处理数据。
执行状态有3个子状态:刷新(Flushed), 运行(Running) 和流结束(End-of-Stream)。在调用start()
方法之后,编解码器立即进入 Flushed 子状态,在此状态之下,编解码器持有所有的缓冲区。一旦第一个输入缓冲区出队(dequeued),编解码器转移到 Running 子状态,编解码器的整个生命周期大部分时间处于此状态。当你使用end-of-stream marker标记入队一个输入缓冲区的时候,编解码器转移到 End-of-Stream 子状态。在此状态下,编解码器不再接收新的输入缓冲区,但是依然会生成输出缓冲区,直到在输出端达到流结束为止。当编解码器处于执行状态的时候,你可以在任何时间使用flush()方法返回到 Flushed 子状态。
调用stop()方法可以使编解码器回到未初始化的状态,然后可以被重新配置。当你使用编解码器完成了所有的操作,你必须调用release()方法来释放它。
在极少数情况下,编解码器可能会遇到错误并进入 Error 状态。在入队操作的时候通过返回一个不合法的值或者通过一个异常来传达此信息。通过调用reset()方法来使编解码器可以重新使用。你可以从任何状态下调用此方法来让编解码器回到未初始化的状态。也可以调用release()方法将编解码器移动到已释放的状态。
创建
使用MediaCodecList来为指定的MediaFormat格式 创建一个 MediaCodec 。 当解码一个文件或流的时候,你可以从MediaExtractor.getTrackFormat获取想要的格式。使用MediaFormat.setFeatureEnabled注入你想要添加的特定功能,然后调用MediaCodecList.findDecoderForFormat方法来获取可以处理特定媒体格式的编解码器的名称。最后,使用createByCodecName(String)来创建编解码器。
注意:在Build.VERSION_CODES.LOLLIPOP版本上,MediaCodecList.findDecoder/EncoderForFormat不能包含帧率(frame rate)。使用format.setString(MediaFormat.KEY_FRAME_RATE, null)来清除媒体格式中和帧率相关的设置。
你也可以使用createDecoder/EncoderByType(java.lang.String)为特定的MIME类型创建首选编码器。但是,这不能用于注入功能,并且可能无法处理特定的媒体格式。
创建安全的解码器
在Build.VERSION_CODES.KITKAT_WATCH以及更早的版本上,安全编解码器可能未在MediaCodecList中列出,但在系统上仍然可用。安全编解码器只能通过名称实例化,通过在常规的编解码器的名称后面添加".secure"(安全的编解码器必须以".secure"结尾),当系统中没有相应的编解码器的时候createByCodecName(String)方法会抛出一个IOException
从Build.VERSION_CODES.LOLLIPOP开始,你使用媒体格式中的CodecCapabilities#FEATURE_SecurePlayback
功能来创建一个安全的解码器。
初始化
在编解码器创建之后,你如果想异步处理数据的话,你可以使用setCallback方法设置一个回调。然后使用指定的媒体格式配置(configure)编解码器。这时候你可以你可以为视频生产者(生成原始视频数据的编解码器)指定输出Surface。这时候你也可以为安全的编解码器(see MediaCrypto)
指定解密参数。最后,由于一个编解码器可以运行多种模式,你必须指定你想让编解码器作为一个解码器还是作为一个编码器工作。
在Build.VERSION_CODES.LOLLIPOP之后,你可以在已配置的状态下查询输入和输出格式。你可以使用这种方式在启动编解码器之前来验证配置的结果,例如色彩模式等等。
如果你想使用一个视频编解码器以本地方式(natively)处理原始的视频输入,你可以在配置状态之后使用createInputSurface()为输入数据创建一个目标Surface(destination Surface)。
或者通过调用setInputSurface(Surface)来设置编解码器使用先前创建的持久性输入surface(persistent input surface)。
编解码器特定的数据(Codec-specific Data)
一些格式,尤其是AAC音频和MPEG4、H.264和H.265视频格式,要求实际数据以包含设置数据(setup data)或编解码器特定数据的多个缓冲区作为前缀。处理此类压缩格式时,必须在调用start()方法之后和在提交任何帧(Frame)数据之前将此数据提交给编解码器。在调用queueInputBuffer方法的时候,此类数据必须使用BUFFER_FLAG_CODEC_CONFIG进行标记。
编码器特定数据也可以在包含在传递给configure方法的MediaFormat中。这些特定数据是MediaFormat中以"csd-0", "csd-1"等键对应的ByteBuffer条目。这些键通常包含在从MediaExtractor中获取到达轨道格式中(track MediaFormat)。
编解码器特定数据在编解码器start()之后会自动提交给编解码器。你必须不能显式提交这些数据。如果格式不包含编码器特定数据,你可以根据格式要求,选择使用特定数字的缓冲区以正确的顺序提交它(编码器特定数据)。对于H.264 AVC,您还可以连接所有编解码器特定数据,并将其作为单个编解码器配置缓冲区提交。
Android使用以下特定于编解码器的数据缓冲区。为了正确的MediaMuxer轨道配置,这些格式也被要求设置到轨道格式中。每个参数集和标有(*)的编解码器特定数据段均必须以"\x00\x00\x00\x01"开头。
具体的表格可以参考原文:Codec-specific Data
注意:如果在start()之后,在任何输出任何缓冲区或输出格式改变返回之前刷新(flushed)编解码器的时候要格外小心,因为编解码器特定数据在刷新过程中可能会丢失。你必须在这样的刷新操作之后,使用以BUFFER_FLAG_CODEC_CONFIG标记的缓冲区来重新提交编解码器特定数据,以保证编解码器正常工作。
编码器(或生成压缩数据的编解码器)将在的输出缓冲区中的任何有效输出缓冲区之前,创建并返回标有codec-config flag标志的编解码器特定数据(通俗一点说,编码器最先输出的是有codec-config flag标记的编解码特定数据)。包含编解码器特定数据的缓冲区没有有意义的时间戳。
数据处理
每个编解码器维护一组输入和输出缓冲区,在API调用的时候,可以通过缓冲区ID(buffer-ID)获取一个输入/输出缓冲区。类似这样:
private ByteBuffer[] mCachedInputBuffers; private ByteBuffer[] mCachedOutputBuffers; //通过buffer-ID获取一个ByteBuffer int bufferID = ...; ByteBuffer buffer = mCachedInputBuffers[bufferID]

成功调用start()方法后,客户端并没有“拥有”输入缓冲区,也没有“拥有”输出缓冲区。在同步模式下,调用dequeueInput/OutputBuffer(…)从编解码器获取一个输入或者输出缓冲区。在异步模式下,你会通过MediaCodec.Callback.onInput/OutputBufferAvailable(…)这两个回调自动获取可以用的缓冲区。
获取一个输入缓冲区,向其中填充数据然后使用queueInputBuffer方法将输入缓冲区提交给编解码器。如果使用解密(decryption)的话,可以使用queueSecureInputBuffer方法提交。不要提交带有相同时间戳的多个输入缓冲区(除非是标记为编解码器特定数据)。
反过来,编解码器会返回一个只读的输出缓冲区,在异步模式下,在onOutputBufferAvailable回调中返回,同步模式下通过调用dequeueOutputBuffer返回。在输出缓冲区已经被处理完毕以后,调用releaseOutputBuffer系列方法之一将输出缓冲区返回给编解码器。
尽管不需要立即将缓冲区重新提交/释放给编解码器,但是持有输入和/或输出缓冲区可能会使编解码器停顿,并且此行为与设备无关。具体来说,编解码器可能会推迟生成输出缓冲区,直到所有未完成的缓冲区都已释放/重新提交为止 。因此,请尝试尽可能少的持有可用缓冲区。(通俗一点说:通过bufferID获取一个输入缓冲区以后,填充完数据后应该立即提交给编解码器,通过bufferID获取一个输出缓冲区后,消费了其中的数据以后也要尽快释放给编解码器,用完给别人用,不要老是占着,哈哈)。
根据API版本,你有三种方式可以处理数据。
处理模式 API version <= 20Jelly Bean/KitKat API version >= 21Lollipop and later
同步模式,使用缓冲区数组 支持 过时
同步模式,使用缓冲区 不可用 支持
异步模式,使用缓冲区 不可用 支持
异步处理,使用缓冲区 在Build.VERSION_CODES.LOLLIPOP以后,异步处理数据是首选的方式,通过在调用configure之前设置一个回调。异步模式稍微改变了编解码器的状态转换流程,因为你
必选在flush()之后调用start()方法将编解码器的状态转变到运行子状态来开始接受输入缓冲区。类似的,首次调用start()方法也会将编解码器的状态直接转变到运行子状态并开始通过回调传递可用的输出缓冲区。
MediaCodec概述
文章图片
异步处理数据状态机.png MediaCodec在异步模式下通常这样使用:
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(…) { … } }); //在调用config之前先设置回调 codec.configure(format, …); mOutputFormat = codec.getOutputFormat(); // option B codec.start(); // wait for processing to complete codec.stop(); codec.release();

同步处理,使用缓冲区 从Build.VERSION_CODES.LOLLIPOP以后,即使在同步模式下,你也应该使用getInput/OutputBuffer(int)和/或getInput/OutputImage(int)来获取输入和输出缓冲区。
这允许框架进行某些优化,例如处理动态内容的时候。如果你调用getInput/OutputBuffers()这种优化会被禁止。
注意: 不要同时使用缓冲区(buffers)和缓冲区数组(buffer arrays)的方法。特别地,只能在start()方法之后或者在编解码器已经出队了一个有INFO_OUTPUT_FORMAT_CHANGED值的输出缓冲区之后,直接调用getInput/OutputBuffers。(这段暂时不明白)
在同步模式下,MediaCodec的用法大概这样:
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();

同步处理,使用缓冲区数组( Buffer Arrays) 已经过时了 在Build.VERSION_CODES.KITKAT_WATCH版本及以前,输入和输出缓冲区的集合由ByteBuffer[]数组表示。成功调用start()后,使用getInput/OutputBuffers()来获取缓冲区数组。使用缓冲区ID作为缓冲区数组中的下标,如下例所示。请注意,虽然数组大小提供了一个上限,但数组大小与系统使用的输入和输出缓冲区的数量之间没有内在的相关性。
MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); codec.start(); ByteBuffer[] inputBuffers = codec.getInputBuffers(); ByteBuffer[] outputBuffers = codec.getOutputBuffers(); for (; ; ) { //缓冲区ID int inputBufferId = codec.dequeueInputBuffer(…); if (inputBufferId >= 0) { // fill inputBuffers[inputBufferId] with valid data … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) { // outputBuffers[outputBufferId] is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { outputBuffers = codec.getOutputBuffers(); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // Subsequent data will conform to new format. MediaFormat format = codec.getOutputFormat(); } } codec.stop(); codec.release();

流结束处理 当到达输入数据的末尾时,必须调用queueInputBuffer方法并指定BUFFER_FLAG_END_OF_STREAM标记来通知编解码器。你可以在最后一个有效的输入缓冲区上执行此操作,也可以额外提交一个设置了BUFFER_FLAG_END_OF_STREAM标记的空的输入缓冲区来实现。如果使用空的缓冲区的话,会忽略时间戳。
编解码器将继续返回输出缓冲区,直到它在dequeueOutputBuffer中的输出缓冲区设置了BUFFER_FLAG_END_OF_STREAM标记或者在onOutputBufferAvailable回调中设置了BUFFER_FLAG_END_OF_STREAM标记。你可以在最后一个有效的输出缓冲区上执行此操作,也可以额外输出一个设置了BUFFER_FLAG_END_OF_STREAM标记的空的输出缓冲区来实现。如果使用空的缓冲区的话,会忽略时间戳。
除非刷新、停止或重新启动了编解码器,否则请在发出输入流结束信号后不要提交其他输入缓冲区。
使用输出表面(Surface) 使用输出Surface处理数据的方式和使用ByteBuffer模式几乎一致;但是,没有可访问的输出缓冲区,getOutputBuffer/Image(int)会返回null,getOutputBuffers()返回的缓冲区数组中的值也都是null。
当使用使用输出表面的时候,你可以选择每个输出缓冲区是否渲染到Surface上。你有三种选择:
  • 不渲染:调用releaseOutputBuffer(bufferId, false)
  • 使用默认的时间戳渲染:调用releaseOutputBuffer(bufferId, true)
  • 使用指定的时间戳渲染:调用releaseOutputBuffer(bufferId, timestamp)
自Build.VERSION_CODES.M以后,默认的时间戳是缓冲区presentation timestamp显示时间戳(转化成微妙)。在Build.VERSION_CODES.M之前没有定义。
另外,从Build.VERSION_CODES.M以后,可以调setOutputSurface方法来动态改变输出表面。
将输出缓冲区渲染到Surface的时候,可以配置Surface丢弃过多的帧(没有被Surface及时消耗掉的帧)。或者配置不丢弃过多的帧。如果配置不丢弃过多的帧,如果Surface不能快速的消耗输出帧,最终会阻塞解码器。在Build.VERSION_CODES.Q之前,除了视图Surface(SurfaceView 或 TextureView)总是丢弃过多的帧意外,没有定义Surface准确的行为。在Build.VERSION_CODES.Q之后,默认的行为是丢弃过多的帧。应用可以为非视图Surface(例如ImageReader 或 SurfaceTexture)取消这种默认行为。实现方式是将target sdk 指定为Build.VERSION_CODES.Q并在配置格式的时候,将MediaFormat#KEY_ALLOW_FRAME_DROP键设置为0
渲染到表面的变换
如果编解码器配置为Surface模式,则将自动应用任何裁剪矩形,旋转和视频缩放模式,但以下情况除外:
在Build.VERSION_CODES.M发布之前,软件解码器在渲染到Surface上时可能尚未应用旋转。不幸的是,没有标准且简单的方法来识别软件解码器,或者是否通过尝试旋转来应用旋转。
也有一些警告:
请注意,在将输出显示到Surface时不考虑像素长宽比。这意味着,如果您使用的是VIDEO_SCALING_MODE_SCALE_TO_FIT模式,则必须对输出Surface进行定位,以使其最终具正确的显示宽高比。相反,您只能将VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式用于具有正方形像素(像素长宽比或1:1)的内容。
另请注意,自Build.VERSION_CODES.N正式版本以后,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能无法和旋转90度或270度的视频正常配合使用。
设置视频缩放模式时,请注意,每次更改输出缓冲区后必须将其重置。由于INFO_OUTPUT_BUFFERS_CHANGED事件已经废弃了,因此可以在每次输出格式更改后执行此操作。
使用输入表面 使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会自动从输入Surface传递到编解码器。调用dequeueInputBuffer将抛出IllegalStateException,getInputBuffers()返回一个不能写入的假的ByteBuffer[]数组。
调用signalEndOfInputStream()通知流结束。调用后,输入表面将立即停止向编解码器提交数据。
快进和自适应性播放支持 视频解码器在快进(seek)和格式更改方面的行为不同,无论它们是否支持并配置为自适应播放。你可以通过CodecCapabilities.isFeatureSupported(String)检查一个解码器是否支持adaptive playback。只有将解码器配置为将数据解码到Surface的时候,视频解码器的自适应播放才会被激活。
流边界和关键帧 在调用start()flush()之后,在一个合适的流边界开始输入数据是非常重要的:第一帧必须是一个关键帧。一个关键帧可以完全独立的被解码(对大多数编解码器来说这意味着一个I帧),并且在一个关键帧之后显示的帧不会参考/引用(refer to)此关键帧之前的帧。
下表总结了适用于各种视频格式的关键帧。
参考原文:Stream Boundary and Key Frames
对于不支持自适应播放(包括不是将数据解码到一个Surface上)的解码器 为了解码和先前提交的数据不相邻的数据(例如在快进之后)你必须刷新解码器。由于所有输出缓冲区都会在刷新时立即被撤销,因此您可能想在调用flush之前先发出信号,然后等待流结束。刷新后的输入数据在合适的流边界/关键帧处开始很重要。
注意:刷新后提交的数据格式不得更改;flush()不支持格式不连续;因此如果格式发生了变化,一个完整的stop()- configure(…)-start()周期是必要的。
也请注意:如果在start()之后过早刷新编解码器(通常在收到第一个输出缓冲区或输出格式更改之前),则需要将编解码器特定数据重新提交给编解码器。有关更多信息,请参见编解码器特定数据部分。
对于支持并配置为自适应播放的解码器 为了开始解码与先前提交的数据不相邻的数据(例如在快进之后),没有必要刷新解码器;但是,不连续之后的输入数据必须从合适的流边界/关键帧开始。
对于某些视频格式-即H.264,H.265,VP8和VP9-也可以在流中间更改图像大小或配置。为此,您必须将整个新的编解码器特定的配置数据与关键帧一起打包到一个缓冲区(包括任何起始代码)中,并将其作为常规输入缓冲区提交。
在图像大小发生更改之后,在返回具有新的尺寸的任何帧之前, 您将在dequeueOutputBuffer
或onOutputFormatChanged回调中收到INFO_OUTPUT_FORMAT_CHANGED的返回值。
注意:
就像编解码器特定数据一样,在更改图像大小后立即调用flush()时要小心 。如果您尚未收到图像尺寸更改的确认,则需要重新请求新的图像尺寸。
错误处理
createByCodecName和createDecoder/EncoderByType在失败的时候会抛出IOException,你必须捕获或者向上传递。当在编解码器不允许的状态调用这些方法的时候,MediaCodec会抛出IllegalStateException;这通常是因为不正确的应用API使用。和安全缓冲区相关的方法可能会抛出CryptoException,可以从CryptoException#getErrorCode获取进一步的错误信息。
即使应用正确使用了API,也可能因为媒体内容损坏,硬件错误,资源耗尽等,导致内部编解码器错误,内部编解码器错误会抛出CodecException。推荐的操作是在收到
CodecException的时候通过调用CodecException#isRecoverable和CodecException#isTransient确定是哪种类型的错误。
  • 可恢复的错误:如果isRecoverable()返回true,可以调用stop(), configure(…), 和 start()从失败中恢复。
  • 暂时错误:如果isTransient()返回true,表明是资源暂时不可用,相应的方法会重试。
  • 致命错误:如果isRecoverable()isTransient()同时返回false,那么CodecException就是致命错误,编解码器必须被重置(reset)和释放(released)。
isRecoverable()isTransient()不会同时返回true。
【MediaCodec概述】参考链接:
  • MediaCodec

    推荐阅读