Android AVDemo(4):音訊解封裝,從 MP4 中解封裝出 AAC丨音影片工程示例
塞尚《河流》
這個公眾號會 路線圖 式的遍歷分享音影片技術 : 音影片基礎(完成) → 音影片工具(完成) → 音影片工程示例(進行中) → 音影片工業實戰(準備) 。 關注一下成本不高,錯過乾貨損失不小 ↓↓↓
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 。
- 完 -
- 想在自己的影片平臺支援 HDR 需要做哪些工作?丨有問有答
- Android AVDemo(11):影片轉封裝,從 MP4 到 MP4丨音影片工程示例
- 音影片面試題集錦 2022.05
- Android AVDemo(6):音訊渲染,免費獲得原始碼丨音影片工程示例
- Android AVDemo(4):音訊解封裝,從 MP4 中解封裝出 AAC丨音影片工程示例
- 如何根據 NALU 裸流資料來判斷其是 H.264 還是 H.265 編碼?丨有問有答
- 音影片知識圖譜 2022.04
- Android AVDemo(2):音訊編碼,採集 PCM 資料編碼為 AAC丨音影片工程示例
- 音影片面試題集錦 2022.04
- Android AVDemo(1):音訊採集,免費獲取全部原始碼丨音影片工程示例
- iOS 影片處理框架及重點 API 合集丨音影片工程示例
- iOS AVDemo(13):影片渲染,用 Metal 渲染丨音影片工程示例
- 如何像抖音直播一樣,從 App 直播間到桌面畫中畫實現畫面無縫切換?丨有問有答
- 如何在影片採集流水線中增加濾鏡處理節點?丨有問有答
- iOS AVDemo(11):影片轉封裝,從 MP4 到 MP4丨音影片工程示例
- 音影片知識圖譜 2022.03
- iOS AVDemo(8):影片編碼,H.264 和 H.265 都支援丨音影片工程示例
- iOS AVDemo(7):影片採集,影片系列來了丨音影片工程示例
- iOS 音訊處理框架及重點 API 合集丨音影片工程示例
- iOS AVDemo(6):音訊渲染,免費獲得原始碼丨音影片工程示例