Android音視頻(四)MediaCodec編解碼AAC

Android音視頻(一) Camera2 API采集數(shù)據(jù)

Android音視頻(二)音頻AudioRecord和AudioTrack

Android音視頻(三)FFmpeg Camera2推流直播

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ù)怎顾。當異步處理數(shù)據(jù)時读慎,使用一組輸入和輸出Buffer隊列。通常槐雾,在邏輯上贪壳,客戶端請求(或接收)數(shù)據(jù)后填入預(yù)先設(shè)定的空輸入緩沖區(qū),輸入Buffer填滿后將其傳遞到MediaCodec并進行編解碼處理蚜退。之后MediaCodec編解碼后的數(shù)據(jù)填充到一個輸出Buffer中。最后彪笼,客戶端請求(或接收)輸出Buffer钻注,消耗輸出Buffer中的內(nèi)容,用完后釋放配猫,給回MediaCodec重新填充輸出數(shù)據(jù)幅恋。

圖片來自網(wǎng)絡(luò)

必須保證輸入和輸出隊列同時非空,即至少有一個輸入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遭京。

圖片來自網(wǎng)絡(luò)

從上圖可以看出:

  1. 當創(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)苦始。當帶有end-of-stream標志的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的優(yōu)缺點

優(yōu)點:功耗低斑举,速度快

缺點:擴展性不強,不同芯片廠商提供的支持方案不同,導(dǎo)致程序移植性差

適用場景:適合有固定的硬件方案的項目熬北,如智能家居類疙描;需要長時間攝像。

MediaCodec 編解碼實現(xiàn)

做了一個Demo讶隐,使用AudioRecord錄音起胰,使用MediaCodec 編碼為AAC并保存文件,然后可以從AAC解碼為PCM數(shù)據(jù)整份,再用AudioTrack播放待错。

Demo截圖

1、編碼PCM數(shù)據(jù)烈评,保存為AAC文件

初始化AudioRecord和編碼器

private void initAudioRecord() {
    int audioSource = MediaRecorder.AudioSource.MIC;
    int sampleRate = 44100;
    int channelConfig = AudioFormat.CHANNEL_IN_MONO;
    int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
    mAudioRecorder = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, 2048));
}
/**
 * 初始化編碼器
 */
private void initAudioEncoder() {
    try {
        mAudioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_BUFFER_SIZE);
        mAudioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    } catch (IOException e) {
        e.printStackTrace();
    }

    if (mAudioEncoder == null) {
        Log.e(TAG, "create mediaEncode failed");
        return;
    }

    mAudioEncoder.start(); // 啟動MediaCodec,等待傳入數(shù)據(jù)
    encodeInputBuffers = mAudioEncoder.getInputBuffers(); //上面介紹的輸入和輸出Buffer隊列
    encodeOutputBuffers = mAudioEncoder.getOutputBuffers();
    mAudioEncodeBufferInfo = new MediaCodec.BufferInfo();
}

開始錄音火俄、編碼

使用線程池,兩條線程讲冠,一個線程去錄音瓜客,另一個線程做編碼操作。錄音線程會將PCM數(shù)據(jù)存入一個隊列中竿开,編碼線程從隊列中取出數(shù)據(jù)編碼谱仪。

// 開啟錄音線程
mExecutorService.submit(new Runnable() {
    @Override
    public void run() {
        startRecorder();
    }
});
// 開啟編碼線程
mExecutorService.submit(new Runnable() {
    @Override
    public void run() {
        encodePCM();
    }
});

 /**
  * 將PCM數(shù)據(jù)存入隊列
  */
    private void putPCMData(byte[] pcmChunk) {
        Log.e(TAG, "putPCMData");
        try {
            queue.put(pcmChunk);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 從隊列取出PCM數(shù)據(jù)
     */
    private byte[] getPCMData() {
        try {
            if (queue.isEmpty()) {
                return null;
            }
            return queue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 添加ADTS頭,如果要與視頻流合并就不用添加否彩,單獨AAC文件就需要添加疯攒,否則無法正常播放
     */
    public static void addADTStoPacket(int sampleRateType, byte[] packet, int packetLen) {
        int profile = 2; // AAC LC
        int chanCfg = 2; // CPE

        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (sampleRateType << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }
音頻數(shù)據(jù)
/**
 * 獲取音頻數(shù)據(jù)
 */
private void startRecorder() {
    try {
        mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/RecorderTest/" + System.currentTimeMillis() + ".aac";
        mAudioFile = new File(mFilePath);
        if (!mAudioFile.getParentFile().exists()) {
            mAudioFile.getParentFile().mkdirs();
        }
        mAudioFile.createNewFile();
        mFileOutputStream = new FileOutputStream(mAudioFile);
        mAudioBos = new BufferedOutputStream(mFileOutputStream, 200 * 1024);
        mAudioRecorder.startRecording();

        start = System.currentTimeMillis();

        while (mIsRecording) {
            int read = mAudioRecorder.read(mBuffer, 0, 2048);
            if (read > 0) {
                byte[] audio = new byte[read];
                System.arraycopy(mBuffer, 0, audio, 0, read);
                putPCMData(audio); // PCM數(shù)據(jù)放入隊列,等待編碼
            }
        }
    } catch (IOException | RuntimeException e) {
        e.printStackTrace();
    } finally {
        if (mAudioRecorder != null) {
            mAudioRecorder.release();
            mAudioRecorder = null;
        }
    }
}
編碼

從隊列中循環(huán)取出數(shù)據(jù)列荔,MediaCodec 編碼敬尺,將編碼后的數(shù)據(jù)寫入文件中。

/**
 * 編碼PCM
 */
private void encodePCM() {
    int inputIndex;
    ByteBuffer inputBuffer;
    int outputIndex;
    ByteBuffer outputBuffer;
    byte[] chunkAudio;
    int outBitSize;
    int outPacketSize;
    byte[] chunkPCM;

    while (mIsRecording || !queue.isEmpty()) {
        chunkPCM = getPCMData();//獲取解碼器所在線程輸出的數(shù)據(jù) 代碼后邊會貼上
        if (chunkPCM == null) {
            continue;
        }
        inputIndex = mAudioEncoder.dequeueInputBuffer(-1);//同解碼器
        if (inputIndex >= 0) {
            inputBuffer = encodeInputBuffers[inputIndex];//同解碼器
            inputBuffer.clear();//同解碼器
            inputBuffer.limit(chunkPCM.length);
            inputBuffer.put(chunkPCM);//PCM數(shù)據(jù)填充給inputBuffer
            mAudioEncoder.queueInputBuffer(inputIndex, 0, chunkPCM.length, 0, 0);//通知編碼器 編碼
        }

        outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);
        while (outputIndex >= 0) {
            outBitSize = mAudioEncodeBufferInfo.size;
            outPacketSize = outBitSize + 7;//7為ADTS頭部的大小
            outputBuffer = encodeOutputBuffers[outputIndex];//拿到輸出Buffer
            outputBuffer.position(mAudioEncodeBufferInfo.offset);
            outputBuffer.limit(mAudioEncodeBufferInfo.offset + outBitSize);
            chunkAudio = new byte[outPacketSize];
            addADTStoPacket(44100, chunkAudio, outPacketSize);//添加ADTS
            outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC數(shù)據(jù) 取出到byte[]中 偏移量offset=7
            outputBuffer.position(mAudioEncodeBufferInfo.offset);
            try {
                mAudioBos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將文件保存到內(nèi)存卡中 *.aac
            } catch (IOException e) {
                e.printStackTrace();
            }
            mAudioEncoder.releaseOutputBuffer(outputIndex, false);
            outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);
        }
    }

    stopRecorder();
}

2贴浙、解碼AAC AudioTrack播放

初始化AudioTrack和解碼器

/**
 * 初始化AudioTrack砂吞,等待播放數(shù)據(jù)
 */
private void initAudioTrack() {
    int streamType = AudioManager.STREAM_MUSIC;
    int sampleRate = 44100;
    int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
    int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    int mode = AudioTrack.MODE_STREAM;

    int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);

    audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat,
            Math.max(minBufferSize, 2048), mode);
    audioTrack.play();
}
/**
 * 初始化解碼器
 */
private void initAudioDecoder() {
    try {
        mMediaExtractor = new MediaExtractor();
        mMediaExtractor.setDataSource(mFilePath);

        MediaFormat format = mMediaExtractor.getTrackFormat(0);
        String mime = format.getString(MediaFormat.KEY_MIME);
        if (mime.startsWith("audio")) {//獲取音頻軌道
            mMediaExtractor.selectTrack(0);//選擇此音頻軌道
            format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
            format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
            format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 0);
            format.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
            format.setInteger(MediaFormat.KEY_IS_ADTS, 1);
            format.setInteger(MediaFormat.KEY_AAC_PROFILE, 0);

            mAudioDecoder = MediaCodec.createDecoderByType(mime);//創(chuàng)建Decode解碼器
            mAudioDecoder.configure(format, null, null, 0);
        } else {
            return;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    if (mAudioDecoder == null) {
        Log.e(TAG, "mAudioDecoder is null");
        return;
    }
    mAudioDecoder.start();//啟動MediaCodec ,等待傳入數(shù)據(jù)
}

解碼并播放

private void decodeAndPlay() {
    boolean isFinish = false;
    MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();
    while (!isFinish && mIsPalying) {
        int inputIdex = mAudioDecoder.dequeueInputBuffer(10000);//獲取可用的inputBuffer -1代表一直等待崎溃,0表示不等待 10000表示10秒超時
        if (inputIdex < 0) {
            isFinish = true;
        }
        ByteBuffer inputBuffer = mAudioDecoder.getInputBuffer(inputIdex);
        inputBuffer.clear();//清空之前傳入inputBuffer內(nèi)的數(shù)據(jù)
        int samplesize = mMediaExtractor.readSampleData(inputBuffer, 0);
        if (samplesize > 0) {
            mAudioDecoder.queueInputBuffer(inputIdex, 0, samplesize, 0, 0); //通知解碼器 解碼
            mMediaExtractor.advance(); //MediaExtractor移動到下一取樣處
        } else {
            isFinish = true;
        }
        int outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000);//獲取解碼得到的byte[]數(shù)據(jù)

        ByteBuffer outputBuffer;
        byte[] chunkPCM;
        //每次解碼完成的數(shù)據(jù)不一定能一次吐出 所以用while循環(huán)蜻直,保證解碼器吐出所有數(shù)據(jù)
        while (outputIndex >= 0) {
            outputBuffer = mAudioDecoder.getOutputBuffer(outputIndex);
            chunkPCM = new byte[decodeBufferInfo.size];
            outputBuffer.get(chunkPCM);
            outputBuffer.clear();//數(shù)據(jù)取出后一定記得清空此Buffer MediaCodec是循環(huán)使用這些Buffer的,不清空下次會得到同樣的數(shù)
            // 播放解碼后的PCM數(shù)據(jù)
            audioTrack.write(chunkPCM, 0, decodeBufferInfo.size);
            mAudioDecoder.releaseOutputBuffer(outputIndex, false);
            outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次獲取數(shù)據(jù)
        }
    }
    stopPlay();
}

Demo完成袁串,手機測試效果不錯概而。MediaCodec的使用要比我預(yù)想的復(fù)雜,網(wǎng)上查了好久才完成這個Demo囱修,希望能幫到需要的人到腥。

如有問題歡迎留言,Github源碼 - MediaCodecActivity

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔚袍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌啤咽,老刑警劉巖晋辆,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宇整,居然都是意外死亡瓶佳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門鳞青,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霸饲,“玉大人,你說我怎么就攤上這事臂拓『衤觯” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵胶惰,是天一觀的道長傻工。 經(jīng)常有香客問我,道長孵滞,這世上最難降的妖魔是什么中捆? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮坊饶,結(jié)果婚禮上泄伪,老公的妹妹穿的比我還像新娘。我一直安慰自己匿级,他們只是感情好蟋滴,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著根蟹,像睡著了一般脓杉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上简逮,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天球散,我揣著相機與錄音,去河邊找鬼散庶。 笑死蕉堰,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的悲龟。 我是一名探鬼主播屋讶,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼须教!你這毒婦竟也來了皿渗?” 一聲冷哼從身側(cè)響起斩芭,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乐疆,沒想到半個月后划乖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡挤土,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年琴庵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仰美。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡迷殿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咖杂,到底是詐尸還是另有隱情庆寺,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布翰苫,位于F島的核電站止邮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奏窑。R本人自食惡果不足惜导披,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埃唯。 院中可真熱鬧撩匕,春花似錦、人聲如沸墨叛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漠趁。三九已至扁凛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間闯传,已是汗流浹背谨朝。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留甥绿,地道東北人字币。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像共缕,于是被迫代替她去往敵國和親洗出。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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