Android MediaCodec 編解碼音視頻

作者:聲網(wǎng)Agora

我們知道 Camera 采集回傳的是 YUV 數(shù)據(jù)巢价,AudioRecord 是 PCM,我們要對(duì)這些數(shù)據(jù)進(jìn)行編碼(壓縮編碼)苦囱,這里我們來(lái)說(shuō)在 Android 上音視頻編解碼逃不過(guò)的坑-MediaCodec榜聂。
MediaCodec
PSMediaCodec 可以用來(lái)編/解碼 音/視頻唉俗。
MediaCodec 簡(jiǎn)單介紹
MediaCodec 類可用于訪問(wèn)低級(jí)媒體編解碼器闹司,即編碼器/解碼器組件娱仔。 它是 Android 低級(jí)多媒體支持基礎(chǔ)結(jié)構(gòu)的一部分(通常與 MediaExtractor,MediaSync游桩,MediaMuxer牲迫,MediaCrypto,MediaDrm借卧,Image恩溅,Surface 和 AudioTrack 一起使用)。關(guān)于 MediaCodec 的描述可參看官方介紹MediaCodec


image.png

廣義而言谓娃,編解碼器處理輸入數(shù)據(jù)以生成輸出數(shù)據(jù)。 它異步處理數(shù)據(jù)蜒滩,并使用一組輸入和輸出緩沖區(qū)滨达。 在簡(jiǎn)單的情況下奶稠,您請(qǐng)求(或接收)一個(gè)空的輸入緩沖區(qū),將其填充數(shù)據(jù)并將其發(fā)送到編解碼器進(jìn)行處理捡遍。 編解碼器用完了數(shù)據(jù)并將其轉(zhuǎn)換為空的輸出緩沖區(qū)之一锌订。 最后,您請(qǐng)求(或接收)已填充的輸出緩沖區(qū)画株,使用其內(nèi)容并將其釋放回編解碼器辆飘。

PS 讀者如果對(duì)生產(chǎn)者-消費(fèi)者模型還有印象的話,那么 MediaCodec 的運(yùn)行模式其實(shí)也不難理解谓传。

下面是 MediaCodec 的簡(jiǎn)單類圖


image.png

MediaCodec 狀態(tài)機(jī)
在 MediaCodec 生命周期內(nèi)蜈项,編解碼器從概念上講處于以下三種狀態(tài)之一:Stopped,Executing 或 Released续挟。Stopped 的集體狀態(tài)實(shí)際上是三個(gè)狀態(tài)的集合:Uninitialized紧卒,Configured 和 Error,而 Executing 狀態(tài)從概念上講經(jīng)過(guò)三個(gè)子狀態(tài):Flushed诗祸,Running 和 Stream-of-Stream跑芳。


image.png

使用工廠方法之一創(chuàng)建編解碼器時(shí),編解碼器處于未初始化狀態(tài)直颅。首先博个,您需要通過(guò) configure(…)對(duì)其進(jìn)行配置,使它進(jìn)入已配置狀態(tài)功偿,然后調(diào)用 start()將其移至執(zhí)行狀態(tài)盆佣。在這種狀態(tài)下,您可以通過(guò)上述緩沖區(qū)隊(duì)列操作來(lái)處理數(shù)據(jù)脖含。
執(zhí)行狀態(tài)具有三個(gè)子狀態(tài):Flushed罪塔,Running 和 Stream-of-Stream。在 start()之后养葵,編解碼器立即處于 Flushed 子狀態(tài)征堪,其中包含所有緩沖區(qū)。一旦第一個(gè)輸入緩沖區(qū)出隊(duì)关拒,編解碼器將移至“Running”子狀態(tài)佃蚜,在此狀態(tài)下將花費(fèi)大部分時(shí)間。當(dāng)您將輸入緩沖區(qū)與流結(jié)束標(biāo)記排隊(duì)時(shí)着绊,編解碼器將轉(zhuǎn)換為 End-of-Stream 子狀態(tài)谐算。在這種狀態(tài)下,編解碼器將不再接受其他輸入緩沖區(qū)归露,但仍會(huì)生成輸出緩沖區(qū)洲脂,直到在輸出端達(dá)到流結(jié)束為止。在執(zhí)行狀態(tài)下剧包,您可以使用 flush()隨時(shí)返回到“刷新”子狀態(tài)恐锦。
調(diào)用 stop()使編解碼器返回 Uninitialized 狀態(tài)往果,隨后可以再次對(duì)其進(jìn)行配置。使用編解碼器完成操作后一铅,必須通過(guò)調(diào)用 release()釋放它陕贮。
在極少數(shù)情況下,編解碼器可能會(huì)遇到錯(cuò)誤并進(jìn)入“錯(cuò)誤”狀態(tài)潘飘。使用來(lái)自排隊(duì)操作的無(wú)效返回值或有時(shí)通過(guò)異常來(lái)傳達(dá)此信息肮之。調(diào)用 reset()使編解碼器再次可用。您可以從任何狀態(tài)調(diào)用它卜录,以將編解碼器移回“Uninitialized”狀態(tài)戈擒。否則,請(qǐng)調(diào)用 release()以移至終端的“Released”狀態(tài)暴凑。

PSMediaCodec 數(shù)據(jù)處理的模式可分為同步和異步峦甩,下面我們會(huì)一一分析

MediaCodec 同步模式


image.png

上代碼
public H264MediaCodecEncoder(int width, int height) {
//設(shè)置MediaFormat的參數(shù)
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);//FPS
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

    try {
      //通過(guò)MIMETYPE創(chuàng)建MediaCodec實(shí)例
        mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
        //調(diào)用configure,傳入的MediaCodec.CONFIGURE_FLAG_ENCODE表示編碼
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //調(diào)用start
        mMediaCodec.start();

    } catch (Exception e) {
        e.printStackTrace();
    }

}

調(diào)用 putData 向隊(duì)列中 add 原始 YUV 數(shù)據(jù)
public void putData(byte[] buffer) {
if (yuv420Queue.size() >= 10) {
yuv420Queue.poll();
}
yuv420Queue.add(buffer);
}
//開啟編碼
public void startEncoder() {
isRunning = true;
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
byte[] input = null;
while (isRunning) {

               if (yuv420Queue.size() > 0) {
                   //從隊(duì)列中取數(shù)據(jù)
                   input = yuv420Queue.poll();
               }
               if (input != null) {
                   try {
                       //【1】dequeueInputBuffer
                       int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_S);
                       if (inputBufferIndex >= 0) {
                          //【2】getInputBuffer
                           ByteBuffer inputBuffer = null;
                           if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                               inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
                           } else {
                               inputBuffer = mMediaCodec.getInputBuffers()[inputBufferIndex];
                           }
                           inputBuffer.clear();
                           inputBuffer.put(input);
                           //【3】queueInputBuffer
                           mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, getPTSUs(), 0);
                       }

                       MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                       //【4】dequeueOutputBuffer
                       int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_S);
                       if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                           MediaFormat newFormat = mMediaCodec.getOutputFormat();
                           if (null != mEncoderCallback) {
                               mEncoderCallback.outputMediaFormatChanged(H264_ENCODER, newFormat);
                           }
                           if (mMuxer != null) {
                               if (mMuxerStarted) {
                                   throw new RuntimeException("format changed twice");
                               }
                               // now that we have the Magic Goodies, start the muxer
                               mTrackIndex = mMuxer.addTrack(newFormat);
                               mMuxer.start();

                               mMuxerStarted = true;
                           }
                       }

                       while (outputBufferIndex >= 0) {
                           ByteBuffer outputBuffer = null;
                            //【5】getOutputBuffer
                           if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                               outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
                           } else {
                               outputBuffer = mMediaCodec.getOutputBuffers()[outputBufferIndex];
                           }
                           if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                               bufferInfo.size = 0;
                           }

                           if (bufferInfo.size > 0) {

                               // adjust the ByteBuffer values to match BufferInfo (not needed?)
                               outputBuffer.position(bufferInfo.offset);
                               outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                               // write encoded data to muxer(need to adjust presentationTimeUs.
                               bufferInfo.presentationTimeUs = getPTSUs();

                               if (mEncoderCallback != null) {
                                   //回調(diào)
                                   mEncoderCallback.onEncodeOutput(H264_ENCODER, outputBuffer, bufferInfo);
                               }
                               prevOutputPTSUs = bufferInfo.presentationTimeUs;
                               if (mMuxer != null) {
                                   if (!mMuxerStarted) {
                                       throw new RuntimeException("muxer hasn't started");
                                   }
                                   mMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);
                               }

                           }
                           mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                           bufferInfo = new MediaCodec.BufferInfo();
                           outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_S);
                       }
                   } catch (Throwable throwable) {
                       throwable.printStackTrace();
                   }
               } else {
                   try {
                       Thread.sleep(500);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           }

       }
   });

}

PS 編解碼這種耗時(shí)操作要在單獨(dú)的線程中完成,我們這里有個(gè)緩沖隊(duì)列

完整代碼請(qǐng)看H264MediaCodecEncoder
MediaCodec 異步模式
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public H264MediaCodecAsyncEncoder(int width, int height) {
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);//FPS
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

    try {
        mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  //設(shè)置回調(diào)
        mMediaCodec.setCallback(new MediaCodec.Callback() {
            @Override
             /**
         * Called when an input buffer becomes available.
         *
         * @param codec The MediaCodec object.
         * @param index The index of the available input buffer.
         */
            public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
                Log.i("MFB", "onInputBufferAvailable:" + index);
                byte[] input = null;
                if (isRunning) {
                    if (yuv420Queue.size() > 0) {
                        input = yuv420Queue.poll();
                    }
                    if (input != null) {
                        ByteBuffer inputBuffer = codec.getInputBuffer(index);
                        inputBuffer.clear();
                        inputBuffer.put(input);
                        codec.queueInputBuffer(index, 0, input.length, getPTSUs(), 0);
                    }
                }
            }

            @Override
              /**
         * Called when an output buffer becomes available.
         *
         * @param codec The MediaCodec object.
         * @param index The index of the available output buffer.
         * @param info Info regarding the available output buffer {@link MediaCodec.BufferInfo}.
         */
            public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
                Log.i("MFB", "onOutputBufferAvailable:" + index);
                ByteBuffer outputBuffer = codec.getOutputBuffer(index);

                if (info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                    info.size = 0;
                }

                if (info.size > 0) {

                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    outputBuffer.position(info.offset);
                    outputBuffer.limit(info.offset + info.size);
                    // write encoded data to muxer(need to adjust presentationTimeUs.
                    info.presentationTimeUs = getPTSUs();

                    if (mEncoderCallback != null) {
                        //回調(diào)
                        mEncoderCallback.onEncodeOutput(H264_ENCODER, outputBuffer, info);
                    }
                    prevOutputPTSUs = info.presentationTimeUs;
                    if (mMuxer != null) {
                        if (!mMuxerStarted) {
                            throw new RuntimeException("muxer hasn't started");
                        }
                        mMuxer.writeSampleData(mTrackIndex, outputBuffer, info);
                    }

                }
                codec.releaseOutputBuffer(index, false);
            }

            @Override
            public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {

            }

            @Override
                /**
           * Called when the output format has changed
           *
           * @param codec The MediaCodec object.
           * @param format The new output format.
           */
            public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
                if (null != mEncoderCallback) {
                    mEncoderCallback.outputMediaFormatChanged(H264_ENCODER, format);
                }
                if (mMuxer != null) {
                    if (mMuxerStarted) {
                        throw new RuntimeException("format changed twice");
                    }
                    // now that we have the Magic Goodies, start the muxer
                    mTrackIndex = mMuxer.addTrack(format);
                    mMuxer.start();

                    mMuxerStarted = true;
                }
            }
        });
        mMediaCodec.start();

    } catch (Exception e) {
        e.printStackTrace();
    }

}

完整代碼請(qǐng)看H264MediaCodecAsyncEncoder
MediaCodec 小結(jié)
MediaCodec 用來(lái)音視頻的編解碼工作(這個(gè)過(guò)程有的文章也稱為硬解)现喳,通過(guò)MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC)函數(shù)中的參數(shù)來(lái)創(chuàng)建音頻或者視頻的編碼器凯傲,同理通過(guò)MediaCodec.createDecoderByType(MIMETYPE_VIDEO_AVC)創(chuàng)建音頻或者視頻的解碼器。對(duì)于音視頻編解碼中需要的不同參數(shù)用MediaFormat來(lái)指定嗦篱。
小結(jié)
本篇文章詳細(xì)的對(duì) MediaCodec 進(jìn)行了分析冰单,讀者可根據(jù)博客對(duì)應(yīng) Demo 來(lái)進(jìn)行實(shí)際操練。
放上 Demo 地址詳細(xì)Demo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灸促,一起剝皮案震驚了整個(gè)濱河市诫欠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浴栽,老刑警劉巖荒叼,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異典鸡,居然都是意外死亡被廓,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門萝玷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嫁乘,“玉大人,你說(shuō)我怎么就攤上這事球碉◎迅” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵睁冬,是天一觀的道長(zhǎng)挎春。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么搂蜓? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任狼荞,我火速辦了婚禮,結(jié)果婚禮上帮碰,老公的妹妹穿的比我還像新娘。我一直安慰自己拾积,他們只是感情好殉挽,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拓巧,像睡著了一般斯碌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肛度,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天傻唾,我揣著相機(jī)與錄音,去河邊找鬼承耿。 笑死冠骄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的加袋。 我是一名探鬼主播凛辣,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼职烧!你這毒婦竟也來(lái)了扁誓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蚀之,失蹤者是張志新(化名)和其女友劉穎蝗敢,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體足删,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寿谴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了壹堰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拭卿。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贱纠,靈堂內(nèi)的尸體忽然破棺而出峻厚,到底是詐尸還是另有隱情,我是刑警寧澤谆焊,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布惠桃,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏辜王。R本人自食惡果不足惜劈狐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呐馆。 院中可真熱鬧肥缔,春花似錦、人聲如沸汹来。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)收班。三九已至坟岔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摔桦,已是汗流浹背社付。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留邻耕,地道東北人鸥咖。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像赊豌,于是被迫代替她去往敵國(guó)和親扛或。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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