浅析webrtc中音频的录制和播放流程
前言
本文是基于PineAppRtc項目https://github.com/thfhongfeng/PineAppRtc)
在webrtc中音頻的錄制和播放都是封裝在內(nèi)部,一般情況下我們也不需要關(guān)注,直接使用即可。
但是最近有一個需求,需要將我們自己的數(shù)據(jù)進行傳輸,所以就需要將這些接口暴露出來使用。所以就需要去研究一下它的源碼,就有了這篇文章。
音頻引擎
在webrtc中其實是有不只一套音頻引擎的,其中有native層的使用OpenSL ES實現(xiàn)的,另外還有一套java層通過android api實現(xiàn)的。
這里注意,java層這套是在audio_device_java.jar中,包名是org.webrtc.voiceengine。但是在最新的官網(wǎng)webrtc代碼中還有一套包名org.webrtc.audio的,貌似是替代前面那套的。
但是在PineAppRtc項目中使用的版本只有org.webrtc.voiceengine這套。
默認情況下是使用OpenSL ES這套。但是可以使用
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true /* enable */);禁用這套,這樣就會使用java層的那套引擎。
那么我們?nèi)绾螌⑺鼈儽┞冻鰜?#xff0c;我們可以直接將這個包的源碼放到項目下,然后將這個jar包刪掉,這樣就可以直接修改代碼了。
發(fā)送數(shù)據(jù)(錄音)
在audio_device_java.jar中WebRtcAudioRecord這個類是負責(zé)錄音的。
這個類及下面函數(shù)都是webrtc底層自動調(diào)用,所以我們不需要考慮參數(shù)的來源,知道怎么使用就好。
首先是構(gòu)造函數(shù)
WebRtcAudioRecord(long nativeAudioRecord) { this.nativeAudioRecord = nativeAudioRecord; ... }這個nativeAudioRecord很重要,是后續(xù)調(diào)用接口需要用到的重要參數(shù)。
下面再來看看init函數(shù)
private int initRecording(int sampleRate, int channels) {if (this.audioRecord != null) {this.reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording.");return -1;} else {int bytesPerFrame = channels * 2;int framesPerBuffer = sampleRate / 100;this.byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);this.emptyBytes = new byte[this.byteBuffer.capacity()];this.nativeCacheDirectBufferAddress(this.byteBuffer, this.nativeAudioRecord);...}return framesPerBuffer; }兩個參數(shù)分別是采樣率和聲道(1是單聲道,2是雙聲道)。這兩個參數(shù)也很重要,是webrtc通過前期socket協(xié)商后選定的。我們也可以修改這兩個參數(shù),后面會說。
注意這里不能隨便修改bytebuffer的容量大小,因為底層會進行校驗。這個大小只能是(采樣率 / 100 * 聲道數(shù) * 2),實際上就是每秒發(fā)送100次數(shù)據(jù)。
如果改動大小,native層會crash,報錯是
Check failed: frames_per_buffer_ == audio_parameters_.frames_per_10ms_buffer() (xxx vs. xxx)
最重要的是nativeCacheDirectBufferAddress這函數(shù),可以看到傳入了一個bytebuffer和nativeAudioRecord,后面就會用到。
nativeCacheDirectBufferAddress之后就是初始化AudioRecorder等。
然后再看看startRecording
private boolean startRecording() {...if (this.audioRecord.getRecordingState() != 3) {...} else {this.audioThread = new WebRtcAudioRecord.AudioRecordThread("AudioRecordJavaThread");this.audioThread.start();return true;} }可以看到啟動了一個線程,線程里做了什么
public void run() {...while(this.keepAlive) {int bytesRead = WebRtcAudioRecord.this.audioRecord.read(WebRtcAudioRecord.this.byteBuffer, WebRtcAudioRecord.this.byteBuffer.capacity());if (bytesRead == WebRtcAudioRecord.this.byteBuffer.capacity()) {...if (this.keepAlive) {WebRtcAudioRecord.this.nativeDataIsRecorded(bytesRead, WebRtcAudioRecord.this.nativeAudioRecord);}} else {...}}... }從record中拿到數(shù)據(jù)后,調(diào)用了nativeDataIsRecorded函數(shù)。
這里看到從record中拿到數(shù)據(jù)時傳入的時之前的bytebuffer,而調(diào)用nativeDataIsRecorded時,只傳入了長度和nativeAudioRecord。
所以可以看到,如果要用自己的數(shù)據(jù)(即不錄音)就需要先有nativeAudioRecord(通過構(gòu)造函數(shù)獲得);然后調(diào)用nativeCacheDirectBufferAddress初始化;然后循環(huán)向bytebuffer寫入數(shù)據(jù),寫入一次調(diào)用一次nativeDataIsRecorded發(fā)送出去。
接收數(shù)據(jù)(放音)
在audio_device_java.jar中WebRtcAudioTrack是負責(zé)播放的。
這個類及下面函數(shù)也是webrtc底層自動調(diào)用,所以我們不需要考慮參數(shù)的來源,知道怎么使用就好。
同樣先是構(gòu)造函數(shù)
WebRtcAudioTrack(long nativeAudioTrack) {...this.nativeAudioTrack = nativeAudioTrack;... }同樣nativeAudioTrack很重要,跟上面的nativeAudioRecord類似
然后來看看init函數(shù)
private boolean initPlayout(int sampleRate, int channels) {...int bytesPerFrame = channels * 2;this.byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / 100));this.emptyBytes = new byte[this.byteBuffer.capacity()];this.nativeCacheDirectBufferAddress(this.byteBuffer, this.nativeAudioTrack);...return true; }采樣率和聲道跟上面一樣,這里也創(chuàng)建了一個bytebuffer并傳入nativeCacheDirectBufferAddress。
這里的bytebuffer容量與錄音一樣不能隨意改動,否則crash。
然后再看看start函數(shù)
private boolean startPlayout() {...if (this.audioTrack.getPlayState() != 3) {...} else {this.audioThread = new WebRtcAudioTrack.AudioTrackThread("AudioTrackJavaThread");this.audioThread.start();return true;} }也是開啟了一個線程,線程里
public void run() {...for(int sizeInBytes = WebRtcAudioTrack.this.byteBuffer.capacity(); this.keepAlive; WebRtcAudioTrack.this.byteBuffer.rewind()) {WebRtcAudioTrack.this.nativeGetPlayoutData(sizeInBytes, WebRtcAudioTrack.this.nativeAudioTrack);...int bytesWritten;if (WebRtcAudioUtils.runningOnLollipopOrHigher()) {bytesWritten = this.writeOnLollipop(WebRtcAudioTrack.this.audioTrack, WebRtcAudioTrack.this.byteBuffer, sizeInBytes);} else {bytesWritten = this.writePreLollipop(WebRtcAudioTrack.this.audioTrack, WebRtcAudioTrack.this.byteBuffer, sizeInBytes);}... }其實跟錄音邏輯差不多,只不過這里先調(diào)用nativeGetPlayoutData讓底層將收到的數(shù)據(jù)寫入bytebuffer中,然后再通過write函數(shù)播放(這兩個write函數(shù)最終都調(diào)用AudioTrack的write函數(shù))。
所以如果我們要自己處理接收的數(shù)據(jù),只需要在這里調(diào)用nativeGetPlayoutData,然后從bytebuffer中讀取數(shù)據(jù)自己處理即可,后面的代碼都可以刪掉。
總結(jié)同樣跟錄音一樣,先構(gòu)造函數(shù)拿nativeAudioTrack這值,然后創(chuàng)建了一個bytebuffer并傳入nativeCacheDirectBufferAddress,然后循環(huán)調(diào)用nativeGetPlayoutData獲取數(shù)據(jù)處理
采樣率、聲道等設(shè)定
關(guān)于這些參數(shù)的設(shè)定,是雙方經(jīng)過協(xié)商定的,應(yīng)該是一方將能支持的參數(shù)發(fā)送給另一方,另一方根據(jù)自己能支持的選出一個合適返回,然后雙方就都這個參數(shù)處理數(shù)據(jù)。
但是我們是否可以干預(yù)這個過程,比如雙方都支持的可能不只一個,我們不想使用自動選擇的那個合適的,怎么做?
在audio_device_java.jar中還有兩個類WebRtcAudioManager和WebRtcAudioUtils
這兩個里就可以做一些設(shè)置,比如
采樣率
在WebRtcAudioManager中
private int getNativeOutputSampleRate() { // if (WebRtcAudioUtils.runningOnEmulator()) { // Logging.d("WebRtcAudioManager", "Running emulator, overriding sample rate to 8 kHz."); // return 8000; // } else if (WebRtcAudioUtils.isDefaultSampleRateOverridden()) { // Logging.d("WebRtcAudioManager", "Default sample rate is overriden to " + WebRtcAudioUtils.getDefaultSampleRateHz() + " Hz"); // return WebRtcAudioUtils.getDefaultSampleRateHz(); // } else { // int sampleRateHz; // if (WebRtcAudioUtils.runningOnJellyBeanMR1OrHigher()) { // sampleRateHz = this.getSampleRateOnJellyBeanMR10OrHigher(); // } else { // sampleRateHz = WebRtcAudioUtils.getDefaultSampleRateHz(); // } // // Logging.d("WebRtcAudioManager", "Sample rate is set to " + sampleRateHz + " Hz"); // return sampleRateHz; // }return 16000; }將原代碼去掉,直接返回我們想要的采樣率。
聲道
同樣在WebRtcAudioManager中
public static synchronized boolean getStereoOutput() {return useStereoOutput; }public static synchronized boolean getStereoInput() {return useStereoInput; }因為這兩個的返回值直接影響聲道數(shù):
private void storeAudioParameters() {this.outputChannels = getStereoOutput() ? 2 : 1;this.inputChannels = getStereoInput() ? 2 : 1;this.sampleRate = this.getNativeOutputSampleRate();this.hardwareAEC = isAcousticEchoCancelerSupported();this.hardwareAGC = false;this.hardwareNS = isNoiseSuppressorSupported();this.lowLatencyOutput = this.isLowLatencyOutputSupported();this.lowLatencyInput = this.isLowLatencyInputSupported();this.proAudio = this.isProAudioSupported();this.outputBufferSize = this.lowLatencyOutput ? this.getLowLatencyOutputFramesPerBuffer() : getMinOutputFrameSize(this.sampleRate, this.outputChannels);this.inputBufferSize = this.lowLatencyInput ? this.getLowLatencyInputFramesPerBuffer() : getMinInputFrameSize(this.sampleRate, this.inputChannels); }上面的代碼中可以看到還有其他設(shè)定,需要的話可以進行相應(yīng)修改。
總結(jié)
這里我們只是簡單分析了一下錄制和播放的過程,知道我們應(yīng)該從哪入手及怎么才能傳送現(xiàn)有音頻并獲取對方音頻數(shù)據(jù),至于如果改造和后續(xù)的處理大家可以自己發(fā)揮了。
關(guān)注公眾號:BennuCTech,發(fā)送“電子書”獲取經(jīng)典電子資料。
總結(jié)
以上是生活随笔為你收集整理的浅析webrtc中音频的录制和播放流程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何向前一个Fragment回传信息?
- 下一篇: 浅析WebRtc中视频数据的收集和发送流