ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-08-22 18:01:48  阅读:306  来源: 互联网

标签:val originalTrack audioTrack 音视频 write 通话录音 WebRtcAudioTrack AudioTrack Android


@

目录

做过WebRTC的音视频通话应该知道WebRTC的sdk只暴露了麦克风输入数据和视频数据,如果要实现音视频录制该怎么办呢?当然可以在通话的各个终端分别进行录制,然后上传服务器进行处理。那如果想在一个设备上进行统一录制呢?通话对方的音频数据该如何获取?

WebRTC是在哪输出音频数据的?

在网上搜索了一圈都说要改源码,WebRTC源码10几个g,还在墙外,编译也有难度,那如何跨过这一步呢?
这一步我们就要去找找源码了。

JavaAudioDeviceModule

在创建PeerConnectionFactory时要传入JavaAudioDeviceModule,即使不传,也会帮我们创建一个默认的。看这个就是用来操作音频相关的。
在这里插入图片描述
gradle下载的源码是没有注释的,可以去网上找找

可以看到AudioRecord作为音频输入,AudioTrack作为音频输出。
因为可以拿到输入的数据,暂时先不管,先去看看AudioTrack。

WebRtcAudioTrack

![在这里插入图片描述](https://www.icode9.com/i/ll/?i=7ca3226291e444a59147c08ac7bc2c63.png?,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NzZG5fc2hlbjAyMjE=,size_16,color_FFFFFF,t_70

查找一圈之后找到了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文件。

Github传送门

标签:val,originalTrack,audioTrack,音视频,write,通话录音,WebRtcAudioTrack,AudioTrack,Android
来源: https://www.cnblogs.com/qq714081644/p/15172979.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有