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。做这类需求的应该能看明白。

相关推荐
峥嵘life4 小时前
Android 蓝牙设备连接广播详解-2026
android·python·学习
MusingByte7 小时前
别再裸用 Claude Code 了!安卓开发者必装 13 个官方推荐插件,效率翻 3 倍省 70% token
android
_李小白7 小时前
【android opencv学习笔记】Day 29: 滤波算法之Sobel 边缘检测
android·opencv·学习
Dxy12393102168 小时前
Python 操作 MySQL 事务:从入门到避坑
android·python·mysql
峥嵘life9 小时前
Android getprop 属性限制详解:User 版本属性获取问题分析
android·开发语言·python·学习
一航jason10 小时前
Speed Tools:一套低侵入的 Android 插件化 + 动态换肤 + 字体切换框架
android·插件化·组件化·换肤
李斯维11 小时前
Jetpack 可观察数据容器 LiveData 的入门与基础使用
android·android jetpack
问心无愧051312 小时前
ctf show web入门261
android·前端·笔记
alexhilton12 小时前
车载系统中的可扩展UI:从UI嵌入到系统窗口编排
android·kotlin·android jetpack
Cloud_Shy61812 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第一章 Item 4 - 6)
android·数据库·论文阅读·python