浅析webrtc中音频的录制和播放流程
前言
本文是基于PineAppRtc項目https://github.com/thfhongfeng/PineAppRtc)
在webrtc中音頻的錄制和播放都是封裝在內部,一般情況下我們也不需要關注,直接使用即可。
但是最近有一個需求,需要將我們自己的數據進行傳輸,所以就需要將這些接口暴露出來使用。所以就需要去研究一下它的源碼,就有了這篇文章。
音頻引擎
在webrtc中其實是有不只一套音頻引擎的,其中有native層的使用OpenSL ES實現的,另外還有一套java層通過android api實現的。
這里注意,java層這套是在audio_device_java.jar中,包名是org.webrtc.voiceengine。但是在最新的官網webrtc代碼中還有一套包名org.webrtc.audio的,貌似是替代前面那套的。
但是在PineAppRtc項目中使用的版本只有org.webrtc.voiceengine這套。
默認情況下是使用OpenSL ES這套。但是可以使用
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true /* enable */);禁用這套,這樣就會使用java層的那套引擎。
那么我們如何將它們暴露出來,我們可以直接將這個包的源碼放到項目下,然后將這個jar包刪掉,這樣就可以直接修改代碼了。
發送數據(錄音)
在audio_device_java.jar中WebRtcAudioRecord這個類是負責錄音的。
這個類及下面函數都是webrtc底層自動調用,所以我們不需要考慮參數的來源,知道怎么使用就好。
首先是構造函數
WebRtcAudioRecord(long nativeAudioRecord) { this.nativeAudioRecord = nativeAudioRecord; ... }這個nativeAudioRecord很重要,是后續調用接口需要用到的重要參數。
下面再來看看init函數
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; }兩個參數分別是采樣率和聲道(1是單聲道,2是雙聲道)。這兩個參數也很重要,是webrtc通過前期socket協商后選定的。我們也可以修改這兩個參數,后面會說。
注意這里不能隨便修改bytebuffer的容量大小,因為底層會進行校驗。這個大小只能是(采樣率 / 100 * 聲道數 * 2),實際上就是每秒發送100次數據。
如果改動大小,native層會crash,報錯是
Check failed: frames_per_buffer_ == audio_parameters_.frames_per_10ms_buffer() (xxx vs. xxx)
最重要的是nativeCacheDirectBufferAddress這函數,可以看到傳入了一個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中拿到數據后,調用了nativeDataIsRecorded函數。
這里看到從record中拿到數據時傳入的時之前的bytebuffer,而調用nativeDataIsRecorded時,只傳入了長度和nativeAudioRecord。
所以可以看到,如果要用自己的數據(即不錄音)就需要先有nativeAudioRecord(通過構造函數獲得);然后調用nativeCacheDirectBufferAddress初始化;然后循環向bytebuffer寫入數據,寫入一次調用一次nativeDataIsRecorded發送出去。
接收數據(放音)
在audio_device_java.jar中WebRtcAudioTrack是負責播放的。
這個類及下面函數也是webrtc底層自動調用,所以我們不需要考慮參數的來源,知道怎么使用就好。
同樣先是構造函數
WebRtcAudioTrack(long nativeAudioTrack) {...this.nativeAudioTrack = nativeAudioTrack;... }同樣nativeAudioTrack很重要,跟上面的nativeAudioRecord類似
然后來看看init函數
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; }采樣率和聲道跟上面一樣,這里也創建了一個bytebuffer并傳入nativeCacheDirectBufferAddress。
這里的bytebuffer容量與錄音一樣不能隨意改動,否則crash。
然后再看看start函數
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);}... }其實跟錄音邏輯差不多,只不過這里先調用nativeGetPlayoutData讓底層將收到的數據寫入bytebuffer中,然后再通過write函數播放(這兩個write函數最終都調用AudioTrack的write函數)。
所以如果我們要自己處理接收的數據,只需要在這里調用nativeGetPlayoutData,然后從bytebuffer中讀取數據自己處理即可,后面的代碼都可以刪掉。
總結同樣跟錄音一樣,先構造函數拿nativeAudioTrack這值,然后創建了一個bytebuffer并傳入nativeCacheDirectBufferAddress,然后循環調用nativeGetPlayoutData獲取數據處理
采樣率、聲道等設定
關于這些參數的設定,是雙方經過協商定的,應該是一方將能支持的參數發送給另一方,另一方根據自己能支持的選出一個合適返回,然后雙方就都這個參數處理數據。
但是我們是否可以干預這個過程,比如雙方都支持的可能不只一個,我們不想使用自動選擇的那個合適的,怎么做?
在audio_device_java.jar中還有兩個類WebRtcAudioManager和WebRtcAudioUtils
這兩個里就可以做一些設置,比如
采樣率
在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; }因為這兩個的返回值直接影響聲道數:
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); }上面的代碼中可以看到還有其他設定,需要的話可以進行相應修改。
總結
這里我們只是簡單分析了一下錄制和播放的過程,知道我們應該從哪入手及怎么才能傳送現有音頻并獲取對方音頻數據,至于如果改造和后續的處理大家可以自己發揮了。
關注公眾號:BennuCTech,發送“電子書”獲取經典電子資料。
總結
以上是生活随笔為你收集整理的浅析webrtc中音频的录制和播放流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何向前一个Fragment回传信息?
- 下一篇: 浅析WebRtc中视频数据的收集和发送流