Use shared audio controls in radio and stream publication

This commit is contained in:
evgeny committed 2026-10-22 15:07:25 +02:00
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() {