Android音視頻編碼錄制mp4

Android錄制視頻有多種方法:MediaRecorder, MediaProjection, MediaMuxer, OpenGL等币狠,每種方法都有其應(yīng)用場景。

這里介紹的是用MediaCodec + MediaMuxer錄制視頻丈屹,這種方式是將音頻流和視頻流用MediaCodec編碼,然后用MediaMuxer混流合成mp4視頻, 這種方式的通用性較好闲坎,它不關(guān)心數(shù)據(jù)來源,只要能獲得音視頻流數(shù)據(jù)茬斧,就能錄制腰懂。

音視頻開發(fā)自有完整體系,其中的知識(shí)點(diǎn)和注意點(diǎn)(坑)也很多项秉。網(wǎng)上的文章和開源項(xiàng)目雖不少绣溜,有些點(diǎn)卻鮮有提及。這里將我開發(fā)中遇到的問題和一些小結(jié)記錄下來娄蔼,如有錯(cuò)誤怖喻,還請指教。

1. 數(shù)據(jù)源

視頻來源選擇Camera2岁诉,給Camera2添加ImageReader锚沸,就能獲得實(shí)時(shí)的圖像數(shù)據(jù)。具體操作可以看這篇文章涕癣。

給ImageReader設(shè)置的輸出格式是ImageFormat.YUV_420_888咒吐,這種格式是官方建議通用性最好的,但是它只能保證輸出是YUV420格式属划,而YUV420分很多種恬叹,具體格式不同設(shè)備是不同的,可能是YUV420P同眯,也可能是YUV420SP绽昼,后面編碼還要考慮這個(gè)問題。

從ImageReader中獲取byte[]數(shù)據(jù)方法如下

    /**
     * 從ImageReader中獲取byte[]數(shù)據(jù)
     */
    public static byte[] getBytesFromImageReader(ImageReader imageReader) {
        try (Image image = imageReader.acquireNextImage()) {
            final Image.Plane[] planes = image.getPlanes();
            ByteBuffer b0 = planes[0].getBuffer();
            ByteBuffer b1 = planes[1].getBuffer();
            ByteBuffer b2 = planes[2].getBuffer();
            int y = b0.remaining(), u = y >> 2, v = u;
            byte[] bytes = new byte[y + u + v];
            if(b1.remaining() > u) { // y420sp
                b0.get(bytes, 0, b0.remaining());
                b1.get(bytes, y, b1.remaining()); // uv
            } else { // y420p
                b0.get(bytes, 0, b0.remaining());
                b1.get(bytes, y, b1.remaining()); // u
                b2.get(bytes, y + u, b2.remaining()); // v
            }
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

從ImageReader中取得Image须蜗,YUV數(shù)據(jù)就在Image的Plane[]中硅确,Plane也就是平面有3個(gè)。Plane[0]是Y平面明肮,數(shù)據(jù)量等于圖像的像素個(gè)數(shù)菱农,用plane.getBuffer().remaining()方法獲得。
YUV420P格式下Plane[1]是U平面柿估,數(shù)據(jù)量是Y/4循未,Plane[2]是V平面,數(shù)據(jù)量也是Y/4秫舌。
YUV420SP格式下Plane[1]和Plane[2]都是UV平面的妖,它們數(shù)據(jù)基本相同绣檬,只是位置錯(cuò)開了一位。一般情況下用Plane[1]中的數(shù)據(jù)即可嫂粟,Plane[1]的數(shù)據(jù)量可能是Y/2娇未,也可能是Y/2 - 1,雖然差那一位數(shù)據(jù)在顯示上沒有影響星虹,但會(huì)導(dǎo)致創(chuàng)建的數(shù)組大小不對零抬。

音頻來源選擇AudioRecord,配置參數(shù)宽涌,啟動(dòng)媚值,然后從單獨(dú)線程中用AudioRecord.read()方法不斷地循環(huán)讀取數(shù)據(jù)。

2. 編碼格式

視頻編碼用MediaCodec护糖,根據(jù)MIME_TYPE = "video/avc"選擇設(shè)備支持的編碼器和colorFormat

    private MediaCodecInfo selectSupportCodec(String mimeType) {
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++) {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            if (!codecInfo.isEncoder()) {
                continue;
            }
            String[] types = codecInfo.getSupportedTypes();
            for (int j = 0; j < types.length; j++) {
                if (types[j].equalsIgnoreCase(mimeType)) {
                    return codecInfo;
                }
            }
        }
        return null;
    }

    /**
     * 根據(jù)mime類型匹配編碼器支持的顏色格式
     */
    private int selectSupportColorFormat(MediaCodecInfo mCodecInfo, String mimeType) {
        MediaCodecInfo.CodecCapabilities capabilities = mCodecInfo.getCapabilitiesForType(mimeType);
        HashSet<Integer> colorFormats = new HashSet<>();
        for(int i : capabilities.colorFormats) colorFormats.add(i);
        if(colorFormats.contains(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)) return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
        if(colorFormats.contains(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)) return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
        return 0;
    }

colorFormat顏色格式很重要,它代表了編碼器接受的圖像格式嚼松。輸入其他格式的圖像嫡良,會(huì)導(dǎo)致編碼失敗或者視頻的圖像顏色錯(cuò)亂。
目前來說:大部分手機(jī)都支持COLOR_FormatYUV420SemiPlanar格式(實(shí)測是NV12)献酗,因此首選這個(gè)格式寝受;少數(shù)手機(jī)(如紅米Note4,魅族MX5)不支持COLOR_FormatYUV420SemiPlanar罕偎,而支持COLOR_FormatYUV420Planar格式(實(shí)測是I420)很澄;這兩種格式基本能覆蓋所有的設(shè)備了。

前面說過Camera2設(shè)置ImageFormat.YUV_420_888后輸出的圖像格式是YUV420P或YUV420SP颜及,實(shí)測格式是I420和NV12甩苛,它們跟視頻編碼器的支持的格式吻合,可以直接提供給視頻編碼器俏站,當(dāng)然一般還需要旋轉(zhuǎn)讯蒲。如果你的圖像不是從Camera2獲取的,或者是其他格式肄扎,就要將圖像轉(zhuǎn)換成編碼器支持的格式墨林。
(根據(jù)我的測試,如果手機(jī)支持COLOR_FormatYUV420SemiPlanar編碼格式犯祠,那么它的Camera2相機(jī)輸出的就是NV12格式旭等;如果手機(jī)只支持COLOR_FormatYUV420Planar編碼格式,那么它的Camera2相機(jī)輸出的就是I420格式衡载。因此我猜測Camera2輸出用的就是相同的硬件視頻編碼器搔耕。)

音頻編碼也用MediaCodec,根據(jù)MIME_TYPE = "audio/mp4a-latm"選擇編碼器即可痰娱,基本沒有設(shè)備差異問題度迂。

3. 合成視頻

主要步驟

  1. 用MediaCodec分別開始編碼圖像和聲音
  2. 將編碼時(shí)獲得的圖像和聲音的MediaFormat添加到MediaMuxer
  3. 啟動(dòng)MediaMuxer將編碼后的圖像和聲音合成mp4

3.1 編碼

編碼器輸入byte[]原始數(shù)據(jù)藤乙,編碼后輸出ByteBuffer數(shù)據(jù)。
編碼比較耗時(shí)惭墓,需要工作在單獨(dú)的線程中坛梁。
編碼的輸入和輸出是異步的,也就是輸入數(shù)據(jù)腊凶,然后用循環(huán)不停獲取輸出划咐。
編碼一段時(shí)間后才能獲取到MediaCodec.INFO_OUTPUT_FORMAT_CHANGED信息,這代表輸出格式確定了钧萍,也就能向MediaMuxer添加軌道了褐缠。

視頻編碼器輸入和輸出如下

    // 視頻編碼器輸入
    private void feedMediaCodecData(byte[] data, long timeStamp) {
        int inputBufferIndex = mVideoEncodec.dequeueInputBuffer(TIMES_OUT);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex);
            if (inputBuffer != null) {
                inputBuffer.clear();
                inputBuffer.put(data);
            }
            Log.e("chao", "video set pts......." + (timeStamp) / 1000 / 1000);
            mVideoEncodec.queueInputBuffer(inputBufferIndex, 0, data.length, System.nanoTime() / 1000
                    , MediaCodec.BUFFER_FLAG_KEY_FRAME);
        }
    }

    // 視頻編碼器輸出
                MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
                int outputBufferIndex;
                do {
                    outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
                    if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//                        Log.i(TAG, "INFO_TRY_AGAIN_LATER");
                    } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        synchronized (H264EncodeConsumer.this) {
                            newFormat = mVideoEncodec.getOutputFormat();
                            if (mMuxerRef != null) {
                                MediaMuxerUtil muxer = mMuxerRef.get();
                                if (muxer != null) {
                                    muxer.addTrack(newFormat, true);
                                }
                            }
                        }

                        Log.i(TAG, "編碼器輸出緩存區(qū)格式改變,添加視頻軌道到混合器");
                    } else {
                        ByteBuffer outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
                        int type = outputBuffer.get(4) & 0x1F;

                        Log.d(TAG, "------還有數(shù)據(jù)---->" + type);
                        if (type == 7 || type == 8) {

                            Log.e(TAG, "------PPS风瘦、SPS幀(非圖像數(shù)據(jù))队魏,忽略-------");
                            mBufferInfo.size = 0;
                        } else if (type == 5) {
                            if (mMuxerRef != null) {
                                MediaMuxerUtil muxer = mMuxerRef.get();
                                if (muxer != null) {
                                    Log.i(TAG, "------編碼混合  視頻關(guān)鍵幀數(shù)據(jù)-----" + mBufferInfo.presentationTimeUs / 1000);
                                    muxer.pumpStream(outputBuffer, mBufferInfo, true);
                                }
                                isAddKeyFrame = true;
                            }
                        } else {
                            if (isAddKeyFrame) {
                                if (isAddKeyFrame && mMuxerRef != null) {
                                    MediaMuxerUtil muxer = mMuxerRef.get();
                                    if (muxer != null) {
                                        Log.i(TAG, "------編碼混合  視頻普通幀數(shù)據(jù)-----" + mBufferInfo.presentationTimeUs / 1000);
                                        muxer.pumpStream(outputBuffer, mBufferInfo, true);
                                    }
                                }
                            }
                        }
                        mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);
                    }
                } while (outputBufferIndex >= 0);

音頻編碼器工作是類似的,由于我用的AudioRecord獲取聲音數(shù)據(jù)万搔,編碼時(shí)會(huì)有一個(gè)問題:

AudioRecord獲取音頻數(shù)據(jù)是用死循環(huán)不斷獲取的胡桨,這樣獲取聲音的速度太快,編碼又是耗時(shí)的瞬雹,就會(huì)造成編碼速度趕不上聲音獲取速度昧谊,也就是生產(chǎn)速度遠(yuǎn)大于消費(fèi)速度,導(dǎo)致大部分?jǐn)?shù)據(jù)都處理不完酗捌,視頻中聲音出問題呢诬。
音頻編碼器能設(shè)置MediaFormat.KEY_MAX_INPUT_SIZE,也就是輸入數(shù)據(jù)包的大小胖缤。我采取的方法是給它設(shè)置一個(gè)較大的值尚镰,獲取到聲音數(shù)據(jù)后先緩存起來,拼接成較大的數(shù)據(jù)包后再提供給編碼器哪廓,這樣就能處理過來了钓猬。

3.2 合成

視頻合成使用MediaMuxer合成器,用addTrack()方法添加視頻軌道和聲音軌道后才能啟動(dòng)撩独,啟動(dòng)后用writeSampleData()方法輸入數(shù)據(jù)后敞曹,直接輸出到指定的mp4文件中。

完整代碼

https://github.com/rome753/android-encode-mp4

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末综膀,一起剝皮案震驚了整個(gè)濱河市澳迫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剧劝,老刑警劉巖橄登,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡拢锹,警方通過查閱死者的電腦和手機(jī)谣妻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卒稳,“玉大人蹋半,你說我怎么就攤上這事〕淇樱” “怎么了减江?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捻爷。 經(jīng)常有香客問我辈灼,道長,這世上最難降的妖魔是什么也榄? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任巡莹,我火速辦了婚禮,結(jié)果婚禮上甜紫,老公的妹妹穿的比我還像新娘降宅。我一直安慰自己,他們只是感情好棵介,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吧史,像睡著了一般邮辽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贸营,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天吨述,我揣著相機(jī)與錄音,去河邊找鬼钞脂。 笑死揣云,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冰啃。 我是一名探鬼主播邓夕,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阎毅!你這毒婦竟也來了焚刚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤扇调,失蹤者是張志新(化名)和其女友劉穎矿咕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碳柱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年捡絮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莲镣。...
    茶點(diǎn)故事閱讀 40,015評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡福稳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出剥悟,到底是詐尸還是另有隱情灵寺,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布区岗,位于F島的核電站略板,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏慈缔。R本人自食惡果不足惜叮称,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望藐鹤。 院中可真熱鬧瓤檐,春花似錦、人聲如沸娱节。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肄满。三九已至谴古,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間稠歉,已是汗流浹背掰担。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怒炸,地道東北人带饱。 一個(gè)月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像阅羹,于是被迫代替她去往敵國和親勺疼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評論 2 355

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