Android AWS KVS WebRTC 通话声道切换到媒体音乐声道

在基于AWS KVS开发音视频建立双向直播流的过程中碰到一个问题,APP端不需要发送手机端的音视频,只需要接收设备端的音视频,但是在APP中播放远端音频(Remote Audio Track)的时候,音频一直占用通话声道,而不是音乐通道,那如何解决这个问题?

有二种方式,第一种方式是修改源码,第二种方式则是不使用默认的JavaAudioDeviceModule自定义创建。这里主要讲第二种方式。

一、创建自定义JavaAudioDeviceModule

主要是修改audioAttributes, 代码如下:

kotlin 复制代码
import android.content.Context
import android.media.*
import android.util.Log
import org.webrtc.audio.JavaAudioDeviceModule

/**
 * @description 用于将 WebRTC 的音频输出切换到媒体声道(STREAM_MUSIC),而不是默认的通话声道(STREAM_VOICE_CALL)
 */
class MediaAudioManager(private val context: Context) {
    
    /**
     * 创建 JavaAudioDeviceModule,并强制使用媒体通道输出
     */
    fun createJavaAudioDeviceModule(): JavaAudioDeviceModule {
        val audioAttributes = AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            .build()

        val audioRecordErrorCallback = object : JavaAudioDeviceModule.AudioRecordErrorCallback {
            override fun onWebRtcAudioRecordInitError(errorMessage: String?) {
                Log.e("MediaAudioManager", "onWebRtcAudioRecordInitError: $errorMessage")
            }

            override fun onWebRtcAudioRecordStartError(
                errorCode: JavaAudioDeviceModule.AudioRecordStartErrorCode?,
                errorMessage: String?
            ) {
                Log.e("MediaAudioManager", "onWebRtcAudioRecordStartError: $errorMessage")
            }

            override fun onWebRtcAudioRecordError(errorMessage: String?) {
                Log.e("MediaAudioManager", "onWebRtcAudioRecordError: $errorMessage")
            }
        }

        val audioTrackErrorCallback = object : JavaAudioDeviceModule.AudioTrackErrorCallback {
            override fun onWebRtcAudioTrackInitError(errorMessage: String?) {
                Log.e("MediaAudioManager", "onWebRtcAudioTrackInitError: $errorMessage")
            }

            override fun onWebRtcAudioTrackStartError(
                errorCode: JavaAudioDeviceModule.AudioTrackStartErrorCode?,
                errorMessage: String?
            ) {
                Log.e("MediaAudioManager", "onWebRtcAudioTrackStartError: $errorMessage")
            }

            override fun onWebRtcAudioTrackError(errorMessage: String?) {
                Log.e("MediaAudioManager", "onWebRtcAudioTrackError: $errorMessage")
            }
        }

        // 创建 JavaAudioDeviceModule 并使用 MEDIA 声道
        return JavaAudioDeviceModule.builder(context)
            .setAudioAttributes(audioAttributes)
            .setUseHardwareAcousticEchoCanceler(true)
            .setUseHardwareNoiseSuppressor(true)
            .setAudioRecordErrorCallback(audioRecordErrorCallback)
            .setAudioTrackErrorCallback(audioTrackErrorCallback)
            .createAudioDeviceModule()
    }
}

在构建PeerConnectionFactory时使用:

kotlin 复制代码
PeerConnectionFactory.builder().apply {
    //解决声音播放占用通话通道的问题
    val mediaAudioManager = MediaAudioManager(mContext)
    val audioModule = mediaAudioManager.createJavaAudioDeviceModule()
    setAudioDeviceModule(audioModule)
    
    ...其他配置略...
    
  }.createPeerConnectionFactory()

二、声音通道的控制

一般情况下都会有一个按钮控制静音,静音或开启的控制逻辑需要区分用户是否佩戴耳机,佩戴耳机的情况下不能开启扬声器避免暴露隐私,具体代码如下:

kotlin 复制代码
/**
 * 开启或关闭声音
 */
fun enableAudio(enable: Boolean) {
    if (enable) {
        //没有佩戴耳机
        if (!isHeadphonesPlugged()) {
            audioManager?.mode = AudioManager.MODE_NORMAL
            setSpeakerphoneOn(true)
        } else {
            audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
            setSpeakerphoneOn(false)
        }
    } else {
        originalAudioMode?.let { audioManager?.mode = it }
        originalSpeakerphoneOn?.let { audioManager?.isSpeakerphoneOn = it }
    }
    try {
        remoteAudioTrack?.setEnabled(enable)
    } catch (ex: Exception) {
        Log.d(TAG,"enable Audio but remote Audio Track has been disposed")
    }
}

检测是否佩戴有线或蓝牙耳机:

kotlin 复制代码
/**
 * 判断是否佩戴耳机,佩戴耳机音频不外放
 */
private fun isHeadphonesPlugged(): Boolean {
    val audioDevices = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
    if (audioDevices != null) {
        for (deviceInfo in audioDevices) {
            if (deviceInfo.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
                || deviceInfo.type == AudioDeviceInfo.TYPE_WIRED_HEADSET
                || deviceInfo.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
            ) {
                return true
            }
        }
    }
    return false
}

最后就是打开手机扬声器的代码:

kotlin 复制代码
/**
 * 设置扬声器的开启和关闭
 */
@SuppressLint("NewApi")
private fun setSpeakerphoneOn(enabled: Boolean) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {   //Android 12及以上
        val preferredDevice = if (enabled) {
            // 获取扬声器设备
            val audioDevices = audioManager?.availableCommunicationDevices
            audioDevices?.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
        } else {
            // 恢复默认设备
            null
        }
        preferredDevice?.let {
            audioManager?.setCommunicationDevice(it)
        } ?: run {
            audioManager?.clearCommunicationDevice()
        }
    } else {
        // 旧版本兼容方式 (Android 11及以下)
        audioManager?.mode = if (enabled) AudioManager.MODE_NORMAL else AudioManager.MODE_IN_COMMUNICATION
        audioManager?.isSpeakerphoneOn = enabled
    }
}

部分类或对象找不到需要参考AWS KVS的Demo。做这类需求的应该能看明白。

相关推荐
Kapaseker18 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴18 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜2 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker2 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95272 天前
Andorid Google 登录接入文档
android
黄林晴2 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android