最简单的MediaCodec和MediaMuxer编码生成mp4

接上一篇最简单的MediaExtractor和MediaCodec播放视频,这一篇我们来实现最简单的视频的硬编码,继续使用上一篇的内容。
我们期望输入一个mp4文件,我们将这个文件解封装 -> 解码 -> 再编码 -> 封装再重新获得一个mp4文件,这个过程中我们需要总共四个角色:

  1. MediaExtractor用来解封装mp4获得h264流
  2. 用来解码的MediaCodec,将h264解码获得yuv数据
  3. 用来编码的MediaCodec,将yuv数据编码获得h264数据
  4. MediaMuxer用来封装h264获得一个mp4文件

需要源码的可以直接点击Encoder查看,上一篇文章中已经使用了1和2来获取到了yuv数据,所以我们现在只关注3. 编码4. 封装
思路很简单,创建好编码器和混合器,解码得到的每一帧yuv数据都先通过编码器编成h264数据,再通过混合器写入mp4文件。这样解码完成编码也就完成了。

使用MediaCodec实现yuv数据编码成h264

如同MediaCodec解码的用法类似,解码的用法是一样的,不同的地方:

  1. 如何获得MediaFormat
    如果使用MediaExtractor来解封装mp4文件,我们可以得到一个MediaFormat对象,我们用这个MediaFormat可以直接创建MediaCodec并且configure。
    而编码的时候我们则需要自己创建这个MediaFormat对象。

  2. configure的时候
    encodeCodec.configure(encodeMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);最后一个参数需要传入一个MediaCodec.CONFIGURE_FLAG_ENCODE,而解码的时候则不需要。

如何创建MediaCodec,先列一段代码:

    void initMediaCodec() {
        String codecName = MediaFormat.MIMETYPE_VIDEO_AVC;
        int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;

        MediaFormat encodeMediaFormat = MediaFormat.createVideoFormat(codecName, width, height);
        encodeMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        encodeMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 2500000);
        encodeMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        encodeMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

        MediaCodec encodeCodec = MediaCodec.createByCodecName(codecName);
        encodeCodec.configure(encodeMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        encodeCodec.start();
    }

先根据类型创建一个MediaFormat,再给此format设置一系列的参数。
通过codec类型创建MediaCodec,再用创建好的MediaFormat来配置codec,调用start进入等待编码状态。

需要关注的点有下面几个:

  1. KEY_COLOR_FORMAT, KEY_BIT_RATE, KEY_FRAME_RATE, KEY_I_FRAME_INTERVAL这四个值是至少需要设置的。这几个参数其实顾名思义,帧率,比特率都是设置的越小,生成的视频就越小。如果没有设置或者设置错误,会报android.media.MediaCodec$CodecException错误。
  2. 可以看出,我单独把colorFormat和codecName抽成了单独的属性,这俩有一定的关联性。
    因为MediaCodec是硬编码,是依赖硬件厂商的支持的,比如上面写的MIMETYPE_VIDEO_AVCCOLOR_FormatYUV420Planar,也就是h264I420,我们要做的其实就是找到一个支持将I420编码成h264的编码器,我们需要给MediaCodec提供的就是这个信息。手机厂商支持的不尽相同,如果我们写的手机并不支持,同样会报android.media.MediaCodec$CodecException错误。

当然要注意输入的yuv格式需要跟配置的colorFormat对的上,如果对不上就需要转换成对应的YUV格式。比如COLOR_FormatYUV420Planar = I420 = YV21 = 19,而COLOR_FormatYUV420SemiPlanar = NV12 = 21。如果我们编码器设置的是COLOR_FormatYUV420Planar,但是我们的源数据却是NV21,那我们就需要先将数据转成I420再丢给编码器去编码。
关于异常的问题,会在后面展开说,这里先介绍MediaCodec编码的基本流程:

private void encodeData(byte[] yuvBytes,  long presentationTimeUs) {
        MediaCodec.BufferInfo encodeOutputBufferInfo = new MediaCodec.BufferInfo();
// 得到可用的InputBuffer的index
        int inputBufferIndex = encodeCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = encodeCodec.getInputBuffer(inputBufferIndex);
// 得到InputBuffer并填充数据
            inputBuffer.put(yuvBytes);
// 交给MediaCodec处理
            encodeCodec.queueInputBuffer(inputBufferIndex, 0, yuvBytes.length, presentationTimeUs, 0);
        }
// 得到编好的h264数据的ByteBuffer的位置
        int outputBufferIndex = encodeCodec.dequeueOutputBuffer(encodeOutputBufferInfo, -1);
// do something
    }

可以看出,MediaCodec编码的基本流程跟解码的基本是一样的。

使用MediaMuxer将h264封装成mp4

MediaMuxer就类似于MediaExtractor,用法相对MediaCodec也简单了不少。

MediaMuxer mMediaMuxer = new MediaMuxer(outputMp4Path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 获得trackIndex
int encodeVideoTrackIndex = mMediaMuxer.addTrack(newFormat);
mMediaMuxer.start();
// 写入数据帧
mMediaMuxer.writeSampleData(encodeVideoTrackIndex, outputBuffer, encodeOutputBufferInfo);
// 停止和销毁
mMediaMuxer.stop();
mMediaMuxer.release();

上面其实就是MediaMuxer的大致使用方法了,其实也是它的大部分公开方法了。

需要注意的点就是addTrack()方法需要设置一个MediaFormat,如何正确的得到这个MediaFormat就是关键。我之前一度以为我可以使用上面在设置编码器MediaCodec时我自己创建的MediaFormat,发现会报java.lang.IllegalStateException: Failed to stop the muxer的错误,同样文章后面展开说这个错误。
这里来说https://bigflake.com/mediacodec/确实是一个入门的好参考。

        int outputBufferIndex = encodeCodec.dequeueOutputBuffer(encodeOutputBufferInfo, -1);
        switch (outputBufferIndex) {
            case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                MediaFormat newFormat = encodeCodec.getOutputFormat();
                encodeVideoTrackIndex = mMediaMuxer.addTrack(newFormat);
                mMediaMuxer.start();
                break;
...
            default:
                ByteBuffer outputBuffer = encodeCodec.getOutputBuffer(outputBufferIndex);
                byte[] outData = new byte[encodeOutputBufferInfo.size];
                outputBuffer.get(outData);

                muxerOutputBufferInfo.offset = 0;
                muxerOutputBufferInfo.size = encodeOutputBufferInfo.size;
                muxerOutputBufferInfo.flags = isVideoEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : MediaCodec.BUFFER_FLAG_KEY_FRAME;
                muxerOutputBufferInfo.presentationTimeUs = presentationTimeUs;
                mMediaMuxer.writeSampleData(encodeVideoTrackIndex, outputBuffer, encodeOutputBufferInfo);

                encodeCodec.releaseOutputBuffer(outputBufferIndex, false);
                break;
        }

可以在编码结束后在INFO_OUTPUT_FORMAT_CHANGED的时候来获取编码器返回的MediaFormat来构造MediaMuxer。
而且它会在真正的数据帧来临之前返回且只返回一次,我们可以放心的等MediaMuxer初始化之后开始调用writeSampleData来写入数据。

可能遇到的问题及思考

1. android.media.MediaCodec$CodecException报错
    android.media.MediaCodec$CodecException: Error 0xfffffc0e
        at android.media.MediaCodec.native_configure(Native Method)
        at android.media.MediaCodec.configure(MediaCodec.java:1960)
        at android.media.MediaCodec.configure(MediaCodec.java:1889)
1.1. 某一个必要的key没有设置

KEY_COLOR_FORMAT, KEY_BIT_RATE, KEY_FRAME_RATE, KEY_I_FRAME_INTERVAL这四个值中的某一个没有设置。

1.2. 解码器对设置的ColorFormat不支持。

设备中的解码器可以使用下面的代码找到:

    public static void getSupportTypes() {
        MediaCodecList allMediaCodecLists = new MediaCodecList(-1);
        MediaCodecInfo avcCodecInfo = null;
        for (MediaCodecInfo mediaCodecInfo : allMediaCodecLists.getCodecInfos()) {
            if (mediaCodecInfo.isEncoder()) {
                String[] supportTypes = mediaCodecInfo.getSupportedTypes();
                for (String supportType : supportTypes) {
                    if (supportType.equals(MediaFormat.MIMETYPE_VIDEO_AVC)) {
                        avcCodecInfo = mediaCodecInfo;
                        LogUtil.d(TAG, "编码器名称:" + mediaCodecInfo.getName() + "  " + supportType);
                        MediaCodecInfo.CodecCapabilities codecCapabilities = avcCodecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC);
                        int[] colorFormats = codecCapabilities.colorFormats;
                        for (int colorFormat : colorFormats) {
                            switch (colorFormat) {
                                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar:
                                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411PackedPlanar:
                                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
                                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
                                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
                                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
                                    LogUtil.d(MediaCodecUtil.TAG, "支持的格式::" + colorFormat);
                                    break;
                            }
                        }
                    }
                }
            }
        }
    }

我这边设备的打印信息为:

D/LogUtil: MediaCodec 编码器名称:OMX.qcom.video.encoder.avc  video/avc
D/LogUtil: MediaCodec 支持的格式::21
D/LogUtil: MediaCodec 编码器名称:OMX.google.h264.encoder  video/avc
D/LogUtil: MediaCodec 支持的格式::19
D/LogUtil: MediaCodec 支持的格式::21

关于19和21代表了什么,定义在:

// MediaCodecInfo.java
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV411Planar            = 17;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV411PackedPlanar      = 18;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV420Planar            = 19;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV420PackedPlanar      = 20;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV420SemiPlanar        = 21;

发现我的机器里,video/avc的编码器有两个:OMX.qcom.video.encoder.avcOMX.google.h264.encoder,分别支持[19]和[19, 21]。
如果我们使用MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)来创建MediaCodec,那么会使用第一个匹配的OMX.qcom.video.encoder.avc,如果我们使用了COLOR_FormatYUV420Planar,那么就会报错。所以我们可以使用MediaCodec.createByCodecName("OMX.google.h264.encoder")来创建MediaCodec,就会制定使用第二个编码器,那么使用19或者21的ColorFormat都可以。

可以看到在API23中已经弃用了COLOR_FormatYUV420Planar转而推荐使用COLOR_FormatYUV420Flexible,YUV420Flexible并不是一种确定的YUV420格式,而是包含COLOR_FormatYUV411Planar, COLOR_FormatYUV411PackedPlanar, COLOR_FormatYUV420Planar, COLOR_FormatYUV420PackedPlanar, COLOR_FormatYUV420SemiPlanar和COLOR_FormatYUV420PackedSemiPlanar。

在API 21引入YUV420Flexible的同时,它所包含的这些格式都deprecated掉了。需要注意的是几乎所有的厂商都支持COLOR_FormatYUV420Flexible,但是是其中的哪一种就不一定了。

2. Failed to stop the muxer

在调用mediaMuxer.stop()的时候会报下面Failed to stop the muxer错误,直接Crash。

 Process: com.yocn.media, PID: 32401
    java.lang.IllegalStateException: Failed to stop the muxer
        at android.media.MediaMuxer.nativeStop(Native Method)
        at android.media.MediaMuxer.stop(MediaMuxer.java:454)

上面说的MediaMuxer.addTrack(mediaFormat)的MediaFormat配置错误,可以在MediaCodec.INFO_OUTPUT_FORMAT_CHANGED之后用encodeMediaCodec.getOutputFormat();来使用。
那到底为什么不一样呢?如果我就是想自己来创建MediaFormat呢?
其实我们打印一下自己创建的MediaFormat跟encodeMediaCodec.getOutputFormat();的比较一下就可以了。

D/LogUtil: encodeMediaFormat::{color-format=19, i-frame-interval=1, mime=video/avc, width=544, bitrate=2500000, frame-rate=20, height=960} 
D/LogUtil: outputFormat::{max-bitrate=2500000, csd-1=java.nio.HeapByteBuffer[pos=0 lim=10 cap=10], mime=video/avc, width=544, bitrate=2500000, frame-rate=20, height=960, csd-0=java.nio.HeapByteBuffer[pos=0 lim=18 cap=18]} 

我们发现其实就是少了csd-0csd-1两个KV值。那其实可以试一下,使用下面代码将存在的取出来再设置到我们自己创建的MediaFormat里面去就不会报错了。

       ByteBuffer csd_0 = decodeMediaFormat.getByteBuffer("csd-0");
       ByteBuffer csd_1 = decodeMediaFormat.getByteBuffer("csd-1");

这是所谓的Codec-specific数据。可以查看MediaCodec API.
有些格式,特别是ACC音频和MPEG4、H.264和H.265视频格式要求实际数据以若干个包含配置数据或编解码器指定数据的缓存为前缀。
Codec-specific数据也可以被包含在传递给configure方法的格式信息(MediaFormat)中,在ByteBuffer条目中以"csd-0", "csd-1"等key标记。这些keys一直包含在通过MediaExtractor获得的Audio Track or Video Track的MediaFormat中。一旦调用start()方法,MediaFormat中的Codec-specific数据会自动提交给编解码器;你不能显示的提交这些数据。如果MediaFormat中不包含编解码器指定的数据,你可以根据格式要求,按照正确的顺序使用指定数目的缓存来提交codec-specific数据。在H264 AVC编码格式下,你也可以连接所有的codec-specific数据并作为一个单独的codec-config buffer提交。

从Android官网截取下来的图片可以看到在H264格式里csd-0csd-1分别代表了spspps

Missing codec specific data
D/MPEG4Writer: Video track source stopping
D/MPEG4Writer: Video track source stopped
E/MPEG4Writer: Missing codec specific data
E/MPEG4Writer: Dumping Video track's last 10 frames timestamp and frame type 
E/MPEG4Writer: (9166667us, 9166667us Non-Key frame) ...
I/MPEG4Writer: Received total/0-length (286/0) buffers and encoded 285 frames. - Video
D/MPEG4Writer: Video track stopped. Stop source

因为上面在mediaMuxer.stop()的时候会crash,我尝试不调用stop的情况下写mp4文件,不会crash,能生成mp4,大小跟生成的h264文件一样大,但是无法播放,在log中会发现上述报错日志。
尝试使用ffplay播放mp4文件的时候会报moov atom not found错误。
解决方法就是上面的解决方法。

不贴大段代码了,具体代码查阅:SimpleVideoDecoderAndEncoder.java,如果文件位置修改查看不了可以看AndroidMediaCodec项目。

参考链接:

Android MediaCodec stuff
Android多媒体--MediaCodec 中文API文档

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

推荐阅读更多精彩内容