zoukankan      html  css  js  c++  java
  • Android端WebRTC音视频通话录音-获取音频输出数据

    @

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

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

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

    JavaAudioDeviceModule

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

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

    WebRtcAudioTrack

    ![在这里插入图片描述](https://img-blog.csdnimg.cn/7ca3226291e444a59147c08ac7bc2c63.png?x-oss-process=image/watermark,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传送门

  • 相关阅读:
    文件上传工具类
    使用java 的api获取两个集合的交集、并集、差集
    如何判断指定Class是否是List的子类或者父类,是否是数组
    如何判断指定Class是否是基础数据类型或者是其包装类型
    OVS中的key解析
    OVS
    Neutron网络学习
    NIO_2
    以太网帧格式总结
    VMWare中桥接、NAT、Host-only
  • 原文地址:https://www.cnblogs.com/qq714081644/p/15172979.html
Copyright © 2011-2022 走看看