MediaCodec中文API

MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。 它是Android低级多媒体支持架构的一部分(通常与MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用)。

MediaCodec中文API_第1张图片
QQ截图20170711163031.png

广义地说,编解码器处理输入数据以产生输出数据。 它异步处理数据并使用一组输入和输出缓冲区。 在一个简单的层面上,您请求(或接收)一个空的输入缓冲区,填写数据并将其发送到编解码器进行处理。 编解码器使用数据并将其发送到一个空输出缓冲区。 最后,您请求(或接收)一个已填充的输出缓冲区,消耗其内容并将其释放回编解码器。

数据类型

编解码器对三种数据进行操作:压缩数据,原始音频数据和原始视频数据。 可以使用ByteBuffers处理所有三种数据,但您应该使用Surface对原始视频数据进行改进,以提高编解码器的性能。 Surface使用本地视频缓冲区,无需映射或复制到ByteBuffers; 因此,效率更高。 通常在使用Surface时无法访问原始视频数据,但您可以使用ImageReader类来访问不安全的解码(原始)视频帧。 这可能比使用ByteBuffers更有效率,因为一些本机缓冲区可能被映射到直接ByteBuffers。 当使用ByteBuffer模式时,您可以使用Image类和getInput / OutputImage(int)访问原始视频帧。

压缩缓冲区

输入缓冲器(用于解码器)和输出缓冲器(用于编码器)包含指定格式类型(format's type)的压缩数据。 对于视频类型,这是一个单一的压缩视频帧。 对于音频数据,这通常是单个访问单元(通常包含由格式类型指定的几毫秒音频的编码音频段),但是这个要求稍微放松,因为缓冲器可以包含多个编码的音频访问单元。 在任一情况下,缓冲区不会以任意字节边界开始或结束,而是在帧/访问单元边界上。

原始音频缓冲区

原始音频缓冲器包含PCM音频数据的整个帧,这是通道顺序中每个通道的一个样本。 每个样本是一个16位有符号整数,以本地字节顺序排列。

 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 = formet.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模式下,视频缓冲区根据其颜色格式进行布局。 您可以从getCodecInfo().getCapabilitiesForType(...).colorFormats获取支持的颜色格式作为数组。 视频编解码器可能支持三种颜色格式:

原生视频格式:由COLOR_FormatSurface标记,可与输入或输出Surface一起使用。

灵活的YUV缓冲区(例如COLOR_FormatYUV420Flexible):可与输入或输出Surface一起使用,也可以通过使用getInput / OutputImage(int)在ByteBuffer模式中使用。

其他特定格式:通常只能在ByteBuffer模式下支持。 一些颜色格式是供应商特定的。 其他的在MediaCodecInfo.CodecCapabilities中定义。 对于等同于灵活格式的颜色格式,您仍然可以使用getInput / OutputImage(int)。

从LOLLIPOP_MR1开始,所有视频编解码器都支持灵活的YUV 4:2:0缓冲区。

访问旧设备上的原始视频字节缓冲区
在LOLLIPOP和Image支持之前,您需要使用KEY_STRIDE和KEY_SLICE_HEIGHT输出格式值来了解原始输出缓冲区的布局。
请注意,在某些设备上,切片高度被标示为0.这可能意味着切片高度与帧高度相同,或者切片高度与帧高度对齐为某个值(通常是2)。 不幸的是,在这种情况下,没有标准和简单的方式来说明实际的切片高度。 此外,平面格式的U平面的垂直步长也没有被指定或定义,尽管通常它是切片高度的一半。

KEY_WIDTH和KEY_HEIGHT键指定视频帧的大小; 然而,对于大多数编码,视频(图片)仅占据视频帧的一部分。 这由“裁剪矩形”表示。

您需要使用以下键从输出格式获取原始输出图像的裁剪矩形。 如果这些键不存在,则视频占据整个视频帧。在应用任何旋转之前,裁剪矩形在输出帧的上下文中被读取。

Format Key Type Description
"crop-left" Integer The left-coordinate (x) of the crop rectangle
"crop-top" Integer The top-coordinate (y) of the crop rectangle
"crop-right" Integer The right-coordinate (x) MINUS 1 of the crop rectangle
"crop-bottom" Integer The bottom-coordinate (y) MINUS 1 of the crop rectangle

右和底坐标可以被理解为裁剪的输出图像的最右边有效列/最有效行的坐标。

视频帧的大小(旋转之前)可以这样计算:

 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的含义在设备之间是不一致的。 在某些设备上,偏移指向裁剪矩形的左上角像素,而在大多数设备上,它指向整个帧的左上角像素。

状态

MediaCodec中文API_第2张图片
QQ截图20170712094108.png

在其生命周期中,编解码器概念上存在于三种状态之一:停止,执行或发布。 停止的集体状态实际上是三个状态的集合:未初始化,配置和错误,而执行状态在概念上通过三个子状态进行:冲洗,运行和流结束。

当您使用工厂方法之一创建编解码器时,编解码器处于未初始化状态。首先,您需要通过configure(...)进行配置,使其进入Configured状态,然后调用start()将其移动到执行状态。在这种状态下,您可以通过上述缓冲区队列处理来处理数据。

执行状态有三个子状态:冲洗,运行和流水。在start()之后立即,编解码器处于Flushed子状态,它保存所有缓冲区。一旦第一个输入缓冲区出现,编解码器就会移动到运行的子状态,在其中大部分时间都会占用它。当您使用流末尾标记对输入缓冲区进行排队时,编解码器将转换到流末端子状态。在这种状态下,编解码器不再接受进一步的输入缓冲区,但仍然产生输出缓冲区,直到输出端到达流。在执行状态下,使用flush()可以随时返回Flushed子状态。

调用stop()将编解码器返回到未初始化状态,然后可以重新配置。完成使用编解码器后,您必须通过调用release()来释放它。

在极少数情况下,编解码器可能会遇到错误并转到错误状态。这是使用来自排队操作的无效返回值或有时通过异常来传送的。调用reset()使编解码器再次可用。您可以从任何状态调用它来将编解码器移回未初始化状态。否则,呼叫release()移动到终端释放状态。

创建

使用MediaCodecList为特定的MediaFormat创建一个MediaCodec。解码文件或流时,您可以从MediaExtractor.getTrackFormat获取所需的格式。使用MediaFormat.setFeatureEnabled注入要添加的任何特定功能,然后调用MediaCodecList.findDecoderForFormat以获取可以处理该特定媒体格式的编解码器的名称。最后,使用createByCodecName(String)创建编解码器。

注意:在LOLLIPOP上,MediaCodecList.findDecoder / EncoderForFormat的格式不能包含帧速率。使用format.setString(MediaFormat.KEY_FRAME_RATE,null)以格式清除任何现有的帧速率设置。

您也可以使用createDecoder / EncoderByType(String)为特定MIME类型创建首选编解码器。然而,这不能用于注入特征,并且可能创建无法处理特定所需媒体格式的编解码器。

创建安全解码器

在版本KITKAT_WATCH及更早版本中,安全编解码器可能未列在MediaCodecList中,但仍可能在系统上可用。存在的安全编解码器可以仅通过名称实例化,通过将“.secure”附加到常规编解码器的名称(所有安全编解码器的名称必须以“.secure”结尾)。如果createByCodecName(String)抛出IOException,编解码器不存在于系统上。

从LOLLIPOP开始,您应该使用媒体格式的FEATURE_SecurePlayback功能来创建安全解码器。

初始化

创建编解码器后,如果要异步处理数据,则可以使用setCallback设置回调。然后,使用特定的媒体格式配置编解码器。这时您可以为视频制作者指定输出曲面 - 生成原始视频数据的编解码器(例如视频解码器)。这时您也可以设置安全编解码器的解密参数(请参阅MediaCrypto)。最后,由于某些编解码器可以在多种模式下运行,因此您必须指定是否要将其用作解码器或编码器。

从版本LOLLIPOP开始,您可以在Configured状态下查询生成的输入和输出格式。您可以使用它来验证结果配置,例如开始编解码器之前的颜色格式。

如果要用本机视频消费者处理原始输入视频缓冲区,则处理原始视频输入的编解码器(如视频编码器)可在配置后使用createInputSurface()创建输入数据的目标Surface。或者,通过调用setInputSurface(Surface)来设置编解码器以使用先前创建的持久输入Surface。

编解码器特定数据
一些格式,特别是AAC音频和MPEG4,H.264和H.265视频格式要求实际数据以包含设置数据或编解码器特定数据的缓冲区为前缀。在处理这种压缩格式时,必须在start()之后和任何帧数据之前将该数据提交到编解码器。在调用queueInputBuffer时,必须使用标志BUFFER_FLAG_CODEC_CONFIG标记此类数据。

特定的解码数据也可以包含在使用密钥“csd-0”,“csd-1”等等的ByteBuffer条目中传递的格式中。这些密钥始终包含在从MediaExtractor获取的TrackField中。start()时,格式中的特定解码数据被自动提交到解码器中;您不用明确提交此数据。如果格式不包含编解码器特定数据,则可以根据格式要求选择使用指定数量的缓冲区以正确的顺序提交它。在H.264 AVC的情况下,您也可以连接所有特定于编解码器的数据,并将其作为单个编解码器配置缓冲区提交。

Android使用以下特定于编解码器的数据缓冲区。这些还需要以跟踪格式设置,以便适当的MediaMuxer跟踪配置。标记为(*)的每个参数集和编解码器特定数据段必须以起始代码“\ x00 \ x00 \ x00 \ x01”开头。

MediaCodec中文API_第3张图片
QQ截图20170712102732.png

注意:在任何输出缓冲区或输出格式更改已被返回之前,如果编解码器立即刷新或启动后不久刷新,则必须小心,因为在刷新过程中编解码器特定数据可能会丢失。 必须在这样的刷新之后使用标有BUFFER_FLAG_CODEC_CONFIG的缓冲区重新提交数据,以确保正确的编解码器操作。

编码器(或生成压缩数据的编解码器)将在标有codec-config标志的输出缓冲区中的任何有效输出缓冲区之前创建并返回编解码器特定数据。 包含编解码器特定数据的缓冲区没有有意义的时间戳。

数据处理

每个编解码器维护一组在API调用中由缓冲区ID引用的输入和输出缓冲区。在成功调用start()后,客户端既不拥有输入也不拥有输出缓冲区。在同步模式下,调用dequeueInput / OutputBuffer(...)从编解码器获取(获得)输入或输出缓冲区的所有权。在异步模式下,您将通过MediaCodec.Callback.onInput / OutputBufferAvailable(...)回调自动接收可用的缓冲区。

一旦获得输入缓冲区,请填写数据,并使用queueInputBuffer - 或queueSecureInputBuffer(如果使用解密)将其提交到编解码器。不要提交具有相同时间戳的多个输入缓冲区(除非是特定于编解码器的数据)。

编解码器又将以异步模式通过onOutputBufferAvailable回调返回一个只读输出缓冲区,或响应于同步模式下的dequeuOutputBuffer调用。在处理输出缓冲区之后,调用其中一个releaseOutputBuffer方法将缓冲区返回到编解码器。

当您不需要立即重新提交/释放缓冲区到编解码器时,保持输入和/或输出缓冲区可能会停止编解码器,并且此行为取决于设备。具体来说,编解码器可能会在产生输出缓冲区之前停止,直到所有未完成的缓冲区被释放/重新提交为止。因此,尽可能少地保持可用的缓冲区。

根据API版本,您可以通过三种方式处理数据:

MediaCodec中文API_第4张图片
QQ截图20170712125841.png

使用缓冲区的异步处理

从版本LOLLIPOP开始,首选方法是通过在调用configure之前设置回调来异步处理数据。 异步模式更改状态稍微转换,因为必须在flush()之后调用start()将编解码器转换为Running子状态并开始接收输入缓冲区。 类似地,在初始调用启动编解码器将直接移动到运行子状态,并开始通过回调传递可用的输入缓冲区。

MediaCodec中文API_第5张图片
QQ截图20170712125857.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(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

使用缓冲器的同步处理

从LOLLIPOP开始,即使在同步模式下使用编解码器,也应该使用getInput / OutputBuffer(int)和/或getInput / OutputImage(int)来检索输入和输出缓冲区。 这允许框架进行某些优化,例如。 处理动态内容时。 如果调用getInput / OutputBuffers(),则此优化将被禁用。

注意:不要同时混合使用缓冲区和缓冲区数组的方法。 具体来说,只有在start()之后才能直接调用getInput / OutputBuffers,或者在输出缓冲区ID的值为INFO_OUTPUT_FORMAT_CHANGED之后才调用。

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();

使用缓冲区数组的同步处理(不建议使用)

在版本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 (;;) {
   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标志来将其发送到编解码器。您可以在最后一个有效的输入缓冲区中执行此操作,也可以通过提交一个额外的空输入缓冲区来设置流终止标志。如果使用空的缓冲区,则时间戳将被忽略。

编解码器将继续返回输出缓冲区,直到通过在dequeueOutputBuffer中设置的MediaCodec.BufferInfo中指定相同的流出尾标志,或者通过onOutputBufferAvailable返回,直到它最终显示输出流的结尾。这可以在最后一个有效的输出缓冲区上设置,也可以在最后一个有效输出缓冲区之后的空缓冲区中设置。这样的空缓冲区的时间戳应该被忽略。

在信号输入流结束后,请勿提交其他输入缓冲区,除非编解码器已被刷新,或停止并重新启动。

使用输出Surface

当使用输出Surface时,数据处理与ByteBuffer模式几乎相同;然而,输出缓冲区将不可访问,并被表示为空值。例如。 getOutputBuffer / Image(int)将返回null,而getOutputBuffers()将返回一个仅包含null的数组。

使用输出Surface时,可以选择是否在表面上呈现每个输出缓冲区。你有三个选择:

不要渲染缓冲区:调用releaseOutputBuffer(bufferId,false)。
使用默认时间戳渲染缓冲区:调用releaseOutputBuffer(bufferId,true)。
使用特定时间戳渲染缓冲区:调用releaseOutputBuffer(bufferId,timestamp)。

Since M,默认时间戳是缓冲区的显示时间戳(转换为纳秒)。之前没有定义。

此外,Since M,您可以使用setOutputSurface动态更改输出Surface。

渲染到表面时的转换

如果编解码器配置为Surface模式,任何裁剪矩形,旋转和视频缩放模式都将自动应用,但有一个例外:
在M版本之前,当渲染到Surface上时,软件解码器可能没有应用旋转。不幸的是,没有标准和简单的方法来识别软件解码器,或者通过尝试的方法应用旋转。

还有一些注意事项。

请注意,当在Surface上显示输出时,不考虑像素长宽比。这意味着如果您使用VIDEO_SCALING_MODE_SCALE_TO_FIT模式,则必须定位输出Surface,使其具有正确的最终显示宽高比。相反,您只能对具有方形像素(像素宽高比或1:1)的内容使用VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式。

还要注意,从N版本起,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能会对旋转90或270度的视频无法正常工作。

当设置视频缩放模式时,请注意,每次输出缓冲区更改后,必须重新设置。由于INFO_OUTPUT_BUFFERS_CHANGED事件已被弃用,因此可以在每次输出格式更改后执行此操作。

使用输入Surface

当使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会从输入表面自动传递到编解码器。调用dequeueInputBuffer将抛出一个IllegalStateException,getInputBuffers()返回一个不能被写入的伪造的ByteBuffer []数组。

调用signalEndOfInputStream()来发送信号流。输入Surface将在此呼叫后立即停止向编解码器提交数据。

寻求和自适应播放支持

视频解码器(以及一般使用压缩视频数据的编解码器)在搜索和格式更改方面的行为方式不同,无论它们是否支持并配置为自适应播放。 您可以通过CodecCapabilities.isFeatureSupported(String)来检查解码器是否支持自适应播放。 视频解码器的自适应播放支持只有在将编码解码器配置到Surface上才被激活。

流边界和关键帧
重要的是,start()或flush()之后的输入数据在适当的流边界开始:第一帧必须是关键帧。 关键帧可以自己完全解码(对于大多数编解码器,这意味着I帧),并且在关键帧之后没有要显示的帧指的是关键帧之前的帧。

下表总结了各种视频格式的合适关键帧

MediaCodec中文API_第6张图片
QQ截图20170712133335.png

对于不支持自适应播放的解码器(包括不在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使用不正确。涉及安全缓冲区的方法可能会丢失MediaCodec.CryptoException,该错误信息可从getErrorCode()获取。

内部编解码器错误导致MediaCodec.CodecException,这可能是由于媒体内容损坏,硬件故障,资源耗尽等,即使应用程序正确使用API​​。接收CodecException时的建议操作可以通过调用isRecoverable()和isTransient()来确定:

可恢复的错误:如果isRecoverable()返回true,则调用stop(),configure(...)和start()来恢复。
瞬态错误:如果isTransient()返回true,则资源暂时不可用,并且可能会在稍后重试该方法。
致命错误:如果isRecoverable()和isTransient()都返回false,则CodecException是致命的,并且必须重置或释放编解码器。
isRecoverable()和isTransient()都不会同时返回true。

你可能感兴趣的:(MediaCodec中文API)