Use shared audio controls in radio and stream publication
This commit is contained in:
1 parent
33ccba1f3f
commit
5d10a16baa
3 files changed
+5
-141
No files matched your search
@@ -22,10 +22,8 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AutoMode
|
||||
import androidx.compose.material.icons.filled.Equalizer
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.HeadsetMic
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.People
|
||||
@@ -56,7 +54,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.utun.chat.ChatApplication
|
||||
import com.utun.chat.data.AudioRoute
|
||||
import com.utun.chat.data.Channel
|
||||
import com.utun.chat.data.Message
|
||||
import com.utun.chat.data.RadioAudioEngine
|
||||
@@ -70,6 +67,7 @@ import com.utun.chat.ui.components.PortableRadioIcon
|
||||
import com.utun.chat.ui.components.GroupAvatar
|
||||
import com.utun.chat.ui.components.GroupPropertiesDialog
|
||||
import com.utun.chat.ui.components.OwnStreamButton
|
||||
import com.utun.chat.ui.components.MediaAudioDeviceButton
|
||||
import com.utun.chat.data.ConversationTarget
|
||||
import com.utun.chat.viewmodel.ChatViewModel
|
||||
import com.utun.chat.data.StreamSource
|
||||
@@ -369,11 +367,6 @@ private fun RadioPttButton(
|
||||
/* Единый выбор входа/выхода, PTT громкостью, VAD и плавающая PTT-кнопка. */
|
||||
@Composable
|
||||
private fun RadioSourceBar(viewModel: ChatViewModel) {
|
||||
val radioAuto by viewModel.radioAuto.collectAsState()
|
||||
val radioInputSource by viewModel.radioInputSource.collectAsState()
|
||||
val wiredInputAvailable by viewModel.wiredInputAvailable.collectAsState()
|
||||
val btInputAvailable by viewModel.btInputAvailable.collectAsState()
|
||||
val radioRoute by viewModel.radioRoute.collectAsState()
|
||||
val provider = ChatApplication.instance.configProvider
|
||||
val scope = rememberCoroutineScope()
|
||||
val overlayEnabled by provider.radioOverlayEnabled.collectAsState(initial = provider.getRadioOverlay())
|
||||
@@ -394,28 +387,7 @@ private fun RadioSourceBar(viewModel: ChatViewModel) {
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val currentDeviceIcon = if (radioRoute == AudioRoute.HEADSET) Icons.Default.HeadsetMic else Icons.AutoMirrored.Filled.VolumeUp
|
||||
val routeName = stringResource(if (radioRoute == AudioRoute.HEADSET) R.string.radio_headset else R.string.radio_speaker)
|
||||
RadioControlButton(Icons.Default.AutoMode, stringResource(R.string.radio_auto_route, routeName),
|
||||
radioAuto, centerIcon = currentDeviceIcon) {
|
||||
viewModel.setRadioAuto()
|
||||
}
|
||||
RadioControlButton(Icons.AutoMirrored.Filled.VolumeUp, stringResource(R.string.radio_phone_audio),
|
||||
!radioAuto && radioInputSource == RadioAudioEngine.INPUT_BUILTIN && radioRoute == AudioRoute.SPEAKER) {
|
||||
viewModel.setRadioInputSource(RadioAudioEngine.INPUT_BUILTIN)
|
||||
}
|
||||
if (wiredInputAvailable) {
|
||||
RadioControlButton(Icons.Default.HeadsetMic, stringResource(R.string.radio_wired_audio),
|
||||
!radioAuto && radioInputSource == RadioAudioEngine.INPUT_WIRED && radioRoute == AudioRoute.HEADSET) {
|
||||
viewModel.setRadioInputSource(RadioAudioEngine.INPUT_WIRED)
|
||||
}
|
||||
}
|
||||
if (btInputAvailable) {
|
||||
RadioControlButton(Icons.Default.HeadsetMic, stringResource(R.string.radio_bluetooth_audio),
|
||||
!radioAuto && radioInputSource == RadioAudioEngine.INPUT_BT && radioRoute == AudioRoute.HEADSET, bluetooth = true) {
|
||||
viewModel.setRadioInputSource(RadioAudioEngine.INPUT_BT)
|
||||
}
|
||||
}
|
||||
MediaAudioDeviceButton(tint = Color.White)
|
||||
}
|
||||
VerticalDivider(modifier = Modifier.height(32.dp), color = Color(0xFF344550))
|
||||
RadioControlButton(Icons.AutoMirrored.Filled.VolumeUp, stringResource(R.string.volume_ptt),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.utun.chat.ui.screens
|
||||
|
||||
import com.utun.chat.ui.components.MediaAudioDeviceButton
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
@@ -264,6 +266,7 @@ private fun StreamCaptureToolbar(cameras: List<StreamCamera>, config: StreamCapt
|
||||
stringResource(R.string.stream_bitrate_auto_value, bitrateValue) else stringResource(R.string.stream_bitrate, bitrateValue)
|
||||
Row(modifier.horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
MediaAudioDeviceButton()
|
||||
CaptureControlButton(stringResource(if (audio) R.string.stream_audio_on else R.string.stream_audio_off),
|
||||
enabled = audioEnabled, checked = audio, onClick = { onAudioChange(!audio) }) {
|
||||
Icon(if (audio) Icons.AutoMirrored.Filled.VolumeUp else Icons.AutoMirrored.Filled.VolumeOff,
|
||||
|
||||
@@ -174,24 +174,6 @@ class ChatViewModel : ViewModel() {
|
||||
private val _radioTalkState = MutableStateFlow(RadioTalkState())
|
||||
val radioTalkState: StateFlow<RadioTalkState> = _radioTalkState
|
||||
|
||||
private val _radioRoute = MutableStateFlow(AudioRoute.SPEAKER)
|
||||
val radioRoute: StateFlow<AudioRoute> = _radioRoute
|
||||
|
||||
private val _radioHeadsetAvailable = MutableStateFlow(false)
|
||||
val radioHeadsetAvailable: StateFlow<Boolean> = _radioHeadsetAvailable
|
||||
|
||||
private val _radioInputSource = MutableStateFlow(RadioAudioEngine.INPUT_AUTO)
|
||||
val radioInputSource: StateFlow<Int> = _radioInputSource
|
||||
|
||||
private val _wiredInputAvailable = MutableStateFlow(false)
|
||||
val wiredInputAvailable: StateFlow<Boolean> = _wiredInputAvailable
|
||||
|
||||
private val _btInputAvailable = MutableStateFlow(false)
|
||||
val btInputAvailable: StateFlow<Boolean> = _btInputAvailable
|
||||
|
||||
private val _radioAuto = MutableStateFlow(false)
|
||||
val radioAuto: StateFlow<Boolean> = _radioAuto
|
||||
|
||||
private var radioToggleCount = 0
|
||||
|
||||
/* рация: активный канал (null = выключена). Одна рация — один канал. */
|
||||
@@ -363,22 +345,12 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
init {
|
||||
radioAudio.router.onChanged = { route, headset ->
|
||||
_radioRoute.value = route
|
||||
_radioHeadsetAvailable.value = headset
|
||||
}
|
||||
radioAudio.router.onInputDevicesChanged = { wired, bt ->
|
||||
_wiredInputAvailable.value = wired
|
||||
_btInputAvailable.value = bt
|
||||
}
|
||||
viewModelScope.launch {
|
||||
radioAudio.activeGroup.collect { group ->
|
||||
if (group == null) clearRadioControls()
|
||||
else {
|
||||
_radioActive.value = radioChannelId == _currentChannel.value?.id
|
||||
_radioTalkState.value = RadioTalkState(group)
|
||||
_radioInputSource.value = radioAudio.inputSource()
|
||||
_radioAuto.value = _radioInputSource.value == RadioAudioEngine.INPUT_AUTO
|
||||
}
|
||||
syncRadioOverlay()
|
||||
}
|
||||
@@ -1064,23 +1036,11 @@ class ChatViewModel : ViewModel() {
|
||||
if (!ready && radioChannelId == ch.id && !radioAudio.isActive()) turnRadioOff()
|
||||
onReady?.invoke(ready)
|
||||
}
|
||||
_radioRoute.value = radioAudio.route()
|
||||
_radioHeadsetAvailable.value = radioAudio.headsetAvailable()
|
||||
_wiredInputAvailable.value = radioAudio.router.wiredInputAvailable
|
||||
_btInputAvailable.value = radioAudio.router.btInputAvailable
|
||||
_radioInputSource.value = radioAudio.inputSource()
|
||||
_radioAuto.value = _radioInputSource.value == RadioAudioEngine.INPUT_AUTO
|
||||
} else {
|
||||
NativeLib.radioSetActive(groupId, false)
|
||||
radioAudio.stop()
|
||||
_radioTalking.value = null
|
||||
_radioTalkState.value = RadioTalkState()
|
||||
_radioRoute.value = AudioRoute.SPEAKER
|
||||
_radioHeadsetAvailable.value = false
|
||||
_wiredInputAvailable.value = false
|
||||
_btInputAvailable.value = false
|
||||
_radioInputSource.value = RadioAudioEngine.INPUT_AUTO
|
||||
_radioAuto.value = false
|
||||
}
|
||||
_radioActive.value = next
|
||||
if (next) RadioOverlayService.requestPermissions() else syncRadioOverlay()
|
||||
@@ -1113,71 +1073,6 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Ручной выбор выхода рации (динамик/гарнитура) — синхронизирует вход. */
|
||||
fun setRadioRoute(r: AudioRoute) {
|
||||
if (!_radioActive.value) return
|
||||
_radioAuto.value = false
|
||||
radioAudio.setRoute(r)
|
||||
_radioRoute.value = radioAudio.route()
|
||||
val linkedInput = when (r) {
|
||||
AudioRoute.SPEAKER -> RadioAudioEngine.INPUT_BUILTIN
|
||||
AudioRoute.HEADSET -> when {
|
||||
_wiredInputAvailable.value -> RadioAudioEngine.INPUT_WIRED
|
||||
_btInputAvailable.value -> RadioAudioEngine.INPUT_BT
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (linkedInput != null && linkedInput != _radioInputSource.value) {
|
||||
_radioInputSource.value = linkedInput
|
||||
viewModelScope.launch {
|
||||
ChatApplication.instance.configProvider.setRadioInputSource(linkedInput)
|
||||
radioAudio.reconfigure()
|
||||
}
|
||||
}
|
||||
LogManager.addLog("INFO", "VM", "setRadioRoute -> ${radioAudio.route()}")
|
||||
}
|
||||
|
||||
/** Ручной выбор входного устройства рации — синхронизирует выход. */
|
||||
fun setRadioInputSource(src: Int) {
|
||||
if (!_radioActive.value) return
|
||||
val clamped = src.coerceIn(RadioAudioEngine.INPUT_AUTO, RadioAudioEngine.INPUT_BT)
|
||||
if (clamped == _radioInputSource.value) return
|
||||
_radioAuto.value = false
|
||||
_radioInputSource.value = clamped
|
||||
val linkedRoute = when (clamped) {
|
||||
RadioAudioEngine.INPUT_BUILTIN -> AudioRoute.SPEAKER
|
||||
RadioAudioEngine.INPUT_WIRED, RadioAudioEngine.INPUT_BT -> AudioRoute.HEADSET
|
||||
else -> null
|
||||
}
|
||||
if (linkedRoute != null) {
|
||||
radioAudio.setRoute(linkedRoute, preferWired = clamped == RadioAudioEngine.INPUT_WIRED)
|
||||
_radioRoute.value = radioAudio.route()
|
||||
}
|
||||
viewModelScope.launch {
|
||||
ChatApplication.instance.configProvider.setRadioInputSource(clamped)
|
||||
radioAudio.reconfigure()
|
||||
}
|
||||
LogManager.addLog("INFO", "VM", "setRadioInputSource -> $clamped")
|
||||
}
|
||||
|
||||
/** Авто: выход следует за гарнитурой, вход — автовыбор. */
|
||||
fun setRadioAuto() {
|
||||
if (!_radioActive.value) return
|
||||
_radioAuto.value = true
|
||||
radioAudio.router.setAutoHeadset(true)
|
||||
radioAudio.router.reapply()
|
||||
_radioRoute.value = radioAudio.route()
|
||||
if (_radioInputSource.value != RadioAudioEngine.INPUT_AUTO) {
|
||||
_radioInputSource.value = RadioAudioEngine.INPUT_AUTO
|
||||
viewModelScope.launch {
|
||||
ChatApplication.instance.configProvider.setRadioInputSource(RadioAudioEngine.INPUT_AUTO)
|
||||
radioAudio.reconfigure()
|
||||
}
|
||||
}
|
||||
LogManager.addLog("INFO", "VM", "setRadioAuto -> route=${radioAudio.route()}")
|
||||
}
|
||||
|
||||
/* Полностью выключить рацию (используется при удалении активного канала). */
|
||||
private fun turnRadioOff() {
|
||||
val chId = radioChannelId ?: return
|
||||
@@ -1191,12 +1086,6 @@ class ChatViewModel : ViewModel() {
|
||||
_radioActive.value = false
|
||||
_radioTalking.value = null
|
||||
_radioTalkState.value = RadioTalkState()
|
||||
_radioRoute.value = AudioRoute.SPEAKER
|
||||
_radioHeadsetAvailable.value = false
|
||||
_wiredInputAvailable.value = false
|
||||
_btInputAvailable.value = false
|
||||
_radioInputSource.value = RadioAudioEngine.INPUT_AUTO
|
||||
_radioAuto.value = false
|
||||
}
|
||||
|
||||
private fun syncRadioOverlay() {
|
||||
|
||||
Reference in new issue
Block a user