vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時獲得最新的音視頻技術(shù)文章损离。
這個公眾號會路線圖 式的遍歷分享音視頻技術(shù):音視頻基礎(chǔ)(完成) → 音視頻工具(完成) → 音視頻工程示例(進行中) → 音視頻工業(yè)實戰(zhàn)(準(zhǔn)備)盯蝴。
iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā),最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后率寡,再借助 iOS/Android 平臺的音視頻能力上手去實踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
過程,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(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ā)圈』加入我們趴樱,當(dāng)然也可以跳過直接看后續(xù)的內(nèi)容馒闷。
1酪捡、音頻解封裝模塊
首先,實現(xiàn)一個 KFDemuxerConfig
類用于定義音頻解封裝參數(shù)的配置窜司。這里包括了:視頻路徑沛善、解封裝類型這幾個參數(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 標(biāo)準(zhǔn)的文件中解封裝出音頻編碼數(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è)置數(shù)據(jù)源setDataSource
谨湘,查找音頻軌道下標(biāo)與格式描述绽快。 - 在
_setupVideoMediaExtractor
方法中初始化視頻解封裝器實例以及設(shè)置數(shù)據(jù)源setDataSource
芥丧,查找視頻軌道下標(biāo)與格式描述。 - 初始化獲取視頻信息實例坊罢,
mRetriever
初始化視頻獲取信息實例以及設(shè)置數(shù)據(jù)源setDataSource
续担。
- 在
- 2)從音視頻輸入源讀取數(shù)據(jù)。
- 音頻讀取方法
readAudioSampleData
活孩,讀取完一幀移動下一幀advance
物遇。 - 視頻讀取方法
readVideoSampleData
,讀取完一幀移動下一幀advance
憾儒。
- 音頻讀取方法
- 3)清理解封裝實例询兴、獲取視頻信息實例,
release
起趾。
更具體細(xì)節(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)設(shè)置好待解封裝的資源艺骂。
- 在
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 對應(yīng)的 ADTS 頭數(shù)據(jù)在
KFAVTools
類的工具方法static ByteBuffer getADTS(int size, int profile, int sampleRate, int channel)
中實現(xiàn)睦番。這個在前面的音頻編碼的 Demo 中已經(jīng)介紹過了类茂。
- 循環(huán)讀取
3、用工具播放 AAC 文件
完成音頻采集和編碼后托嚣,可以將 sdcard
文件夾下面的 test.aac
文件拷貝到電腦上巩检,使用 ffplay
播放來驗證一下音頻采集是效果是否符合預(yù)期:
$ ffplay -I test.aac
關(guān)于播放 AAC 文件的工具,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具和《可視化音視頻分析工具》第 1.1 節(jié) Adobe Audition示启。
- 完 -
推薦閱讀