@
做過WebRTC的音視頻通話應該知道WebRTC的sdk只暴露了麥克風輸入數據和視頻數據,如果要實現音視頻錄制該怎么辦呢?當然可以在通話的各個終端分別進行錄制,然后上傳服務器進行處理。那如果想在一個設備上進行統一錄制呢?通話對方的音頻數據該如何獲取?
WebRTC是在哪輸出音頻數據的?
在網上搜索了一圈都說要改源碼,WebRTC源碼10幾個g,還在牆外,編譯也有難度,那如何跨過這一步呢?
這一步我們就要去找找源碼了。
JavaAudioDeviceModule
在創建PeerConnectionFactory時要傳入JavaAudioDeviceModule,即使不傳,也會幫我們創建一個默認的。看這個就是用來操作音頻相關的。

gradle下載的源碼是沒有注釋的,可以去網上找找
可以看到AudioRecord作為音頻輸入,AudioTrack作為音頻輸出。
因為可以拿到輸入的數據,暫時先不管,先去看看AudioTrack。
WebRtcAudioTrack

查找一圈之后找到了WebRtcAudioTrack,再進去看看。

坑,這個類竟然不是public...,算了,這是源碼,也沒轍。
既然找到了AudioTrack,再找找AudioTrack.write()方法是在那調用的。

在AudioTrackThread.writeBytes()方法中,

到這里就大概了解AudioTrackThread是用來讀取播放數據,然后write到AudioTrack中。到這里,就找到了我們想要的數據,那該如何取出來呢?
獲取write到AudioTrack的數據
首先要確定的是WebRtcAudioTrack這個類僅包可見,所以要創建一個相同的包才能讀取到。
AudioTrackThread也是private,所以能操作的只有AudioTrack,要用到反射,來個狸貓換太子,把WebRtcAudioTrack中的audioTrack,替換成自己自定義的,然后從write()回調出數據即可。
自定義類繼承AudioTrack
首先要自定義一個類,繼承AudioTrack
package org.webrtc.audio
class AudioTrackInterceptor constructor(
/**
* 即:原[WebRtcAudioTrack.audioTrack]
*/
private var originalTrack: AudioTrack,
/**
* 音頻數據輸出回調
*/
private var samplesReadyCallback: JavaAudioDeviceModule.SamplesReadyCallback
) : AudioTrack(//不用關心這里傳的參數,只是一個殼
AudioManager.STREAM_VOICE_CALL,
44100,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
8192,
MODE_STREAM
) {
}
自定義類其實就是一個空殼,不用關心構造方法中傳的參數
這里有兩個傳參,一個是原WebRtcAudioTrack.audioTrack,另外一個就是數據回調,基本的思想就是要把原WebRtcAudioTrack.audioTrack調用的相關方法要重寫一遍,然后使用originalTrack重新調用一遍即可,比如這樣:
...
override fun getState(): Int {
return originalTrack.state
}
override fun play() {
originalTrack.play()
}
override fun getPlayState(): Int {
return originalTrack.playState
}
...
下面就是就是重中之重,拿到輸出的數據,先看看源代碼是怎么處理的
private int writeBytes(AudioTrack audioTrack, ByteBuffer byteBuffer, int sizeInBytes) {
if (Build.VERSION.SDK_INT >= 21) {
//android5.0及以上調用
return audioTrack.write(byteBuffer, sizeInBytes, AudioTrack.WRITE_BLOCKING);
} else {
//android5.0以下調用
return audioTrack.write(byteBuffer.array(), byteBuffer.arrayOffset(), sizeInBytes);
}
}
在AudioTrack中有很多write()方法,但源碼中只調用了上面的兩種,所以單獨處理這兩種就可以了。
/**
* [WebRtcAudioTrack.AudioTrackThread.writeBytes]
* 寫入音頻數據,這里我們處理一下,回調即可
*/
override fun write(audioData: ByteArray, offsetInBytes: Int, sizeInBytes: Int): Int {
val write = originalTrack.write(audioData, offsetInBytes, sizeInBytes)
if (write == sizeInBytes) {
val bytes = audioData.copyOfRange(offsetInBytes, offsetInBytes + sizeInBytes)
samplesReadyCallback.onWebRtcAudioRecordSamplesReady(
JavaAudioDeviceModule.AudioSamples(
originalTrack.audioFormat,
originalTrack.channelCount,
originalTrack.sampleRate,
bytes
)
)
}
return write
}
/**
* [WebRtcAudioTrack.AudioTrackThread.writeBytes]
* 寫入音頻數據,這里我們處理一下,回調即可
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun write(audioData: ByteBuffer, sizeInBytes: Int, writeMode: Int): Int {
val position = audioData.position()
val from = if (audioData.isDirect) position else audioData.arrayOffset() + position
val write = originalTrack.write(audioData, sizeInBytes, writeMode)
if (write == sizeInBytes) {
val bytes = audioData.array().copyOfRange(from, from + sizeInBytes)
samplesReadyCallback.onWebRtcAudioRecordSamplesReady(
JavaAudioDeviceModule.AudioSamples(
originalTrack.audioFormat,
originalTrack.channelCount,
originalTrack.sampleRate,
bytes
)
)
}
return write
}
到這里,用於替換的類就基本上完成了。
反射,替換WebRtcAudioTrack.audioTrack
直接上代碼
package org.webrtc.audio
/**
* 回調音頻輸入數據
* 反射,替換[WebRtcAudioTrack.audioTrack],使用[AudioTrackInterceptor]
* 其中要把[WebRtcAudioTrack.audioTrack]賦值給[AudioTrackInterceptor.originalTrack],
* [AudioTrackInterceptor]只是一個殼,具體實現是[AudioTrackInterceptor.originalTrack]
*
* @param samplesReadyCallback 回調接口 ,原始pcm數據
*/
fun JavaAudioDeviceModule.setAudioTrackSamplesReadyCallback(samplesReadyCallback: JavaAudioDeviceModule.SamplesReadyCallback) {
val deviceModuleClass = this::class.java
val audioOutputField = deviceModuleClass.getDeclaredField("audioOutput")
audioOutputField.isAccessible = true
val webRtcAudioTrack = audioOutputField.get(this) as WebRtcAudioTrack
val audioTrackClass = webRtcAudioTrack::class.java
val audioTrackFiled = audioTrackClass.getDeclaredField("audioTrack")
audioTrackFiled.isAccessible = true
val audioTrack = audioTrackFiled.get(webRtcAudioTrack)?.let {
it as AudioTrack
} ?: return
val interceptor = AudioTrackInterceptor(audioTrack, samplesReadyCallback)
audioTrackFiled.set(webRtcAudioTrack, interceptor)
}
流程就是先拿到JavaAudioDeviceModule中的audioOutput,即WebRtcAudioTrack,然后再從WebRtcAudioTrack讀取audioTrack,當作參數傳入自定義用於替換的類,然后再將自定義的對象傳給WebRtcAudioTrack中audioTrack用於替換。
要注意的是這個反射的方法中判斷了WebRtcAudioTrack。audioTrack是否為null,關於WebRtcAudioTrack中audioTrack初始化的時機,讀取源碼可以看到audioTrack是有native層初始化的。方法在WebRtcAudioTrack#initPlayout(),上面有個注解@CalledByNative。具體調用的時機,暫時先不深究,可以自行跟蹤下WebRTC的日志。這里從別的地方入手。
在JavaAudioDeviceModule發現有一個方法是用來回調AudioTrack狀態的。
JavaAudioDeviceModule.Builder setAudioTrackStateCallback(JavaAudioDeviceModule.AudioTrackStateCallback audioTrackStateCallback) {
}
具體開始狀態調用是在WebRtcAudioTrack.AudioTrackThread#run(),那么在這里進行反射替換,就能保證WebRtcAudioTrack.audioTrack不為空。
private lateinit var audioDeviceModule: JavaAudioDeviceModule
fun init(applicationContext: Context) {
...
audioDeviceModule = JavaAudioDeviceModule.builder(applicationContext)
.setSamplesReadyCallback {
//音頻輸入數據,麥克風數據,原始pcm數據,可以直接錄制成pcm文件,再轉成mp3
val audioFormat = it.audioFormat
val channelCount = it.channelCount
val sampleRate = it.sampleRate
//pcm格式數據
val data = it.data
}
.setAudioTrackStateCallback(object : JavaAudioDeviceModule.AudioTrackStateCallback {
override fun onWebRtcAudioTrackStart() {
audioDeviceModule.setAudioTrackSamplesReadyCallback {
//音頻輸出數據,通話時對方數據,原始pcm數據,可以直接錄制成pcm文件,再轉成mp3
val audioFormat = it.audioFormat
val channelCount = it.channelCount
val sampleRate = it.sampleRate
//pcm格式數據
val data = it.data
}
//如果使用Java
// JavaAudioDeviceModuleExtKt.setAudioTrackSamplesReadyCallback(
// audioDeviceModule,
// audioSamples -> {
// //音頻輸出數據,通話時對方數據,原始pcm數據,可以直接錄制成pcm文件,再轉成mp3
// int audioFormat = audioSamples.getAudioFormat();
// int channelCount = audioSamples.getChannelCount();
// int sampleRate = audioSamples.getSampleRate();
// //pcm格式數據
// byte[] data = audioSamples.getData ();
// });
}
override fun onWebRtcAudioTrackStop() {
}
})
.createAudioDeviceModule()
...
}
至此,實現流程基本結束,如有錯誤或其它更好的方法,歡迎指正。
接口返回的是pcm原始數據,若要播放需要轉成mp3或其他格式,可以使用RxFFmpeg將pcm文件轉成mp3文件。
