vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時獲得最新的音視頻技術(shù)文章。
[圖片上傳失敗...(image-1f0a2a-1654072303086)]
這個公眾號會路線圖 式的遍歷分享音視頻技術(shù):音視頻基礎(chǔ)(完成) → 音視頻工具(完成) → 音視頻工程示例(進(jìn)行中) → 音視頻工業(yè)實(shí)戰(zhàn)(準(zhǔn)備)。
iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā)潭苞,最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后,再借助 iOS/Android 平臺的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
過程真朗,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)此疹。
在音視頻工程示例這個欄目,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)遮婶。
這里是 Android 第三篇:Android 音頻封裝 Demo蝗碎。這個 Demo 里包含以下內(nèi)容:
- 1)實(shí)現(xiàn)一個音頻采集模塊;
- 2)實(shí)現(xiàn)一個音頻編碼模塊蹭睡;
- 3)實(shí)現(xiàn)一個音頻封裝模塊衍菱;
- 4)串聯(lián)音頻采集、編碼肩豁、封裝模塊脊串,將采集到的音頻數(shù)據(jù)輸入給 AAC 編碼模塊進(jìn)行編碼辫呻,再將編碼后的數(shù)據(jù)輸入給 M4A 封裝模塊封裝和存儲;
- 5)詳盡的代碼注釋琼锋,幫你理解代碼邏輯和原理放闺。
如果你想獲得全部源碼和參與音視頻技術(shù)討論,可以知識星球搜索『關(guān)鍵幀的音視頻開發(fā)圈』加入我們缕坎,當(dāng)然也可以跳過直接看后續(xù)的內(nèi)容怖侦。
1、音頻采集模塊
在這個 Demo 中谜叹,音頻采集模塊 KFAudioCapture
的實(shí)現(xiàn)與 《Android 音頻采集 Demo》 中一樣匾寝,這里就不再重復(fù)介紹了,其接口如下:
KFAudioCapture.java
public class KFAudioCapture {
public KFAudioCapture(KFAudioCaptureConfig config,KFAudioCaptureListener listener);
public void startRunning(); // 開始采集音頻數(shù)據(jù)荷腊。
public void stopRunning(); // 停止采集音頻數(shù)據(jù)艳悔。
public void release(); // 釋放音頻采集。
}
2女仰、音頻編碼模塊
同樣的猜年,音頻編碼模塊 KFAudioByteBufferEncoder
的實(shí)現(xiàn)與《Android 音頻編碼 Demo》中一樣,這里就不再重復(fù)介紹了疾忍,其接口如下:
KFMediaCodecInterface.java
public interface KFMediaCodecInterface {
public static final int KFMediaCodecInterfaceErrorCreate = -2000;
public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
public static final int KFMediaCodecInterfaceErrorStart = -2002;
public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
public static final int KFMediaCodecInterfaceErrorParams = -2004;
public static int KFMediaCodeProcessParams = -1;
public static int KFMediaCodeProcessAgainLater = -2;
public static int KFMediaCodeProcessSuccess = 0;
///< 初始化 Codec乔外,第一個參數(shù)需告知使用編碼還是解碼。
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
///< 釋放 Codec一罩。
public void release();
///< 獲取輸出格式描述杨幼。
public MediaFormat getOutputMediaFormat();
///< 獲取輸入格式描述。
public MediaFormat getInputMediaFormat();
///< 處理每一幀數(shù)據(jù)擒抛,編碼前與編碼后都可以推汽,支持編解碼 2 種模式。
public int processFrame(KFFrame frame);
///< 清空 Codec 緩沖區(qū)歧沪。
public void flush();
}
3歹撒、音頻封裝模塊
接下來,我們來實(shí)現(xiàn)一個音頻封裝模塊诊胞,在這里輸入編碼后的數(shù)據(jù)暖夭,輸出封裝后的文件。
這次我們要封裝的格式是 M4A撵孤,屬于 MPEG-4 標(biāo)準(zhǔn)迈着,通常普通的 MPEG-4 文件擴(kuò)展名是 .mp4
,只包含音頻的 MPEG-4 文件擴(kuò)展名用 .m4a
邪码。所以裕菠,其實(shí)我們這里實(shí)現(xiàn)的是一個 MP4 封裝模塊,支持將音頻編碼數(shù)據(jù)封裝成 M4A闭专,也支持將音視頻數(shù)據(jù)封裝成 MP4奴潘。關(guān)于 MP4 格式旧烧,可以看一看《MP4 格式》這篇文章了解一下。
由于 MP4 封裝涉及到一些參數(shù)設(shè)置画髓,所以我們先實(shí)現(xiàn)一個 KFMuxerConfig
類用于定義 MP4 封裝的參數(shù)的配置掘剪。這里包括了:封裝文件輸出地址、封裝文件類型這幾個參數(shù)奈虾。
KFMuxerConfig.java
public class KFMuxerConfig {
///< 輸出路徑夺谁。
public String outputPath = null;
///< 封裝僅音頻、僅視頻肉微、音視頻匾鸥。
public KFMediaBase.KFMediaType muxerType = KFMediaBase.KFMediaType.KFMediaAV;
public KFMuxerConfig(String path) {
outputPath = path;
}
}
其中用到的 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;
}
}
}
接下來,我們來實(shí)現(xiàn) KFMP4Muxer
模塊浪册。
KFMP4Muxer.java
public class KFMP4Muxer {
public static final int KFMuxerErrorCreate = -2200;
public static final int KFMuxerErrorAudioAddTrack = -2201;
public static final int KFMuxerErrorVideoAddTrack = -2202;
private static final String TAG = "KFMuxer";
private KFMuxerConfig mConfig = null; ///< 封裝配置
private KFMuxerListener mListener = null; ///< 回調(diào)
private MediaMuxer mMediaMuxer = null; ///< 封裝實(shí)例
private int mVideoTrackIndex = -1; ///< 視頻 track 軌道下標(biāo)
private MediaFormat mVideoFormat = null; ///< 視頻輸入視頻格式描述
private List<KFBufferFrame> mVideoList = new ArrayList<>(); ///< 視頻輸入緩存
private int mAudioTrackIndex = -1; ///< 音頻 track 軌道下標(biāo)
private MediaFormat mAudioFormat = null; ///< 音頻輸入視頻格式描述
private List<KFBufferFrame> mAudioList = new ArrayList<>(); ///< 音頻輸入緩存
private boolean mIsStart = false;
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主線程
public KFMP4Muxer(KFMuxerConfig config, KFMuxerListener listener) {
mConfig = config;
mListener = listener;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void start() {
_setupMuxer();
}
public void stop() {
_stop();
}
public void setVideoMediaFormat(MediaFormat mediaFormat) {
mVideoFormat = mediaFormat;
}
public void setAudioMediaFormat(MediaFormat mediaFormat) {
mAudioFormat = mediaFormat;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
///< 寫入音視頻數(shù)據(jù)(編碼后數(shù)據(jù))扫腺。
public void writeSampleData(boolean isVideo, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) {
if ((bufferInfo.flags & BUFFER_FLAG_CODEC_CONFIG) != 0) {
return;
}
if (buffer ==null || bufferInfo == null || mMediaMuxer == null || bufferInfo.size == 0) {
return;
}
///< 校驗(yàn)視頻數(shù)據(jù)是否進(jìn)入。
if (!_hasAudioTrack() && !isVideo) {
return;
}
///< 校驗(yàn)視頻數(shù)據(jù)是否進(jìn)入村象。
if (!_hasVideoTrack() && isVideo) {
return;
}
///< 數(shù)據(jù)轉(zhuǎn)換結(jié)構(gòu)體 KFBufferFrame。
KFBufferFrame packet = new KFBufferFrame();
ByteBuffer newBuffer = ByteBuffer.allocateDirect(bufferInfo.size);
newBuffer.put(buffer).position(0);
MediaCodec.BufferInfo newInfo = new MediaCodec.BufferInfo();
newInfo.size = bufferInfo.size;
newInfo.flags = bufferInfo.flags;
newInfo.presentationTimeUs = bufferInfo.presentationTimeUs;
packet.buffer = newBuffer;
packet.bufferInfo = newInfo;
if (isVideo) {
///< 初始化視頻 Track攒至。
if (mVideoFormat != null && mVideoTrackIndex == -1) {
_setupVideoTrack();
}
mVideoList.add(packet);
} else {
///< 初始化音頻Track
if (mAudioFormat != null && mAudioTrackIndex == -1) {
_setupAudioTrack();
}
mAudioList.add(packet);
}
///< 校驗(yàn)音視頻 Track 是否都初始化好厚者。
if ((_hasAudioTrack() && _hasVideoTrack() && mAudioTrackIndex >=0 && mVideoTrackIndex >= 0) ||
(_hasAudioTrack() && !_hasVideoTrack() && mAudioTrackIndex >= 0) ||
(!_hasAudioTrack() && _hasVideoTrack() && mVideoTrackIndex >= 0)) {
if (!mIsStart) {
_start();
mIsStart = true;
}
///< 音視頻交錯,目的音視頻時間戳盡量不跳躍迫吐。
if(mIsStart){
_avInterleavedBuffers();
}
}
}
public void release() {
_stop();
}
private void _start() {
///< 開啟封裝库菲。
try {
if (mMediaMuxer != null) {
mMediaMuxer.start();
}
} catch (Exception e) {
Log.e(TAG, "start" + e);
}
}
private void _stop() {
///< 關(guān)閉封裝
try {
if (mMediaMuxer != null) {
///< 兜底一路沒進(jìn)來的 case,如果外層配置音視頻一起封裝但最終只進(jìn)來一路也會處理志膀。
if (!mIsStart && (mVideoTrackIndex != 0 || mAudioTrackIndex != 0) && (mVideoList.size() > 0 || mAudioList.size() > 0)) {
mMediaMuxer.start();
mIsStart = true;
}
///< 將緩沖中數(shù)據(jù)推入封裝器熙宇。
if (mIsStart) {
_appendAudioBuffers();
_appendVideoBuffers();
mMediaMuxer.stop();
}
///< 釋放封裝器實(shí)例。
mMediaMuxer.release();
mMediaMuxer = null;
}
} catch (Exception e) {
Log.e(TAG, "stop release" + e);
}
///< 清空相關(guān)緩存與標(biāo)記位溉浙。
mVideoTrackIndex = -1;
mAudioTrackIndex = -1;
mIsStart = false;
mVideoList.clear();
mAudioList.clear();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean _hasAudioTrack() {
return (mConfig.muxerType.value() & KFMediaBase.KFMediaType.KFMediaAudio.value()) != 0;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean _hasVideoTrack() {
return (mConfig.muxerType.value() & KFMediaBase.KFMediaType.KFMediaVideo.value()) != 0;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _setupMuxer() {
///< 初始化封裝器烫止。
if(mMediaMuxer == null){
try {
mMediaMuxer = new MediaMuxer(mConfig.outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
Log.e(TAG, "new MediaMuxer" + e);
_callBackError(KFMuxerErrorCreate,e.getMessage());
return;
}
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _setupVideoTrack() {
///< 根據(jù)外層輸入格式描述初始化視頻 Track。
if (mVideoFormat != null) {
///< 添加視頻 Track戳稽。
try {
mVideoTrackIndex = mMediaMuxer.addTrack(mVideoFormat);
} catch (Exception e) {
Log.e(TAG, "addTrack" + e);
_callBackError(KFMuxerErrorVideoAddTrack,e.getMessage());
}
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _setupAudioTrack() {
///< 根據(jù)外層輸入格式描述初始化音頻 Track馆蠕。
if(mAudioFormat != null){
///< 添加音頻 Track。
try {
mAudioTrackIndex = mMediaMuxer.addTrack(mAudioFormat);
} catch (Exception e) {
Log.e(TAG, "addTrack" + e);
_callBackError(KFMuxerErrorAudioAddTrack,e.getMessage());
}
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _avInterleavedBuffers() {
///< 音視頻交錯惊奇,通過對比時間戳大小交錯進(jìn)入互躬。
if (_hasVideoTrack() && _hasAudioTrack()) {
while (mAudioList.size() > 0 && mVideoList.size() > 0) {
KFBufferFrame audioPacket = mAudioList.get(0);
KFBufferFrame videoPacket = mVideoList.get(0);
if (audioPacket.bufferInfo.presentationTimeUs >= videoPacket.bufferInfo.presentationTimeUs) {
mMediaMuxer.writeSampleData(mVideoTrackIndex,videoPacket.buffer,videoPacket.bufferInfo);
mVideoList.remove(0);
} else {
mMediaMuxer.writeSampleData(mAudioTrackIndex,audioPacket.buffer,audioPacket.bufferInfo);
mAudioList.remove(0);
}
}
} else if (_hasVideoTrack()) {
_appendVideoBuffers();
} else if (_hasAudioTrack()) {
_appendAudioBuffers();
}
}
private void _appendAudioBuffers() {
///< 音頻隊列緩沖區(qū)推到封裝器。
while (mAudioList.size() > 0) {
KFBufferFrame packet = mAudioList.get(0);
mMediaMuxer.writeSampleData(mAudioTrackIndex,packet.buffer,packet.bufferInfo);
mAudioList.remove(0);
}
}
private void _appendVideoBuffers() {
///< 視頻隊列緩沖區(qū)推到封裝器颂郎。
while (mVideoList.size() > 0) {
KFBufferFrame packet = mVideoList.get(0);
mMediaMuxer.writeSampleData(mVideoTrackIndex,packet.buffer,packet.bufferInfo);
mVideoList.remove(0);
}
}
private void _callBackError(int error, String errorMsg) {
///< 錯誤回調(diào)吼渡。
if (mListener != null) {
mMainHandler.post(()->{
mListener.muxerOnError(error,TAG + errorMsg);
});
}
}
}
上面是 KFMP4Muxer
的實(shí)現(xiàn),從代碼上可以看到主要有這幾個部分:
- 1)創(chuàng)建封裝器實(shí)例乓序。調(diào)用
start
寺酪。 - 在
_setupMuxer
方法中實(shí)現(xiàn)舟奠,通過輸出路徑與格式 2 個參數(shù)生成。
- 在
- 2)創(chuàng)建音視頻軌道及添加音頻和視頻數(shù)據(jù)房维。調(diào)用
writeSampleData:
檢測音視頻數(shù)據(jù)會創(chuàng)建對應(yīng)的軌道沼瘫。 - 在
_setupVideoTrack
與_setupAudioTrack
方法中實(shí)現(xiàn)。音頻和視頻的格式描述分別為mVideoFormat
咙俩、mAudioFormat
耿戚。 - 當(dāng)音頻軌道與視頻軌道都創(chuàng)建好后,會觸發(fā)真正的開始
_start
阿趁。這樣設(shè)計的原因是外層可能優(yōu)先輸入音頻或視頻膜蛔,但封裝器開始前又需要創(chuàng)建音視頻軌道,所以這里實(shí)現(xiàn)了等待邏輯脖阵。
- 在
- 3)用兩個隊列作為緩沖區(qū)皂股,分別管理音頻和視頻待封裝數(shù)據(jù)。
- 這兩個隊列分別是
mAudioList
和mVideoList
命黔,存儲數(shù)據(jù)類型為KFBufferFrame
呜呐。 - 每次當(dāng)外部調(diào)用
writeSampleData:
方法送入待封裝數(shù)據(jù)時,都是把數(shù)據(jù)放入兩個隊列中的一個悍募,以便根據(jù)情況進(jìn)行后續(xù)的音視頻數(shù)據(jù)交織蘑辑。
- 這兩個隊列分別是
- 4)同時封裝音頻和視頻數(shù)據(jù)時,進(jìn)行音視頻數(shù)據(jù)交織坠宴。
- 在
_avInterleavedBuffers
方法中實(shí)現(xiàn)音視頻數(shù)據(jù)交織洋魂。當(dāng)帶封裝的數(shù)據(jù)既有音頻又有視頻,就需要根據(jù)他們的時間戳信息進(jìn)行交織喜鼓,這樣便于在播放該音視頻時提升體驗(yàn)副砍。
- 在
- 5)音視頻數(shù)據(jù)寫入封裝。
- 同時封裝音頻和視頻數(shù)據(jù)時庄岖,在做完音視頻交織后豁翎,即分別將交織后的音視頻數(shù)據(jù)寫入封裝器
mMediaMuxer writeSampleData
。在_avInterleavedBuffers
中實(shí)現(xiàn)顿锰。 - 單獨(dú)封裝音頻或視頻數(shù)據(jù)時谨垃,則直接將數(shù)據(jù)寫入封裝器
mMediaMuxer writeSampleData
。分別在_appendAudioBuffers
和_appendVideoBuffers
方法中實(shí)現(xiàn)硼控。
- 同時封裝音頻和視頻數(shù)據(jù)時庄岖,在做完音視頻交織后豁翎,即分別將交織后的音視頻數(shù)據(jù)寫入封裝器
- 6)停止寫入刘陶。
- 在
stop
→_stop
方法中實(shí)現(xiàn)。 - 在停止前牢撼,還需要消費(fèi)掉
mAudioList
和mVideoList
的剩余數(shù)據(jù)匙隔,要調(diào)用_appendAudioBuffers
與_appendVideoBuffers
。 - 封裝器執(zhí)行停止操作
mMediaMuxer stop
熏版。
- 在
更具體細(xì)節(jié)見上述代碼及其注釋纷责。
4捍掺、采集音頻數(shù)據(jù)進(jìn)行 AAC 編碼以及 M4A 封裝和存儲
我們還是在一個 MainActivity
中來實(shí)現(xiàn)采集音頻數(shù)據(jù)進(jìn)行 AAC 編碼、M4A 封裝和存儲的邏輯再膳。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private KFAudioCapture mAudioCapture = null; ///< 音頻采集
private KFAudioCaptureConfig mAudioCaptureConfig = null; ///< 音頻采集配置
private KFMediaCodecInterface mEncoder = null; ///< 音頻編碼
private MediaFormat mAudioEncoderFormat = null; ///< 音頻編碼格式描述
private KFMP4Muxer mMuxer; ///< 封裝起器
private KFMuxerConfig mMuxerConfig; ///< 封裝器配置
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
///< 申請存儲挺勿、音頻采集權(quán)限。
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);
}
///< 創(chuàng)建采集實(shí)例喂柒。
mAudioCaptureConfig = new KFAudioCaptureConfig();
mAudioCapture = new KFAudioCapture(mAudioCaptureConfig,mAudioCaptureListener);
mAudioCapture.startRunning();
mMuxerConfig = new KFMuxerConfig(Environment.getExternalStorageDirectory().getPath() + "/test.m4a");
mMuxerConfig.muxerType = KFMediaBase.KFMediaType.KFMediaAudio;
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)建音頻編碼實(shí)例不瓶。
if (mEncoder == null) {
mEncoder = new KFAudioByteBufferEncoder();
MediaFormat mediaFormat = KFAVTools.createAudioFormat(mAudioCaptureConfig.sampleRate,mAudioCaptureConfig.channel,96*1000);
mEncoder.setup(true,mediaFormat,mAudioEncoderListener,null);
((Button)view).setText("停止");
mMuxer = new KFMP4Muxer(mMuxerConfig,mMuxerListener);
} else {
mEncoder.release();
mEncoder = null;
mMuxer.stop();
mMuxer.release();
mMuxer = null;
((Button)view).setText("開始");
}
}
});
addContentView(startButton, startParams);
}
private KFAudioCaptureListener mAudioCaptureListener = new KFAudioCaptureListener() {
@Override
public void onError(int error, String errorMsg) {
Log.e("KFAudioCapture","errorCode" + error + "msg"+errorMsg);
}
@Override
public void onFrameAvailable(KFFrame frame) {
///< 采集回調(diào)輸入編碼。
if (mEncoder != null) {
mEncoder.processFrame(frame);
}
}
};
private KFMediaCodecListener mAudioEncoderListener = new KFMediaCodecListener() {
@Override
public void onError(int error, String errorMsg) {
Log.i("KFMediaCodecListener","error" + error + "msg" + errorMsg);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void dataOnAvailable(KFFrame frame) {
///< 編碼回調(diào)寫入封裝器灾杰。
if (mAudioEncoderFormat == null && mEncoder != null) {
mAudioEncoderFormat = mEncoder.getOutputMediaFormat();
mMuxer.setAudioMediaFormat(mEncoder.getOutputMediaFormat());
mMuxer.start();
}
if (mMuxer != null) {
mMuxer.writeSampleData(false,((KFBufferFrame)frame).buffer,((KFBufferFrame)frame).bufferInfo);
}
}
};
private KFMuxerListener mMuxerListener = new KFMuxerListener() {
@Override
public void muxerOnError(int error, String errorMsg) {
///< 音頻封裝錯誤回調(diào)蚊丐。
Log.i("KFMuxerListener","error" + error + "msg" + errorMsg);
}
};
}
上面是 MainActivity
的實(shí)現(xiàn),其中主要包含這幾個部分:
- 1)在采集音頻前需要設(shè)置
Manifest.permission.RECORD_AUDIO
權(quán)限艳吠。 - 2)通過啟動和停止音頻采集來驅(qū)動整個采集和編碼流程麦备。
- 3)在采集模塊
KFAudioCapture
的數(shù)據(jù)回調(diào)中將數(shù)據(jù)交給編碼模塊KFAudioByteBufferEncoder
進(jìn)行編碼。 - 在
KFAudioCaptureListener
的onFrameAvailable
回調(diào)中實(shí)現(xiàn)昭娩。
- 在
- 4)在編碼模塊
KFAudioByteBufferEncoder
的數(shù)據(jù)回調(diào)中獲取編碼后的 AAC 裸流數(shù)據(jù)凛篙,并將數(shù)據(jù)交給封裝器KFMP4Muxer
進(jìn)行封裝。 - 在
KFMediaCodecListener
的dataOnAvailable
回調(diào)中實(shí)現(xiàn)题禀。
- 在
- 5)在調(diào)用
stop
停止整個流程后鞋诗,如果沒有出現(xiàn)錯誤,封裝的 M4A 文件會被存儲到mMuxerConfig
設(shè)置的路徑迈嘹。
5、用工具播放 M4A 文件
完成音頻采集和編碼后全庸,可以將 sdcard
文件夾下面的 test.m4a
文件拷貝到電腦上秀仲,使用 ffplay
播放來驗(yàn)證一下音頻采集是效果是否符合預(yù)期:
$ ffplay -I test.m4a
關(guān)于播放 M4A 文件的工具,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具和《可視化音視頻分析工具》第 1.1 節(jié) Adobe Audition壶笼。
上面我們講過 M4A 格式是屬于 MPEG-4 標(biāo)準(zhǔn)神僵,所以我們這里還可以用《可視化音視頻分析工具》第 3.1 節(jié) MP4Box.js 等工具來查看它的格式:
[圖片上傳失敗...(image-f772b-1654072303086)]
- 完 -
推薦閱讀