Android音视频(三) MediaCodec编码

MediaCodec类可以访问底层媒体编解码框架(StageFright 或 OpenMAX),即编解码组件,它是Android基本的多媒体支持基础架构的一部分,通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用。它本身并不是Codec,它通过调用底层编解码组件获得了Codec的能力。

MediaCodec的工作方式


MediaCodec处理输入数据产生输出数据。当异步处理数据时,使用一组输入和输出Buffer队列。通常,在逻辑上,客户端请求(或接收)数据后填入预先设定的空输入缓冲区,输入Buffer填满后将其传递到MediaCodec并进行编解码处理。之后MediaCodec编解码后的数据填充到一个输出Buffer中。最后,客户端请求(或接收)输出Buffer,消耗输出Buffer中的内容,用完后释放,给回MediaCodec重新填充输出数据。

必须保证输入和输出队列同时非空,即至少有一个输入Buffer和输出Buffer才能工作。

MediaCodec状态周期图


在MediaCodec的生命周期中存在三种状态 :Stopped、Executing、Released。
Stopped状态实际上还可以处在三种状态:Uninitialized、Configured、Error。
Executing状态也分为三种子状态:Flushed, Running、End-of-Stream。


从上图可以看出:

  1. 当创建编解码器的时候处于未初始化状态。首先你需要调用configure(…)方法让它处于Configured状态,然后调用start()方法让其处于Executing状态。在Executing状态下,你就可以使用上面提到的缓冲区来处理数据。
  2. Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。在start() 调用后,编解码器处于Flushed状态,这个状态下它保存着所有的缓冲区。一旦第一个输入buffer出现了,编解码器就会自动运行到Running的状态。当带有end-of-stream标志的buffer进去后,编解码器会进入End-of-Stream状态,这种状态下编解码器不在接受输入buffer,但是仍然在产生输出的buffer。此时你可以调用flush()方法,将编解码器重置于Flushed状态。
  3. 调用stop()将编解码器返回到未初始化状态,然后可以重新配置。 完成使用编解码器后,您必须通过调用release()来释放它。
  4. 在极少数情况下,编解码器可能会遇到错误并转到错误状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用reset()使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回未初始化状态。 否则,调用 release()动到终端释放状态。

MediaCodec 基本使用流程:

- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
    - dequeueInputBuffer
    - queueInputBuffer
    - dequeueOutputBuffer
    - releaseOutputBuffer
}
- stop
- release
  • 实时采集音频并编码
    为了保证兼容性,推荐的配置是 44.1kHz、单通道、16 位精度。首先创建并配置 AudioRecord 和 MediaCodec
    // 输入源 麦克风
    private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    // 采样率 44.1kHz,所有设备都支持
    private final static int SAMPLE_RATE = 44100;
    // 通道 单声道,所有设备都支持
    private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    // 精度 16 位,所有设备都支持
    private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    // 通道数 单声道
    private static final int CHANNEL_COUNT = 1;
    // 比特率
    private static final int BIT_RATE = 96000;

    public void createAudio() {
        mBufferSizeInBytes = AudioRecord.getMinBufferSize(AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT);
        if (mBufferSizeInBytes <= 0) {
            throw new RuntimeException("AudioRecord is not available, minBufferSize: " + mBufferSizeInBytes);
        }
        Log.i(TAG, "createAudioRecord minBufferSize: " + mBufferSizeInBytes);

        mAudioRecord = new AudioRecord(AudioEncoder.AUDIO_SOURCE, AudioEncoder.SAMPLE_RATE, AudioEncoder.CHANNEL_CONFIG, AudioEncoder.AUDIO_FORMAT, mBufferSizeInBytes);
        int state = mAudioRecord.getState();
        Log.i(TAG, "createAudio state: " + state + ", initialized: " + (state == AudioRecord.STATE_INITIALIZED));
    }

    public void createMediaCodec() throws IOException {
        MediaCodecInfo mediaCodecInfo = CodecUtils.selectCodec(MIMETYPE_AUDIO_AAC);
        if (mediaCodecInfo == null) {
            throw new RuntimeException(MIMETYPE_AUDIO_AAC + " encoder is not available");
        }
        Log.i(TAG, "createMediaCodec: mediaCodecInfo " + mediaCodecInfo.getName());

        MediaFormat format = MediaFormat.createAudioFormat(MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT);
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);

        mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_AUDIO_AAC);
        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    }

然后开始录音,得到原始音频数据,再编码为 AAC 格式。这个地方会阻塞调用的线程,而且编码比较耗时,一定要在主线程之外调用。

public void offerEncoder(AudioRecord record, boolean endOfStream) throws Exception {
        try {
            int e = this.mEncoder.dequeueInputBuffer(0L);
            // 当输入缓冲区有效时,就是>=0
            if(e >= 0) {
                // 输入Buffer 队列,用于传送数据进行编码
                ByteBuffer[] inputBuffers = this.mEncoder.getInputBuffers();
                ByteBuffer bufferCache = inputBuffers[e];
                int audioSize = record.read(bufferCache, bufferCache.remaining());
                if(audioSize != AudioRecord.ERROR_INVALID_OPERATION
                        && audioSize != AudioRecord.ERROR_BAD_VALUE) {
                    int flag = endOfStream?4:0;
                    // 通知编码器编码
                    this.mEncoder.queueInputBuffer(e, 0, audioSize, this.mLastPresentationTimeUs, flag);
                    if(this.presentationInterval == 0) {
                        this.presentationInterval = (int)((float)audioSize / (float)this.sampleByteSizeInSec * 1000000.0F);
                    }
                    // 时间戳保证递增就是
                    this.mLastPresentationTimeUs += (long)this.presentationInterval;
                } else {
                    Log.w(TAG, "offerEncoder : error audioSize = " + audioSize);
                }
            } else if(endOfStream) {
                this.unExpectedEndOfStream = true;
            }
        } catch (Exception e) {
            if(null != this.mCallback) {
                this.mCallback.onStatus(AudioWorker.ENCODE_OFFER_ERROR, new Object[]{e});
            }
        }

    }

    public void drainEncoder(boolean endOfStream) throws Exception {
        while(true) {
            // 输出Buffer队列, 用于取到编码后的数据
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            // 拿到输出缓冲区的索引
            int bufferIndex = this.mEncoder.dequeueOutputBuffer(info, 0L);
            ByteBuffer[] buffers = this.mEncoder.getOutputBuffers();
            if(bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat data1 = this.mEncoder.getOutputFormat();
                if(null != this.mCallback) {
                    this.mCallback.onStatus(AudioWorker.STATUS_START, new Object[]{data1});
                }
            } else if(bufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                this.mEncoder.getOutputBuffers();
            } else {
                if(bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                    if(endOfStream && !this.unExpectedEndOfStream) {
                        continue;
                    }
                } else {
                    if(bufferIndex < 0) {
                        Log.w(TAG, "AudioEncoderCore.drainEncoder : bufferIndex < 0 ");
                        continue;
                    }

                    ByteBuffer data = buffers[bufferIndex];
                    if(null != data) {
                        data.position(info.offset);
                        data.limit(info.offset + info.size);
                    }

                    if (info.flags ==  MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                        if (null != this.mCallback) {
                            this.mCallback.onStatus(AudioWorker.STATUS_HEAD, new Object[]{data, info});
                        }
                    } else if(null != this.mCallback) {
                        this.mCallback.onStatus(AudioWorker.STATUS_DATA, new Object[]{data, info});
                    }

                    this.mEncoder.releaseOutputBuffer(bufferIndex, false);
                    if((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0) {
                        continue;
                    }
                }
                return;
            }
        }
    }

dequeueInputBuffer 返回缓冲区索引,如果索引小于 0 ,则表示当前没有可用的缓冲区。它的参数 timeoutUs 表示超时时间 ,毕竟用的是 MediaCodec 的同步模式,如果没有可用缓冲区,就会阻塞指定参数时间,如果参数为负数,则会一直阻塞下去。

queueInputBuffer 方法将数据入队时,除了要传递出队时的索引值,然后还需要传入当前缓冲区的时间戳 presentationTimeUs 和当前缓冲区的一个标识 flag 。

其中,时间戳通常是缓冲区渲染的时间,而标识则有多种标识,标识当前缓冲区属于那种类型:

BUFFER_FLAG_CODEC_CONFIG
标识当前缓冲区携带的是编解码器的初始化信息,并不是媒体数据
BUFFER_FLAG_END_OF_STREAM
结束标识,当前缓冲区是最后一个了,到了流的末尾
BUFFER_FLAG_KEY_FRAME
表示当前缓冲区是关键帧信息,也就是 I 帧信息
在编码的时候可以计算当前缓冲区的时间戳,也可以直接传递 0 就好了,对于标识也可以直接传递 0 作为参数。

把数据传入给 MediaCodec 之后,通过 dequeueOutputBuffer 方法取出编解码后的数据,除了指定超时时间外,还需要传入 MediaCodec.BufferInfo 对象,这个对象里面有着编码后数据的长度、偏移量以及标识符。

取出 MediaCodec.BufferInfo 内的数据之后,根据不同的标识符进行不同的操作:

BUFFER_FLAG_CODEC_CONFIG
表示当前数据是一些配置数据,在 H264 编码中就是 SPS 和 PPS 数据,也就是 00 00 00 01 67 和 00 00 00 01 68 开头的数据,这个数据是必须要有的,它里面有着视频的宽、高信息。
BUFFER_FLAG_KEY_FRAME
关键帧数据,对于 I 帧数据,也就是开头是 00 00 00 01 65 的数据,
BUFFER_FLAG_END_OF_STREAM
表示结束,MediaCodec 工作结束
对于返回的 flags ,不符合预定义的标识,则可以直接写入,那些数据可能代表的是 H264 中的 P 帧 或者 B 帧。

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

推荐阅读更多精彩内容