做过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文件。
Github传送门