MediaCodec专题(二):使用

前言

MediaCodec前面已经做了简介,那么这一篇就是使用了。

参考文章

  1. 官方MediaCodec
  2. Android视频处理之MediaCodec-2-使用
  3. 安卓解码器MediaCodec解析

Creation 创建

1、createByCodecName(String)

使用MediaCodecList为特定的MediaFormat创建MediaCodec

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

2、createDecoder/EncoderByType(String)

根据指定的MIME type来创建codec。

注意:使用这种方法创建的codec不能用于注入特征,并且可能创建无法处理特定所需媒体格式的编解码器。
扩展:
/**
* Sets whether a feature is to be enabled ({@code true}) or disabled
* ({@code false}).
*
* If {@code enabled} is {@code true}, the feature is requested to be present.
* Otherwise, the feature is requested to be not present.
*
* @param feature the name of a {@link MediaCodecInfo.CodecCapabilities} feature.
*
* @see MediaCodecList#findDecoderForFormat
* @see MediaCodecList#findEncoderForFormat
* @see MediaCodecInfo.CodecCapabilities#isFormatSupported
*/
public void setFeatureEnabled(@NonNull String feature, boolean enabled) {
setInteger(KEY_FEATURE_ + feature, enabled ? 1 : 0);
}

public void setFeatureEnabled (String feature, boolean enabled)
设置是启用(true)还是禁用(false)功能。 如果enabled为true,则要求该功能存在。 否则,要求该功能不存在。

两个方案如下::

    private void createDecoderCodec() throws IOException {
        MediaExtractor mExtractor = new MediaExtractor();
        //extract /sdcard/test.mp4
        mExtractor.setDataSource("/sdcard/test.mp4");
        int trackCount = mExtractor.getTrackCount();
        for (int i = 0; i < trackCount; i++) {
            //1. 解码文件或流时,可以从MediaExtractor#getTrackFormat获得所需的格式。
            MediaFormat format = mExtractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                //方案一
                //2. 使用MediaFormat#setFeatureEnabled注入要添加的所有特定功能,创建安全的解码器。
                format.setFeatureEnabled(MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback,true);
                //3. 然后调用MediaCodecList#findDecoderForFormat以获取可以处理该特定媒体格式的编解码器的名称。
                MediaCodecList list = new MediaCodecList(REGULAR_CODECS);
                String encoderForFormat = list.findDecoderForFormat(format);
                //4. 使用createByCodecName(String)创建编解码器。
                MediaCodec codec = MediaCodec.createByCodecName(encoderForFormat);

                //方案二
                MediaCodec decoderByType = MediaCodec.createDecoderByType(mime);
            }
        }
    }

3、创建安全的解码器

在Android 4.4(KITKAT_WATCH)及之前版本,安全的编解码器没有被列在MediaCodecList中,但是仍然可以在系统中使用。
安全编解码器只能够通过名字进行实例化,其名字是在常规编解码器的名字后附加.secure标识(所有安全编解码器的名字都必须以.secure结尾),调用createByCodecName(String)方法创建安全编解码器时,如果系统中不存在指定名字的编解码器就会抛出IOException异常。

从Android 5.0(LOLLIPOP)及之后版本,您应该使用媒体格式的CodecCapabilities#FEATURE_SecurePlayback功能来创建安全的解码器。

Initialization 初始化

根据之前我们提到的流程,创建完编解码器之后,需要进行configure。如果只是需要原始视频数据,直接configure就可以了。如果需要结合Surface使用,需要增加configuresurface参数。
如果要使用视频使用者(用于处理原始视频输入的编解码器,例如视频编码器)本地处理原始输入视频缓冲区,请在配置后使用createInputSurface()为输入数据创建目标Surface。 或者,通过调用setInputSurface(Surface)将编解码器设置为使用以前创建的持久输入表面。

Codec-specific Data 编解码器专用数据

某些格式,尤其是AAC音频和MPEG4,H.264和H.265视频格式,要求实际数据的前缀是许多包含设置数据或编解码器特定数据的缓冲区。

处理此类压缩格式时,必须在start()之后和任何帧数据之前将这些数据提交给编解码器。 必须在对queueInputBuffer的调用中使用标志BUFFER_FLAG_CODEC_CONFIG标记此类数据。

特定格式的编解码器数据也可以包含在传递的格式中,以使用键“ csd-0”,“ csd-1”等在ByteBuffer条目中进行配置。这些键始终包含在从MediaExtractor#getTrackFormat获得的MediaFormat轨道中。
特定格式的编解码器数据在start()时自动提交给编解码器, 您不得明确提交此数据。
如果该格式不包含编解码器专用数据,则可以根据格式要求选择使用正确数量的指定缓冲区使用指定数量提交。 对于H.264 AVC,您还可以连接所有特定于编解码器的数据,并将其作为单个编解码器配置缓冲区提交。

Android使用以下特定于编解码器的数据缓冲区。 为了正确配置MediaMuxer轨道,还需要将其设置为轨道格式。 每个参数集和标有(*)的编解码器专用数据部分必须以“ \ x00 \ x00 \ x00 \ x01”的起始代码开头。


AAC audio, MPEG4, H.264, H.265 video格式的数据作为输入源解码的时候,需要指定一个特殊的前缀设置信息,这个信息通常包含在数据中,但是需要自己提取出来,在mediacodec执行start之后提交这些数据,比如h264的sps和pps,在queueinputbuffer的时候flag设置为BUFFER_FLAG_CODEC_CONFIG提交给解码器。同样这些数据可以在configure的时候提交给mediacodec,效果和前边的一样:

mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(sps));//sps是一个包含sps信息的byte数组
mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(pps));//pps是一个包含pps信息的byte数组

当调用start方法启动时这些信息同样会传给mediacodec。你绝不能直接提交这些数据。 如果格式不包含编解码器特定数据,则可以根据格式要求,选择使用指定数量的缓冲区以正确的顺序提交它。 在H.264 AVC的情况下,还可以连接所有编解码器专用数据并将其作为单个编解码器配置缓冲区提交。


下图所有带*的值必须加上前缀"\x00\x00\x00\x01"

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

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

Data Processing 数据处理

每个codec都有一片属于自己的输入/输出缓冲区,每个缓冲区都有bufferID来指向。在调用start方法后,用户不能访问任何的input bufferoutput buffer

在同步模式下,调用dequeueInput / OutputBuffer(…)从编解码器获取(或拥有)输入或输出缓冲区。
在异步模式下,您将通过Callback#onInputBufferAvailable / Callback#onOutputBufferAvailable回调自动接收可用缓冲区。

模式 输入方法 输出方法
同步模式 dequeueInputBuffer() dequeueOutputBuffer()
异步模式 MediaCodec.Callback.OnInputBufferAvailabe() MediaCodec.Callback.OnInputBufferAvailabe()

当获得inputbuffer(输入方法执行后)后,所有权交给了用户,这些缓冲区由用户填满数据后需要使用queueInputBuffer(加密数据的话请使用queueSecureInputBuffer)提交缓冲区,提交后缓冲区后所有权交给了codec。注意不要为多个帧提供相同的时间戳,除非是配置信息,也就是标记为BUFFER_FLAG_CODEC_CONFIG的帧可以随意使用时间戳。

当获得outputbuffer(输出方法执行)后,用户可访问一个只读的缓冲区,当使用完毕后,请调用releaseOutputBuffer方法来将缓冲区返回给codec。

我们可以不立即queueinputbuffer/releaseOutputBuffer到编解码器,但用户持有input/outputbuffer可能会使编解码器停止工作,并且此行为取决于设备。 编解码器有可能在产生输出缓冲区之前暂停,直到所有未完成的缓冲区queueinputbuffer/releaseOutputBuffer。 因此,用户最好每次获得缓冲区后执行释放操作。

根据API版本,你可以用三种方式处理数据:

处理数据的方式

Asynchronous Processing using Buffers 使用缓冲区的异步处理

从LOLLIPOP开始,首选方法是在调用configure方法之前通过设置回调来异步处理数据。 异步模式会稍微改变状态转换步骤,在running状态时必须在调用flush()之后调用start()方法,将编解码器转换为Running子状态并开始接收输入缓冲区。 同样,在初始调用开始时,codec将直接移至Running子状态,并通过回调开始传递可用的输入缓冲区。
官方解释如下图:


使用缓冲区的异步处理

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

设置回调方法必须在mediacodec创建之后,并且在configure方法之前。

MediaCodec.Callback共有四个方法:

void onError(MediaCodec codec, MediaCodec.CodecException e)//发生错误时回调此方法

void onInputBufferAvailable(MediaCodec codec, int index)//当inputbuffer可用时回调此方法

void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)
//当output方法可用时回调此方法
void onOutputFormatChanged(MediaCodec codec, MediaFormat format)//当输出格式变化时回调此方法

看一下几个重要的方法:

  1. ByteBuffer getInputBuffer (int index)

该方法会返回一个已清空、可写入的input缓冲区,通过调用ByteBuffer.put(data)方法将data中的数据放到缓冲区后,也可以进行其他处理,然后调用void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)就可以将缓冲区返回给codec。index是回调函数中返回的index,offset是缓冲区提交数据的起始未知,可以不从0开始,size是需要提交的长度,presentationTimeUs是时间戳,这个时间戳最好是按帧率来计算(单位:ns),当使用surface作为输出时,这个时间会作为视频的时间戳来显示;flags一般三个值:BUFFER_FLAG_CODEC_CONFIG:配置信息,BUFFER_FLAG_END_OF_STREAM:结束标志,BUFFER_FLAG_KEY_FRAME:关键帧,不建议使用。
在执行此方法后index指向的缓冲区将不可访问,继续使用将会抛出异常。

  1. ByteBuffer getOutputBuffer (int index)

用法同getinputbuffer一样。

  1. void releaseOutputBuffer (int index,boolean render)
    void releaseOutputBuffer (int index,long renderTimestampNs)

这两个方法都会释放index所指向的缓冲区。
假如使用了surface,第二个参数传入传入true将会把数据先输出给surface,当surface不再使用时立即返回给codec,传入long型时:
如果在SurfaceView上渲染缓冲区,则可以使用时间戳在特定时间渲染缓冲区(在缓冲区时间戳之后或之后的VSYNC处)。为了达到这个目的,时间戳需要合理地接近当前的nanoTime()。目前,这是在(1)秒内设定的。

一些注意事项:
该缓冲区将不会返回到编解码器,直到时间戳已经过去并且该缓冲区不再被Surface使用。
缓冲区会按顺序处理,因此您可能会阻止后续缓冲区显示在Surface上。如果您想对用户操作做出反应,这很重要。停止视频或寻求。
如果将多个缓冲区发送到要在同一个VSYNC上渲染的Surface,则会显示最后一个缓冲区,其他将被放弃。
如果时间戳不与当前系统时间“合理接近”,Surface将忽略时间戳,并在最早的可行时间显示缓冲区。在这种模式下,它不会丢帧。
为获得最佳性能和质量,当您在所需渲染时间之前约两个VSYNC的时间时调用此方法。对于60Hz的显示器,这是大约33毫秒。
这段话的大概意思就是不要使用这个方法。

Synchronous Processing using Buffers 使用缓冲区的同步处理

从LOLLIPOP开始,即使在同步模式下使用编解码器,也应使用getInput / OutputBuffer(int)和/或getInput / OutputImage(int)检索输入和输出缓冲区。 这允许框架进行某些优化,例如 处理动态内容时。 如果调用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();

看一下重要方法:

int dequeueInputBuffer (long timeoutUs)
返回值是缓冲区的BufferId,假如返回值为-1则表示缓冲区不能使用。传入的参数为正,则是最长等待时间,为0则会立即返回缓冲区的id,负数则会无限等待。

Synchronous Processing using Buffer Arrays (deprecated) 使用缓冲区数组的同步处理(不建议使用)

Build.VERSION_CODES.KITKAT_WATCH及更低版本中,输入和输出缓冲区的集合由ByteBuffer []数组表示。 成功调用start()后,使用getInput / OutputBuffers()检索缓冲区数组。 使用缓冲区ID作为这些数组的索引(非负数时),如以下示例所示。 请注意,尽管数组大小提供了上限,但数组大小与系统使用的输入和输出缓冲区的数量之间没有固有的相关性。

MediaCodec通常在同步模式下这样使用:

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

当结束输入数据时,发送如下代码即可:

codec.queueInputBuffer(index,0,0,0,BUFFER_FLAG_END_OF_STREAM); //第三个时间戳可以随意设置,空缓冲区的时间戳被忽略。

接受到这个信号后,codec将不再接受任何新的数据,在这个信号之前的数据会全部输出。
除非已刷新,停止或重新启动编解码器,否则请在发出输入流结束信号后不要提交其他输入缓冲区。

Using an Output Surface 使用surface作为输出

使用surface做为输出时与使用Bytebuffer基本一致,只是在surface模式下所有的bytebuffer和image全部为null。

releaseOutputBuffer(bufferId, false);  //不会渲染到surface上
releaseOutputBuffer(bufferId, true); //使用默认的时间戳渲染视频
releaseOutputBuffer(bufferId, timestamp) //使用指定的时间戳渲染视频

Build.VERSION_CODES.M开始,默认时间戳为缓冲区的BufferInfo#presentationTimeUs(转换为纳秒)。 在此之前未定义。

Using an Input Surface 使用surface作为输入

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

调用signalEndOfInputStream()以信号流结束。 调用后,输入Surface将立即停止向编解码器提交数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339