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

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時獲得最新的音視頻技術(shù)文章。

塞尚《河流》 .jpeg

這個公眾號會路線圖 式的遍歷分享音視頻技術(shù)音視頻基礎(chǔ)(完成)音視頻工具(完成)音視頻工程示例(進行中) → 音視頻工業(yè)實戰(zhàn)(準備)。

iOS/Android 客戶端開發(fā)同學如果想要開始學習音視頻開發(fā),最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后,再借助 iOS/Android 平臺的音視頻能力上手去實踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程,并借助音視頻工具來分析和理解對應的音視頻數(shù)據(jù)。

音視頻工程示例這個欄目,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)。

這里是 Android 第四篇:Android 音頻解封裝 Demo。這個 Demo 里包含以下內(nèi)容:

  • 1)實現(xiàn)一個音頻解封裝模塊;
  • 2)實現(xiàn)對 MP4 文件中音頻部分的解封裝邏輯并將解封裝后的編碼數(shù)據(jù)存儲為 AAC 文件;
  • 3)詳盡的代碼注釋,幫你理解代碼邏輯和原理。

如果你想獲得全部源碼和參與音視頻技術(shù)討論,可以知識星球搜索『關(guān)鍵幀的音視頻開發(fā)圈』加入我們,當然也可以跳過直接看后續(xù)的內(nèi)容。

1、音頻解封裝模塊

首先,實現(xiàn)一個 KFDemuxerConfig 類用于定義音頻解封裝參數(shù)的配置。這里包括了:視頻路徑、解封裝類型這幾個參數(shù)。這樣設計是因為這個配置類不僅會用于音頻解封裝,后續(xù)的視頻解封裝也會使用。

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;
        }
    }
}

接下來,我們實現(xiàn)一個 KFMP4Demuxer 類來實現(xiàn) MP4 的解封裝。它能從符合 MP4 標準的文件中解封裝出音頻編碼數(shù)據(jù)。

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; ///< 回調(diào)
    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() {
        ///< 視頻旋轉(zhuǎn)。
        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() {
        ///< 音頻聲道數(shù)。
        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() {
        ///< 視頻畫質(zhì)級別 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) {
        ///< 音頻數(shù)據(jù)讀取。
        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) {
        ///< 視頻數(shù)據(jù)讀取
        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 的實現(xiàn),從代碼上可以看到主要有這幾個部分:

  • 1)構(gòu)造方法創(chuàng)建解封裝器實例及獲取視頻信息實例。
    • _setupAudioMediaExtractor 方法中初始化音頻解封裝器實例以及設置數(shù)據(jù)源 setDataSource,查找音頻軌道下標與格式描述。
    • _setupVideoMediaExtractor 方法中初始化視頻解封裝器實例以及設置數(shù)據(jù)源 setDataSource,查找視頻軌道下標與格式描述。
    • 初始化獲取視頻信息實例,mRetriever 初始化視頻獲取信息實例以及設置數(shù)據(jù)源 setDataSource。
  • 2)從音視頻輸入源讀取數(shù)據(jù)。
    • 音頻讀取方法 readAudioSampleData,讀取完一幀移動下一幀 advance。
    • 視頻讀取方法 readVideoSampleData,讀取完一幀移動下一幀 advance
  • 3)清理解封裝實例、獲取視頻信息實例,release。

更具體細節(jié)見上述代碼及其注釋

2、解封裝 MP4 文件中的音頻部分存儲為 AAC 文件

我們還是在一個 MainActivity 中來實現(xiàn)對一個 MP4 文件解封裝、獲取其中的音頻編碼數(shù)據(jù)并存儲為 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) {
                ///< 創(chuàng)建解封裝實例。
                if (mDemuxer == null) {
                    mDemuxer = new KFMP4Demuxer(mDemuxerConfig,mDemuxerListener);

                    ///< 讀取音頻數(shù)據(jù)。
                    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() {
        ///< 解封裝錯誤回調(diào)。
        @Override
        public void demuxerOnError(int error, String errorMsg) {
            Log.i("KFDemuxer","error" + error + "msg" + errorMsg);
        }
    };
}

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

  • 1)設置好待解封裝的資源。
    • mDemuxerConfig 中實現(xiàn),我們這里是一個 MP4 文件。
  • 2)創(chuàng)建解封裝器。
    • new KFMP4Demuxer(mDemuxerConfig,mDemuxerListener)
  • 3)讀取解封裝后的音頻編碼數(shù)據(jù)并存儲為 AAC 文件。
    • 循環(huán)讀取 readAudioSampleData AAC 裸數(shù)據(jù)。
    • 需要注意的是,我們從解封裝器讀取的音頻 AAC 編碼數(shù)據(jù)在存儲為 AAC 文件時需要添加 ADTS 頭。生成一個 AAC packet 對應的 ADTS 頭數(shù)據(jù)在 KFAVTools 類的工具方法 static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel) 中實現(xiàn)。這個在前面的音頻編碼的 Demo 中已經(jīng)介紹過了。

3、用工具播放 AAC 文件

完成音頻采集和編碼后,可以將 sdcard 文件夾下面的 test.aac 文件拷貝到電腦上,使用 ffplay 播放來驗證一下音頻采集是效果是否符合預期:

$ ffplay -I test.aac

關(guān)于播放 AAC 文件的工具,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具《可視化音視頻分析工具》第 1.1 節(jié) Adobe Audition。

- 完 -

推薦閱讀

《Android AVDemo(3):音頻封裝》

《Android AVDemo(2):音頻編碼》

《Android AVDemo(1):音頻采集》

《iOS AVDemo(4):音頻解封裝》

《iOS AVDemo(3):音頻封裝》

《iOS AVDemo(2):音頻編碼》

《iOS AVDemo(1):音頻采集》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容