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傳送門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM