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

MzDavid發表於2019-03-04

Android音視訊(一) Camera2 API採集資料

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

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

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的優缺點

優點:功耗低,速度快

缺點:擴充套件性不強,不同晶片廠商提供的支援方案不同,導致程式移植性差

適用場景:適合有固定的硬體方案的專案,如智慧家居類;需要長時間攝像。

MediaCodec 編解碼實現

做了一個Demo,使用AudioRecord錄音,使用MediaCodec 編碼為AAC並儲存檔案,然後可以從AAC解碼為PCM資料,再用AudioTrack播放。

Demo截圖

1、編碼PCM資料,儲存為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,等待傳入資料
    encodeInputBuffers = mAudioEncoder.getInputBuffers(); //上面介紹的輸入和輸出Buffer佇列
    encodeOutputBuffers = mAudioEncoder.getOutputBuffers();
    mAudioEncodeBufferInfo = new MediaCodec.BufferInfo();
}
複製程式碼

開始錄音、編碼

使用執行緒池,兩條執行緒,一個執行緒去錄音,另一個執行緒做編碼操作。錄音執行緒會將PCM資料存入一個佇列中,編碼執行緒從佇列中取出資料編碼。

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

 /**
  * 將PCM資料存入佇列
  */
    private void putPCMData(byte[] pcmChunk) {
        Log.e(TAG, "putPCMData");
        try {
            queue.put(pcmChunk);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 從佇列取出PCM資料
     */
    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;
    }
複製程式碼
音訊資料
/**
 * 獲取音訊資料
 */
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資料放入佇列,等待編碼
            }
        }
    } catch (IOException | RuntimeException e) {
        e.printStackTrace();
    } finally {
        if (mAudioRecorder != null) {
            mAudioRecorder.release();
            mAudioRecorder = null;
        }
    }
}
複製程式碼
編碼

從佇列中迴圈取出資料,MediaCodec 編碼,將編碼後的資料寫入檔案中。

/**
 * 編碼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();//獲取解碼器所線上程輸出的資料 程式碼後邊會貼上
        if (chunkPCM == null) {
            continue;
        }
        inputIndex = mAudioEncoder.dequeueInputBuffer(-1);//同解碼器
        if (inputIndex >= 0) {
            inputBuffer = encodeInputBuffers[inputIndex];//同解碼器
            inputBuffer.clear();//同解碼器
            inputBuffer.limit(chunkPCM.length);
            inputBuffer.put(chunkPCM);//PCM資料填充給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資料 取出到byte[]中 偏移量offset=7
            outputBuffer.position(mAudioEncodeBufferInfo.offset);
            try {
                mAudioBos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將檔案儲存到記憶體卡中 *.aac
            } catch (IOException e) {
                e.printStackTrace();
            }
            mAudioEncoder.releaseOutputBuffer(outputIndex, false);
            outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000);
        }
    }

    stopRecorder();
}
複製程式碼

2、解碼AAC AudioTrack播放

初始化AudioTrack和解碼器

/**
 * 初始化AudioTrack,等待播放資料
 */
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);//建立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 ,等待傳入資料
}
複製程式碼

解碼並播放

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內的資料
        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[]資料

        ByteBuffer outputBuffer;
        byte[] chunkPCM;
        //每次解碼完成的資料不一定能一次吐出 所以用while迴圈,保證解碼器吐出所有資料
        while (outputIndex >= 0) {
            outputBuffer = mAudioDecoder.getOutputBuffer(outputIndex);
            chunkPCM = new byte[decodeBufferInfo.size];
            outputBuffer.get(chunkPCM);
            outputBuffer.clear();//資料取出後一定記得清空此Buffer MediaCodec是迴圈使用這些Buffer的,不清空下次會得到同樣的數
            // 播放解碼後的PCM資料
            audioTrack.write(chunkPCM, 0, decodeBufferInfo.size);
            mAudioDecoder.releaseOutputBuffer(outputIndex, false);
            outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次獲取資料
        }
    }
    stopPlay();
}
複製程式碼

Demo完成,手機測試效果不錯。MediaCodec的使用要比我預想的複雜,網上查了好久才完成這個Demo,希望能幫到需要的人。

如有問題歡迎留言,Github原始碼 – MediaCodecActivity

相關文章