Android端WebRTC音视频通话录音-获取音频输出数据

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android端WebRTC音视频通话录音-获取音频输出数据相关的知识,希望对你有一定的参考价值。

做过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,当作参数传入自定义用于替换的类,然后再将自定义的对象传给WebRtcAudioTrackaudioTrack用于替换。

要注意的是这个反射的方法中判断了WebRtcAudioTrack。audioTrack是否为null,关于WebRtcAudioTrackaudioTrack初始化的时机,读取源码可以看到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文件。

以上是关于Android端WebRTC音视频通话录音-获取音频输出数据的主要内容,如果未能解决你的问题,请参考以下文章

使用webrtc for android,如何在视频通话中保存图片?

浅聊WebRTC视频通话

reSipWebRTC

WebRTC Native M96数据统计-- 使用PeerConnection::GetStats获取WebRTC实时统计信息

如何实现 iOS开发webrtc 视频通话时录像,截屏。

如何构建一个可以进行基本视频通话的简单 Native WebRTC Android 应用程序?