Android AVDemo(4):音訊解封裝,從 MP4 中解封裝出 AAC丨音視訊工程示例

語言: CN / TW / HK

塞尚《河流》

這個公眾號會 路線圖 式的遍歷分享音視訊技術 音視訊基礎(完成)  →  音視訊工具(完成)  →  音視訊工程示例(進行中)  →  音視訊工業實戰(準備) 關注一下成本不高,錯過乾貨損失不小 ↓↓↓

iOS/Android 客戶端開發同學如果想要開始學習音視訊開發,最絲滑的方式是對音視訊基礎概念知識有一定了解後,再借助 iOS/Android 平臺的音視訊能力上手去實踐音視訊的 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 過程,並藉助音視訊工具來分析和理解對應的音視訊資料。

在音視訊工程示例這個欄目,我們將通過拆解 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 流程並實現 Demo 來向大家介紹如何在 iOS/Android 平臺上手音視訊開發。

這裡是 Android 第四篇: Android 音訊解封裝 Demo 。這個 Demo 裡包含以下內容:

  • 1)實現一個音訊解封裝模組;

  • 2)實現對 MP4 檔案中音訊部分的解封裝邏輯並將解封裝後的編碼資料儲存為 AAC 檔案;

  • 3)詳盡的程式碼註釋,幫你理解程式碼邏輯和原理。

如果你想獲得全部原始碼和參與音視訊技術討論,可以通過下面二維碼加入『關鍵幀的音視訊開發圈』,當然也可以跳過直接看後續的內容。

長按識別二維碼→加入我們

1、音訊解封裝模組

首先,實現一個 KFDemuxerConfig 類用於定義音訊解封裝引數的配置。這裡包括了:視訊路徑、解封裝型別這幾個引數。這樣設計是因為這個配置類不僅會用於音訊解封裝,後續的視訊解封裝也會使用。

KFDemuxerConfig.java

public class KFDemuxerConfig {
    ///< 輸入路徑。
    public String path;
    ///< 音視訊解封裝型別(僅音訊、僅視訊、音視訊)。
    public KFMediaBase.KFMediaType demuxerType = KFMediaBase.KFMediaType.KFMediaAV;
}

其中用到的 KFMediaType 是定義在 KFMediaBase 中的一個列舉:

KFMediaBase.java

public class KFMediaBase {
    public enum KFMediaType{
        KFMediaUnkown(0),
        KFMediaAudio (1 << 0),
        KFMediaVideo  (1 << 1),
        KFMediaAV ((1 << 0) | (1 << 1));
        private int index;
        KFMediaType(int index) {
            this.index = index;
        }

        public int value() {
            return index;
        }
    }
}

接下來,我們實現一個 KFMP4Demuxer 類來實現 MP4 的解封裝。它能從符合 MP4 標準的檔案中解封裝出音訊編碼資料。

KFMP4Demuxer.java

public class KFMP4Demuxer {
    public static final int KFDemuxerErrorAudioSetDataSource = -2300;
    public static final int KFDemuxerErrorVideoSetDataSource = -2301;
    public static final int KFDemuxerErrorAudioReadData = -2302;
    public static final int KFDemuxerErrorVideoReadData = -2303;

    private static final String TAG = "KFDemuxer";
    private KFDemuxerConfig mConfig = null; ///< 解封裝配置
    private KFDemuxerListener mListener = null; ///< 回撥
    private MediaExtractor mAudioMediaExtractor = null; ///< 音訊解封裝器
    private MediaFormat mAudioMediaFormat = null; ///< 音訊格式描述
    private MediaExtractor mVideoMediaExtractor = null; ///< 視訊解封裝器
    private MediaFormat mVideoMediaFormat = null; ///< 視訊格式描述
    private MediaMetadataRetriever mRetriever = null; ///< 視訊資訊獲取例項
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主執行緒

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public KFMP4Demuxer(KFDemuxerConfig config, KFDemuxerListener listener) {
        mConfig = config;
        mListener = listener;
        if (mRetriever == null) {
            mRetriever = new MediaMetadataRetriever();
            mRetriever.setDataSource(mConfig.path);
        }

        ///< 初始化音訊解封裝器。
        if (hasAudio() && (config.demuxerType.value() & KFMediaBase.KFMediaType.KFMediaAudio.value()) != 0) {
            _setupAudioMediaExtractor();
        }

        ///< 初始化視訊解封裝器。
        if (hasVideo() && (config.demuxerType.value() & KFMediaBase.KFMediaType.KFMediaVideo.value()) != 0) {
            _setupVideoMediaExtractor();
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public void release() {
        ///< 釋放音視訊解封裝器、視訊資訊獲取例項。
        if (mAudioMediaExtractor != null) {
            mAudioMediaExtractor.release();
            mAudioMediaExtractor = null;
        }

        if (mVideoMediaExtractor != null) {
            mVideoMediaExtractor.release();
            mVideoMediaExtractor = null;
        }

        if (mRetriever != null) {
            mRetriever.release();
            mRetriever = null;
        }
    }

    public boolean hasVideo() {
        ///< 是否包含視訊。
        if (mRetriever == null) {
            return false;
        }
        String value = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
        return value != null && value.equals("yes");
    }

    public boolean hasAudio() {
        ///< 是否包含音訊。
        if (mRetriever == null) {
            return false;
        }
        String value = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO);
        return value != null && value.equals("yes");
    }

    public int duration() {
        ///< 檔案時長。
        if (mRetriever == null) {
            return 0;
        }
        return Integer.parseInt(mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int rotation() {
        ///< 視訊旋轉。
        if (mVideoMediaFormat == null) {
            return 0;
        }
        return mVideoMediaFormat.getInteger(MediaFormat.KEY_ROTATION);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public boolean isHEVC() {
        ///< 是否為 H.265。
        if (mVideoMediaFormat == null) {
            return false;
        }
        String mime = mVideoMediaFormat.getString(MediaFormat.KEY_MIME);
        return mime.contains("hevc") || mime.contains("dolby-vision");
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int width() {
        ///< 視訊寬度。
        if (mVideoMediaFormat == null) {
            return 0;
        }
        return mVideoMediaFormat.getInteger(MediaFormat.KEY_WIDTH);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int height() {
        ///< 視訊高度。
        if (mVideoMediaFormat == null) {
            return 0;
        }
        return mVideoMediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int samplerate() {
        ///< 音訊取樣率。
        if (mAudioMediaFormat == null) {
            return 0;
        }
        return mAudioMediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int channel() {
        ///< 音訊聲道數。
        if (mAudioMediaFormat == null) {
            return 0;
        }
        return mAudioMediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int audioProfile() {
        ///< AAC、HEAAC 等。
        if (mAudioMediaFormat == null) {
            return 0;
        }
        return mAudioMediaFormat.getInteger(MediaFormat.KEY_PROFILE);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public int videoProfile() {
        ///< 視訊畫質級別 BaseLine Main High 等。
        if (mVideoMediaFormat == null) {
            return 0;
        }
        return mVideoMediaFormat.getInteger(MediaFormat.KEY_PROFILE);
    }

    public MediaFormat audioMediaFormat() {
        return mAudioMediaFormat;
    }

    public MediaFormat videoMediaFormat() {
        return mVideoMediaFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public ByteBuffer readAudioSampleData(MediaCodec.BufferInfo bufferInfo) {
        ///< 音訊資料讀取。
        if (mAudioMediaExtractor == null) {
            return null;
        }

        ByteBuffer buffer = ByteBuffer.allocateDirect(500 * 1024);
        try {
            bufferInfo.size = mAudioMediaExtractor.readSampleData(buffer, 0);
        } catch (Exception e) {
            Log.e(TAG, "readSampleData" + e);
            return null;
        }

        if (bufferInfo.size > 0) {
            bufferInfo.flags = mAudioMediaExtractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
            bufferInfo.presentationTimeUs = mAudioMediaExtractor.getSampleTime();
            mAudioMediaExtractor.advance();
            return buffer;
        } else {
            bufferInfo.flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
            return null;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public ByteBuffer readVideoSampleData(MediaCodec.BufferInfo bufferInfo) {
        ///< 視訊資料讀取
        if (mVideoMediaExtractor == null) {
            return null;
        }

        ByteBuffer buffer = ByteBuffer.allocateDirect(1000 * 1024);
        try {
            bufferInfo.size = mVideoMediaExtractor.readSampleData(buffer, 0);
        } catch (Exception e) {
            Log.e(TAG, "readVideoData" + e);
            return null;
        }

        if (bufferInfo.size > 0) {
            bufferInfo.flags = mVideoMediaExtractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
            bufferInfo.presentationTimeUs = mVideoMediaExtractor.getSampleTime();
            mVideoMediaExtractor.advance();
            return buffer;
        } else {
            bufferInfo.flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
            return null;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void _setupAudioMediaExtractor() {
        ///< 初始化音訊解封裝器。
        if (mAudioMediaExtractor == null) {
            mAudioMediaExtractor = new MediaExtractor();
            try {
                mAudioMediaExtractor.setDataSource(mConfig.path);
            } catch (Exception e) {
                Log.e(TAG, "setDataSource" + e);
                _callBackError(KFDemuxerErrorAudioSetDataSource,e.getMessage());
                return;
            }

            ///< 查詢音訊軌道與格式描述。
            int numberTracks = mAudioMediaExtractor.getTrackCount();
            for(int index = 0; index < numberTracks; index ++) {
                MediaFormat format = mAudioMediaExtractor.getTrackFormat(index);
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("audio/")) {
                    mAudioMediaFormat = format;
                    mAudioMediaExtractor.selectTrack(index);
                    mAudioMediaExtractor.seekTo(0,MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
                }
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void _setupVideoMediaExtractor() {
        ///< 初始化視訊解封裝器。
        if (mVideoMediaExtractor == null) {
            mVideoMediaExtractor = new MediaExtractor();
            try {
                mVideoMediaExtractor.setDataSource(mConfig.path);
            } catch (Exception e) {
                Log.e(TAG, "setDataSource" + e);
                _callBackError(KFDemuxerErrorVideoSetDataSource,e.getMessage());
                return;
            }

            ///< 查詢視訊軌道與格式描述。
            int numberTracks = mVideoMediaExtractor.getTrackCount();
            for(int index = 0; index < numberTracks; index++) {
                MediaFormat format = mVideoMediaExtractor.getTrackFormat(index);
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("video/")) {
                    mVideoMediaFormat = format;
                    mVideoMediaExtractor.selectTrack(index);
                    mVideoMediaExtractor.seekTo(0,MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
                }
            }
        }
    }

    private void _callBackError(int error, String errorMsg) {
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.demuxerOnError(error,TAG + errorMsg);
            });
        }
    }
}

上面是 KFMP4Demuxer 的實現,從程式碼上可以看到主要有這幾個部分:

  • 1)構造方法建立解封裝器例項及獲取視訊資訊例項。

    • _setupAudioMediaExtractor 方法中初始化音訊解封裝器例項以及設定資料來源 setDataSource ,查詢音訊軌道下標與格式描述。
    • _setupVideoMediaExtractor 方法中初始化視訊解封裝器例項以及設定資料來源 setDataSource ,查詢視訊軌道下標與格式描述。
    • 初始化獲取視訊資訊例項, mRetriever 初始化視訊獲取資訊例項以及設定資料來源 setDataSource
  • 2)從音視訊輸入源讀取資料。

    • 音訊讀取方法 readAudioSampleData ,讀取完一幀移動下一幀 advance
    • 視訊讀取方法 readVideoSampleData ,讀取完一幀移動下一幀 advance
  • 3)清理解封裝例項、獲取視訊資訊例項, release

更具體細節見上述程式碼及其註釋。

2、解封裝 MP4 檔案中的音訊部分儲存為 AAC 檔案

我們還是在一個 MainActivity 中來實現對一個 MP4 檔案解封裝、獲取其中的音訊編碼資料並存儲為 AAC 檔案。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private KFMP4Demuxer mDemuxer; ///< 解封裝例項
    private KFDemuxerConfig mDemuxerConfig; ///< 解封裝配置
    private FileOutputStream mStream = null;

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
                ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions((Activity) this,
                    new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    1);
        }

        mDemuxerConfig = new KFDemuxerConfig();
        mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + "/2.mp4";
        mDemuxerConfig.demuxerType = KFMediaBase.KFMediaType.KFMediaAudio;
        if (mStream == null) {
            try {
                mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.aac");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }

        FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
        startParams.gravity = Gravity.CENTER_HORIZONTAL;
        Button startButton = new Button(this);
        startButton.setTextColor(Color.BLUE);
        startButton.setText("開始");
        startButton.setVisibility(View.VISIBLE);
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ///< 建立解封裝例項。
                if (mDemuxer == null) {
                    mDemuxer = new KFMP4Demuxer(mDemuxerConfig,mDemuxerListener);

                    ///< 讀取音訊資料。
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    ByteBuffer nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
                    while (nextBuffer != null) {
                        try {
                            ///< 新增 ADTS。
                            ByteBuffer adtsBuffer = KFAVTools.getADTS(bufferInfo.size,mDemuxer.audioProfile(),mDemuxer.samplerate(),mDemuxer.channel());
                            byte[] adtsBytes = new byte[adtsBuffer.capacity()];
                            adtsBuffer.get(adtsBytes);
                            mStream.write(adtsBytes);

                            byte[] dst = new byte[bufferInfo.size];
                            nextBuffer.get(dst);
                            mStream.write(dst);
                        }  catch (IOException e) {
                            e.printStackTrace();
                        }
                        nextBuffer = mDemuxer.readAudioSampleData(bufferInfo);
                    }
                    Log.i("KFDemuxer","complete");
                }
            }
        });
        addContentView(startButton, startParams);
    }

    private KFDemuxerListener mDemuxerListener = new KFDemuxerListener() {
        ///< 解封裝錯誤回撥。
        @Override
        public void demuxerOnError(int error, String errorMsg) {
            Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
        }
    };
}

上面是 MainActivity 的實現,其中主要包含這幾個部分:

  • 1)設定好待解封裝的資源。

    • mDemuxerConfig 中實現,我們這裡是一個 MP4 檔案。
  • 2)建立解封裝器。

    • new KFMP4Demuxer(mDemuxerConfig,mDemuxerListener)
  • 3)讀取解封裝後的音訊編碼資料並存儲為 AAC 檔案。

    • 迴圈讀取 readAudioSampleData AAC 裸資料。
    • 需要注意的是,我們從解封裝器讀取的音訊 AAC 編碼資料在儲存為 AAC 檔案時需要新增 ADTS 頭。生成一個 AAC packet 對應的 ADTS 頭資料在 KFAVTools 類的工具方法 static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel) 中實現。這個在前面的音訊編碼的 Demo 中已經介紹過了。

3、用工具播放 AAC 檔案

完成音訊採集和編碼後,可以將 sdcard 資料夾下面的 test.aac 檔案拷貝到電腦上,使用 ffplay 播放來驗證一下音訊採集是效果是否符合預期:

$ ffplay -i test.aac

關於播放 AAC 檔案的工具,可以參考 《FFmpeg 工具》第 2 節 ffplay 命令列工具《視覺化音視訊分析工具》第 1.1 節 Adobe Audition

- 完 -