Android音視頻(三) MediaCodec編碼

MediaCodec類可以訪問底層媒體編解碼框架(StageFright 或 OpenMAX),即編解碼組件茂浮,它是Android基本的多媒體支持基礎(chǔ)架構(gòu)的一部分,通常和MediaExtractor只损、MediaSync呆盖、MediaMuxer、MediaCrypto那伐、MediaDrm踏施、Image、Surface和AudioTrack一起使用喧锦。它本身并不是Codec读规,它通過調(diào)用底層編解碼組件獲得了Codec的能力。

MediaCodec的工作方式


MediaCodec處理輸入數(shù)據(jù)產(chǎn)生輸出數(shù)據(jù)燃少。當(dāng)異步處理數(shù)據(jù)時束亏,使用一組輸入和輸出Buffer隊列。通常阵具,在邏輯上碍遍,客戶端請求(或接收)數(shù)據(jù)后填入預(yù)先設(shè)定的空輸入緩沖區(qū),輸入Buffer填滿后將其傳遞到MediaCodec并進行編解碼處理阳液。之后MediaCodec編解碼后的數(shù)據(jù)填充到一個輸出Buffer中怕敬。最后,客戶端請求(或接收)輸出Buffer帘皿,消耗輸出Buffer中的內(nèi)容东跪,用完后釋放,給回MediaCodec重新填充輸出數(shù)據(jù)。

必須保證輸入和輸出隊列同時非空虽填,即至少有一個輸入Buffer和輸出Buffer才能工作丁恭。

MediaCodec狀態(tài)周期圖


在MediaCodec的生命周期中存在三種狀態(tài) :Stopped、Executing斋日、Released牲览。
Stopped狀態(tài)實際上還可以處在三種狀態(tài):Uninitialized、Configured恶守、Error第献。
Executing狀態(tài)也分為三種子狀態(tài):Flushed, Running、End-of-Stream兔港。


從上圖可以看出:

  1. 當(dāng)創(chuàng)建編解碼器的時候處于未初始化狀態(tài)庸毫。首先你需要調(diào)用configure(…)方法讓它處于Configured狀態(tài),然后調(diào)用start()方法讓其處于Executing狀態(tài)押框。在Executing狀態(tài)下岔绸,你就可以使用上面提到的緩沖區(qū)來處理數(shù)據(jù)。
  2. Executing的狀態(tài)下也分為三種子狀態(tài):Flushed, Running盒揉、End-of-Stream。在start() 調(diào)用后刚盈,編解碼器處于Flushed狀態(tài)挂脑,這個狀態(tài)下它保存著所有的緩沖區(qū)藕漱。一旦第一個輸入buffer出現(xiàn)了,編解碼器就會自動運行到Running的狀態(tài)崭闲。當(dāng)帶有end-of-stream標(biāo)志的buffer進去后,編解碼器會進入End-of-Stream狀態(tài)刁俭,這種狀態(tài)下編解碼器不在接受輸入buffer,但是仍然在產(chǎn)生輸出的buffer牍戚。此時你可以調(diào)用flush()方法侮繁,將編解碼器重置于Flushed狀態(tài)。
  3. 調(diào)用stop()將編解碼器返回到未初始化狀態(tài)如孝,然后可以重新配置。 完成使用編解碼器后锁孟,您必須通過調(diào)用release()來釋放它。
  4. 在極少數(shù)情況下品抽,編解碼器可能會遇到錯誤并轉(zhuǎn)到錯誤狀態(tài)桑包。 這是使用來自排隊操作的無效返回值或有時通過異常來傳達的。 調(diào)用reset()使編解碼器再次可用哑了。 您可以從任何狀態(tài)調(diào)用它來將編解碼器移回未初始化狀態(tài)烧颖。 否則,調(diào)用 release()動到終端釋放狀態(tài)拆火。

MediaCodec 基本使用流程:

- createEncoderByType/createDecoderByType
- configure
- start
- while(true) {
    - dequeueInputBuffer
    - queueInputBuffer
    - dequeueOutputBuffer
    - releaseOutputBuffer
}
- stop
- release
  • 實時采集音頻并編碼
    為了保證兼容性涂圆,推薦的配置是 44.1kHz、單通道模狭、16 位精度踩衩。首先創(chuàng)建并配置 AudioRecord 和 MediaCodec
    // 輸入源 麥克風(fēng)
    private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    // 采樣率 44.1kHz,所有設(shè)備都支持
    private final static int SAMPLE_RATE = 44100;
    // 通道 單聲道锚赤,所有設(shè)備都支持
    private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    // 精度 16 位褐鸥,所有設(shè)備都支持
    private final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    // 通道數(shù) 單聲道
    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);
    }

然后開始錄音晶疼,得到原始音頻數(shù)據(jù),再編碼為 AAC 格式锭吨。這個地方會阻塞調(diào)用的線程寒匙,而且編碼比較耗時躏将,一定要在主線程之外調(diào)用考蕾。

public void offerEncoder(AudioRecord record, boolean endOfStream) throws Exception {
        try {
            int e = this.mEncoder.dequeueInputBuffer(0L);
            // 當(dāng)輸入緩沖區(qū)有效時,就是>=0
            if(e >= 0) {
                // 輸入Buffer 隊列,用于傳送數(shù)據(jù)進行編碼
                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隊列, 用于取到編碼后的數(shù)據(jù)
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            // 拿到輸出緩沖區(qū)的索引
            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 返回緩沖區(qū)索引塞帐,如果索引小于 0 ,則表示當(dāng)前沒有可用的緩沖區(qū)荷鼠。它的參數(shù) timeoutUs 表示超時時間 榔幸,畢竟用的是 MediaCodec 的同步模式,如果沒有可用緩沖區(qū)牍疏,就會阻塞指定參數(shù)時間拨齐,如果參數(shù)為負數(shù),則會一直阻塞下去奏黑。

queueInputBuffer 方法將數(shù)據(jù)入隊時,除了要傳遞出隊時的索引值馁害,然后還需要傳入當(dāng)前緩沖區(qū)的時間戳 presentationTimeUs 和當(dāng)前緩沖區(qū)的一個標(biāo)識 flag 蹂匹。

其中,時間戳通常是緩沖區(qū)渲染的時間忍啸,而標(biāo)識則有多種標(biāo)識履植,標(biāo)識當(dāng)前緩沖區(qū)屬于那種類型:

BUFFER_FLAG_CODEC_CONFIG
標(biāo)識當(dāng)前緩沖區(qū)攜帶的是編解碼器的初始化信息,并不是媒體數(shù)據(jù)
BUFFER_FLAG_END_OF_STREAM
結(jié)束標(biāo)識凿滤,當(dāng)前緩沖區(qū)是最后一個了,到了流的末尾
BUFFER_FLAG_KEY_FRAME
表示當(dāng)前緩沖區(qū)是關(guān)鍵幀信息翁脆,也就是 I 幀信息
在編碼的時候可以計算當(dāng)前緩沖區(qū)的時間戳,也可以直接傳遞 0 就好了沙热,對于標(biāo)識也可以直接傳遞 0 作為參數(shù)罢缸。

把數(shù)據(jù)傳入給 MediaCodec 之后,通過 dequeueOutputBuffer 方法取出編解碼后的數(shù)據(jù),除了指定超時時間外蛾洛,還需要傳入 MediaCodec.BufferInfo 對象,這個對象里面有著編碼后數(shù)據(jù)的長度钞螟、偏移量以及標(biāo)識符谎碍。

取出 MediaCodec.BufferInfo 內(nèi)的數(shù)據(jù)之后,根據(jù)不同的標(biāo)識符進行不同的操作:

BUFFER_FLAG_CODEC_CONFIG
表示當(dāng)前數(shù)據(jù)是一些配置數(shù)據(jù)拯啦,在 H264 編碼中就是 SPS 和 PPS 數(shù)據(jù)熔任,也就是 00 00 00 01 67 和 00 00 00 01 68 開頭的數(shù)據(jù),這個數(shù)據(jù)是必須要有的甫匹,它里面有著視頻的寬惦费、高信息。
BUFFER_FLAG_KEY_FRAME
關(guān)鍵幀數(shù)據(jù)薪贫,對于 I 幀數(shù)據(jù)瞧省,也就是開頭是 00 00 00 01 65 的數(shù)據(jù)吠各,
BUFFER_FLAG_END_OF_STREAM
表示結(jié)束勉抓,MediaCodec 工作結(jié)束
對于返回的 flags ,不符合預(yù)定義的標(biāo)識藕筋,則可以直接寫入隐圾,那些數(shù)據(jù)可能代表的是 H264 中的 P 幀 或者 B 幀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜜笤,一起剝皮案震驚了整個濱河市盐碱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓮顽,老刑警劉巖暖混,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異晾咪,居然都是意外死亡贮配,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門剂跟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酣藻,“玉大人,你說我怎么就攤上這事送淆∨陆危” “怎么了辟拷?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵阐斜,是天一觀的道長谒出。 經(jīng)常有香客問我,道長笤喳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任蒙畴,我火速辦了婚禮呜象,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己干跛,他們只是感情好楼入,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著遥赚,像睡著了一般阐肤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上孕惜,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天衫画,我揣著相機與錄音,去河邊找鬼瞄勾。 笑死,一個胖子當(dāng)著我的面吹牛进陡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播换况,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼盗蟆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了觉吭?” 一聲冷哼從身側(cè)響起仆邓,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤节值,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后搞疗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡桩皿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年泄隔,在試婚紗的時候發(fā)現(xiàn)自己被綠了宛徊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡巷燥,死狀恐怖号枕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钝腺,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布定硝,位于F島的核電站毫目,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏镀虐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一空猜、第九天 我趴在偏房一處隱蔽的房頂上張望恨旱。 院中可真熱鬧,春花似錦谆沃、人聲如沸仪芒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铆隘。三九已至南用,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間裹虫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工雳窟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匣屡,地道東北人拇涤。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓鹅士,卻偏偏與公主長得像惩坑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子以舒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容