android中MediaCodec类解析

作者: 昵称真难选 | 来源:发表于2017-05-07 00:43 被阅读2396次

    原文出自王艳涛的专栏转载请注明出处!

    简介

    MediaCodec类可以获取底层媒体编码/解码库,是Android底层多媒体支持库的一部分(一般和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack搭配使用)。


    MediaCodec Buffers流

    宽泛的说,codec(编解码器)通过异步的方式对输入的数据进行处理,输出处理后的数据,过程中需要一系列的输入/输出Buffers。最简单的情况下,先把数据放进一个空的输入buffer(申请或者获取得到)中,发送给codec,codec对数据进行处理转换后放进一个输出buffer中,拿到输出buffer后,自行处理输出buffer中的数据,之后释放输出buffer并返回给codec。

    数据类型

    codec可以操作三种数据:压缩过的数据、raw格式audio数据、raw格式video数据。

    • 这三种数据都可以用ByteBuffers进行处理。
    • 但是处理raw video数据时,应该使用Surface以增强性能。Surface使用native层buffer,没有经过映射、拷贝到ByteBuffers,所以效率更高。正常情况下,使用Surface时获取不到raw格式的video数据,需要通过ImageReader类获取raw格式的vedio帧数据,由于native层的buffers可以直接映射为ByteBuffer,所以效率依然很高。
    • 当使用ByteBuffer模式时,通过Image类的getInput()/OutPutImage(int)方法,就可以得到raw格式的video帧数据。

    Commpressed Buffer

    输入buffers和输出buffers根据格式类型包含不同的压缩数据,对于vedio,buffer中是视频的一帧数据;对于audio来说,一般下是一个访问单元(若干毫秒的audio)数据,也有可能是多个访问单元的数据。任何情况下,buffer只在帧或者访问单元的边界上开始或结束,不会在任一字符边界开始或者结束。

    Raw Audio Buffers

    Raw Audio buffers包含了PCM audio数据的整个帧,是音频通道中每个通道的样本。每个样本在都是一个用native byte顺序存放的16-bit signed integer。

    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;
    }
    

    Raw Video Buffers

    在ByteBuffer模式下,video buffers是根据其颜色格式存放的,通过getCodeInfo().getCapabilitiesForType(……)可以获取支持的颜色格式数组。Video codecs支持以下三种颜色格式。

    • native raw videoformat:通过COLOR_FormatSurface标记的格式,可以与Surface的输入/输出一起使用
    • flexible YUV buffers(例如COLOR_FormatYUV420Flexible):可以同时与Surface的输入/输出和ByteBuffer模式下使用。
    • 其他具体的格式:一般只支持ByteBuffer模式。其中一些是厂家的具体格式,另外的声明在MediaCodecInfo.CodecCapabilities。对于和flexible格式相同的颜色格式,仍然可以使用getInput/OutPutImage(int)。

    从Android LOLLIPOP_MR1开始,所有的video codecs都支持flexible YUV4::2:0 buffers

    在老设备上获取Raw Video ByteBuffers

    在Android LOLLIPOP和Image支持之前,需要通过输出格式参数:KEY_STRIDE和DEY_SLICE_HEIGHT来描述raw格式的输出buffers。

    在一些设备上切片高度被标示为0。折意味着切片的高度要么与帧的高度一样,要么与切片的高度的某个值对其(通常为2的幂)。但是,这种情况下没有一个标准或简单的方法分辨切片的实际高度。而且,U平面竖直方向上的格式也没有被指定或者定义,通常为切片高度的一半。

    参数KEY_WIDTH和DEY_HEIGHT指定video帧的尺寸,但是大多数的编码的video(picture)只是video帧的一部分,由裁剪矩形表示。
    使用下面一些参数可以获取raw output images的裁剪矩形,这些参数通过outputformat获取。如果这些参数不存在,video将占据整个video帧。在应用任何旋转操作之前,裁剪矩形在输出帧的context中被解释。

    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

    The right and bottom coordinates can be understood as the coordinates of the right-most valid column/bottom-most valid row of the cropped output image.

    旋转之前的video帧的尺寸可以通过如下方式计算:

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

    状态

    在codec的生命周期中,存在三种状态:

    1. Stopped:包含Uninitialized、Configured、Error三种子状态。
    2. Executing:包含Flushed、Running、End-of-Stream三种子状态。
    3. Released。
    MediaCodec状态
    1. 当用工厂方法床架一个codec时,codec处于Uninitialized状态。首先通过configure(……)方法注册codec,使其处于Configure状态;然后调用start()方法使其进入Executing状态,在这个状态下,就可以通过buffer队列处理数据。
    2. Executing有三个子状态:Flushed、Running、End-of-Stream。在调用start()之后codec立即今日Flushed状态,持有所有的buffers;当从buffer队列拿到第一个输入buffer时,codec进入Running状态,codec整个生命周期的大部分时间都处在这个状态;当输入buffer是end-of-stream标示时,codec切换到End-of-Stream状态,这个状态下,codec不再接收输入buffer,但是仍然生成输出buffer,直到遇见输出buffer的end-of-stream标示。在Executing状态下,可以通过调用flush()方法,使codec在任何时候切换回Flushed状态。
    3. 当调用stop()方法后,codec返回到Uninitialized状态,可以再次Configured。在codec使用结束后,必须调用release()方法释放codec。
    4. 在极少数情况下,codec可能遇到error进入Error状态。这是队列操作的无效返回值或者exception进行通讯的。这时可以调用reset()方法就可以使codec再次可用,也可以在任何状态下调用reset()方法,使codec返回到Uninitialized状态,否则就应该调用release()方法释放。

    创建

    使用MediaCodecList创建一个制定媒体格式(MediaFormat)的MediaCodec。当解码文件或者流时,可以通过MediaExtractor.getTrackFormat获取所需的格式,如果需要注入特定的特性,可以通过MediaFormat.setFeatureEnabled添加;然后调用MediaCodecList.findDecoderForFormat获取能够处理这种媒体格式的codec的名称;最后,使用createByCodecName(String)创建codec。

    注意:在android LOLLIPOP中,给MediaCodecList.findDecoder/EncoderForFormat指定的格式一定不能包含帧速率。使用format.setString(MediaFormat.KEY_FRAME_RATE,null)来清除任何format中存在的帧速率。

    也可以通过createDecoder/EncoderByType(String)创建一个为特定MIME类型格式的codec,但是不能注入特性,而且可能创建一个处理不了指定媒体格式的codec。

    创建安全解码器

    在androi KITKAT_WATCH及更早的版本中,安全的codec可能不在MediaCodecList中,但是在系统中仍然可用。存在的安全codec可以通过名字+".secure"(所有的安全codec都必须以".secure"结尾)进行实例化。如果codec不存在,createByCodecName(String)将抛出一个IOException异常。
    从Android LOLLIPOP开始,应该在媒体格式中使用FEATURE_SecurePlayback特性创建安全解码器。

    初始化

    创建codec后,如果要异步处理数据,需要通过setCallback指定一个回调函数,然后使用特定的媒体格式配置一个codec。初始化时可以指定视频源输出到Surface、生成raw格式video data(如 video 解码器),设置安全codec的解码参数。由于某些codec可以在多种模式下运行,所以必须指定是否将其作为一个解码器或者编码器。
    从android LOLLIPOP,可以在Configured状态查询输入/输出的格式,然后在codec starting之前验证配置配置结果。
    如果要用raw video buffer的codec(如video编码器)本地处理raw格式的输入video buffer,可以在配置后通过createInputSurface()为输入数据创建一个目标Surface。或者建立一个codec通过调用setInputSurface(Suface)来使用已经创建过的持久化的输入Surface。

    格式中的Codec-specific Data

    一些格式,特别是AAC音频和MPEG4、H.264、H.265视频格式,要求实际数据要以Codec-specific Data或者buffer包含的设置数据的数字为前缀。处理这些压缩的格式时,Codec-specific Data数据必须在code start()之后、在任何帧数据到来之前提交给codec。在调用queueInputBuffer()时,必须对这些数据进行BUFFER_FLAG_CODEC_CONFIG标示。
    Codec-specific Data可以包含在传递给带有"csd-0","csd-1"等关键字的ByteBuffer条目的配置中,这些关键字始终包含在从MediaExtractor获取的MediaFormat中。格式中的Codec-specific Data在codec start()时会自动提交给codec,不要明确递交。如果格式中不包含,可以根据格式要求,使用buffer指定的数字,用正确的顺序选择提交。在H.264 AVC的情况下,还可以连接所有codec-specific data,并将其作为单个codec-config buffer提交。
    android中使用如下codec-specific data buffer。这些也需要按照适合MediaMuxer轨道配置的轨道格式进行设置。每个参数集和标有(*)的 codec-specific-data 都必须以起始代码“\ x00 \ x00 \ x00 \ x01”开头。

    Format CSD buffer #0 CSD buffer #1 CSD buffer #2
    AAC Decoder-specific information from ESDS* Not Used Not Used
    VORBIS Identification header Setup header Not Used
    OPUS Identification header Pre-skip in nanosecs

    (unsigned 64-bit native-order integer.)
    This overrides the pre-skip value in the identification header. | Seek Pre-roll in nanosecs
    (unsigned 64-bit native-order integer.)
    MPEG-4 | Decoder-specific information from ESDS* | Not Used | Not Used
    H.264 AVC | SPS (Sequence Parameter Sets) | PPS (Picture Parameter Sets) | Not Used
    H.265 HEVC | VPS (Video Parameter Sets) +
    SPS (Sequence Parameter Sets
    ) +
    PPS (Picture Parameter Sets*) | Not Used | Not Used
    VP9 | VP9 CodecPrivate Data (optional) | Not Used | Not Used

    注意:如果codec在输出buffer或者输出格式更改返回之前或者刚刚start()就被立即flushed,codec specific data可能在flush时丢失,必须使用 BUFFER_FLAG_CODEC_CONFIG标示buffer重新提交codec specific data,以保证codec正常工作。

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

    数据处理

    在调用api时,每个docec都持有一系列被buffer-ID引用的输入/输出buffre,当成功的start()后,客户端拥有既不输入也不输出的buffer。在同步模式下,调用dequeueInput/OutputBuffer(……)从codec中获取一个输入/输出buffer;在异步模式下,通过MediaCodec.Callback.onInput/OutputBufferAvailable(……)回调函数会自动接收到可用的buffer。
    在获取输入buffer时,放入数据后通过queueInputBuffer或者queueSecureInputBuffer(如果使用decryption)提交给codec。不要提交多个具有相同时间戳的输入buffer(除非是标记为 codec-specific data)
    在异步模式下,codec会通过onOutputBufferAvailable回调函数返回一个只读的输出buffer;在同步模式下,调用dequeuOutputBuffer函数获取只读的输出buffer。当输出buffer处理完毕后,调用其中一个的releaseOutputBuffer的方法,将buffer返回给codec。
    当不需要立即向codec提交或者释放buffer时,持有输入/输出buffer可能造成codec停止,视设备而定。具体来说,codec可能会推迟生成输出buffer,直到所有未完成的buffer都被提交或者释放,所以要尽少的持有可用的buffer。
    根据API的版本,有三种处理数据的方式:
    Processing Mode | API version <= 20
    Jelly Bean/KitKat | API version >= 21
    Lollipop and later
    ------------ | -------------|
    Synchronous API using buffer arrays | Supported | Deprecated
    Synchronous API using buffers | Not Available | Supported
    Asynchronous API using buffers | Not Available | Supported

    使用Buffer异步处理

    从android LOLLIPOP开始,首选异步处理数据,在调用configure之前,设置一个回调函数,异步模式下,因为要让codec进入Running子状态接收输入buffer,所以必须在flush()之后调用start(),这就会使codec状态发生改变。同样,在初始调用下启动的codec将会直接进入到Running子状态,通过回调函数开始传递可用的输入buffer。


    MediaCodec异步流程

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

    使用Buffers同步处理

    从android LOLLIPOP开始,即使在同步模式下使用codec也应该通过getInput/OutputBuffer(int)/getInput/OutputImage(int) 来获取输入/输出buffer,这样可以让框架层进行一些优化,如处理动态内容。如果使用getInput/OutputBuffers()则优化将禁用。

    不要同时混用buffer和buffer array的方法,具体来说,只有在start()之后直接调用getInput/OutputBuffers,或者dequeued一个值为 INFO_OUTPUT_FORMAT_CHANGED的输出buffer ID后才能混用。

    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同步处理(deprecated)

    在android KITKAT_WATCH及以前,输入/输出buffer的集合用ByteBuffer[]表示。在start()方法调用成功后,使用 getInput/OutputBuffers()检索数组,用buffer IDs(非负)作为索引,如下面代码所示。注意,尽管数组的大小有一个上限,但是数组的大小和系统使用的输入/输出buffer的数量没有固定的联系。

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

    处理 End-of-stream

    当到达输入数据的末尾时,在调用queueInputBuffer时,必须指定BUFFER_FLAG_END_OF_STREAM标志,发送给codec,可以在最后一个可用的输入buffer或者额外提交一个空的输入buffer设置end-of-stream标志。如果使用空buffer,其时间戳将被忽略。
    codec会持续返回输出buffer,直到收到带有与MediaCodec.BufferInfo中end-of-stream标志相同的dequeueOutputBuffer或者返回onOutputBufferAvailable时停止,可以在最后一个有效的输出buffer上设置,也可以在最后一个有效输出buffer之后的空buffer中设置。
    在输入流结束信号发送后,不要在提交额外的输入buffer,除非codec已经flushed或者stoped——restarted。

    使用输出Surface

    当使用输出Surface时,数据处理和ByteBuffer模式基本相同,但是不能获取输出buffer,值为null,如 getOutputBuffer/Image(int) 将会返回null;getOutputBuffers() 返回一个null数组。
    使用输出Surface时,可以选择是否在surface上渲染每一个输出buffer,有以下三种方式:

    • 不渲染buffer:调用releaseOutputBuffer(bufferId, false);
    • 根据默认时间戳渲染buffer:调用releaseOutputBuffer(bufferId, true);
    • 根据指定时间戳渲染buffer:调用releaseOutputBuffer(bufferId, timestamp)。

    从android M后,默认的时间戳是buffer的显示时间戳(纳秒),之前版本没有定义。另外,android M后,可以通过setOutputSurface动态改变输出Surface。

    渲染到Surface时的转换

    如果codec配置为Surface模式,任何裁剪矩形、旋转、缩放将会自动应用。

    在android M之前,渲染到Surface时,软解可能没有应用旋转,也没有标准或者简单的方法识别软解,只能尝试看是否已应用旋转。
    当渲染到Surface时,像素的长宽比未被考虑,意味着如果要保证恰当的最终城乡长宽比,在使用VIDEO_SCALING_MODE_SCALE_TO_FIT模式时,必须定位输出Surface。相反,只能对方形像素的内容使用VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式。
    从android N 开始,视屏旋转90或者270度时,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能无法正常工作。
    当设置视频缩放模式时,注意每次输出buffer更改后都要reset。由于INFO_OUTPUT_BUFFERS_CHANGED事件deprecated,可以在每次输出格式更改后进行reset。

    使用输入Surface

    当使用输入Surface时,没有可访问的输入buffer,这是因为buffer自动的从输入Surface传到codec,调用dequeueInputBuffer将会抛出IllegalStateException异常,getInputBuffers() 返回的是一个不可写的伪ByteBuffer[]。
    调用 signalEndOfInputStream() 传递 end-of-stream标志后,输入Surface就会停止向codec提交数据。

    seeking和自适应播放支持

    无论是否支持并配置为自适应播放,视频解码器(以及一些压缩数据的编码器)在seek和格式更改的行为都不同。可以通过CodecCapatilities.isFeatureSupported(String)检查解码器是否支持自适应播放。视频解码器的自适应播放只有在将codec配置到Surface上时,才会被激活。

    流边界和关键帧

    Format Suitable key frame
    VP9/VP8 a suitable intraframe where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.)
    H.265 HEVC IDR or CRA
    H.264 AVC IDR

    重要的是,在start()或者flush()后输入数据要在合适的流边界开始:第一帧必须是关键帧。
    关键帧可以通过自身被完全解码(大多数codec的I帧),并且关键帧之后没有帧要显示指的是关键帧之前的帧。
    下表对不同视频格式合适的关键帧进行了总结:

    Format Suitable key frame
    VP9/VP8 a suitable intraframe where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.)
    H.265 HEVC IDR or CRA
    H.264 AVC IDR

    MPEG-4
    H.263
    MPEG-2 | a suitable I-frame where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.)

    不支持自适应播放的解码器(包括不解码到Surface)

    为了开始解码与之前提交数据不相邻的数据,必须flush解码器。由于所有的输出buffer在flush时被立即撤销,所以需要首先发送信号,等到end-of-stream标志时在flush。重要的是,刷新后的输入数据在合适的流边界/关键帧开始。
    对于某些视频格式,即H.264,H.265,VP8和VP9,也可以改变画面大小或配置中间流。为此,须将整个新的codec-specific configuration data与关键帧一起打包到单个buffer(包括任何起始代码)中,并将其作为常规输入buffer提交。
    在图像大小更改发生之后及在返回新尺寸的任何帧之前,可以从dequeueOutputBuffer或onOutputFormatChanged回调中获得INFO_OUTPUT_FORMAT_CHANGED返回值。

    就像codec-specific configuration data一样,在更改图片大小后不久,调用flush()时要小心,如果没有收到图片尺寸更改的确认,需要重新请求图片大小。

    错误处理

    工厂方法createByCodecName和createDecoder / EncoderByType抛出IOException异常,必须捕获或声明传递。当不允许该方法调用该codec状态时,MediaCodec方法会抛出IllegalStateException异常,通常是由于应用程序API使用不正确。涉及安全buffer的方法可能会抛出MediaCodec.CryptoException异常,该错误的详细信息可从getErrorCode()获取。
    内部codec错误导致MediaCodec.CodecException异常,即使应用程序正确使用API,也可能由于媒体内容损坏,硬件故障,资源耗尽等引起此异常。接收到CodecException异常时,建议操作可以通过调用isRecoverable()和isTransient()来确定:

    • recoverable errors(可恢复的错误):如果isRecoverable()返回true,则调用stop(),configure(...)和start()来恢复。
    • transient errors(瞬时错误):如果isTransient()返回true,则资源暂时不可用,并且可能会在稍后重试该方法。
    • fatal errors(致命错误):如果bothRecoverable()和isTransient()都返回false,则CodecException是致命的,codec必须rest或released。

    isRecoverable()和isTransient()不会同时返回true

    相关文章

      网友评论

        本文标题:android中MediaCodec类解析

        本文链接:https://www.haomeiwen.com/subject/fxzytxtx.html