Expose shared audio devices in stream viewing and document them

This commit is contained in:
evgeny committed 2026-10-22 15:07:26 +02:00
1 parent 5d10a16baa
commit 44a3b33c3a
3 files changed
+28 -14

No files matched your search

+15 -11
View File
@@ -811,7 +811,8 @@ Android:
Разделённые CODEC_CONFIG buffers собираются отдельно с пределом 64KiB до разбора конфигурации.
- Camera MediaCodec Surface timestamps и AudioTimestamp.TIMEBASE_MONOTONIC приводятся к общему
origin System.nanoTime; время callback/получения блока не заменяет capture PTS.
- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo. Перед Opus AUDIO/128kbps
- Встроенный микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo (mono fallback при отказе формата).
Гарнитуры используют разговорный тракт; mono приводится к stereo перед Opus AUDIO/128kbps. Перед кодированием
применяется тот же audio_compressor, что в TX рации: общие настройки включения, max_gain и rise_rate,
lookback=200ms, lookahead=0, release=rise*5, target=0.25. История и gain принадлежат только audio worker
трансляции; оба канала используют общий gain, сохраняющий стереобаланс. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона,
@@ -934,17 +935,20 @@ Android:
Это ограниченный drain, не подтверждение воспроизведения всеми viewers. Abort/core restart очищает очередь сразу.
Завершение логирует срок drain, оставшиеся RS-байты выдач и локальных readers; RS=0 не означает подтверждение доставки ETCP.
Поздние команды старого session ID не меняют новый запуск.
- Рация работает одновременно с публикацией. При совпадении выбранного входа она получает копию
PCM CAMCORDER 48kHz stereo с исходными timestamp; её source/stereo предпочтения не меняют трансляцию.
Другой явно выбранный вход открывается отдельно; отказ не заменяет его системным default.
Трансляция имеет приоритет глобального AudioManager: рация не включает разговорный режим и не меняет SCO
общего тракта. Перед запуском трансляции ранее работавшая рация освобождает capture и затем подключается
к фактическому входу трансляции либо повторно открывает свой отдельный вход.
Очереди потребителей ограничены; PTT упорядочен с PCM, разрыв сбрасывает историю детектора рации.
Последняя подписка закрывает AudioRecord; stop рации/трансляции не закрывает захват другого потребителя.
Проверяется фактический Android route и системное silencing: конфликтующий отдельный capture рации
завершается с диагностикой, приём рации сохраняется. Окно VAD/экран/разрешения не обходятся общим захватом.
- Рация, публикация и просмотр трансляций используют общий профиль MediaAudioController: AUTO/PHONE/WIRED/BLUETOOTH.
Выбор из любого экрана меняет общий вход и маршруты всех активных AudioTrack; настройка сохраняется одна.
Аппаратный микрофон один, очереди потребителей и обработка PCM независимы. PTT упорядочен с PCM.
Подключение публикации переводит встроенный вход в CAMCORDER stereo; профиль сохраняется до последней подписки.
Смена устройства сохраняет подписки, камеру, видеокодер, source_epoch и временную шкалу эфира.
MediaCaptureTimeline выдаёт 20ms PCM в общей сетке с запасом 200ms; аппаратные timestamps задают положение звука.
Отсутствующий PCM заменяется тишиной. Запоздавшие данные отбрасываются, а не получают новые PTS.
Подтверждаются фактический маршрут, свежий PCM и отсутствие системного silencing. При ошибке — диагностика
и резервный телефонный профиль; при полном отказе входа публикация продолжает видео с тишиной.
Последний потребитель закрывает вход; выходы живут до удаления своих AudioTrack. Просмотр не открывает микрофон.
Окно VAD/экран/разрешения не обходятся общим захватом. Ошибки видны в общем меню выбора устройства.
Звонок и voice recording по-прежнему исключают публикацию в обе стороны.
Аппаратная проверка (debug APK, RECORD_AUDIO разрешён):
adb shell am instrument -w com.utun.chat/com.utun.chat.data.MediaAudioProbe
Звук трансляции и рация (Android и desktop):
- Во время публикации с микрофоном звук можно выключить и включить. Заглушается только копия PCM
@@ -1,5 +1,7 @@
package com.utun.chat.ui.screens
import com.utun.chat.ui.components.MediaAudioDeviceButton
import android.os.SystemClock
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
@@ -110,7 +112,8 @@ internal fun StreamGridScreen(group: Long, title: String, sources: List<StreamSo
Scaffold(modifier = if (ownPreviewVisible) Modifier else Modifier.clearAndSetSemantics {}, topBar = {
if (focused == null)
TopAppBar(title = { Text(stringResource(R.string.stream_all_title, title), maxLines = 1, overflow = TextOverflow.Ellipsis) },
navigationIcon = { IconButton(onClick = onClose) { Icon(Icons.Filled.Close, stringResource(R.string.stream_close_view)) } })
navigationIcon = { IconButton(onClick = onClose) { Icon(Icons.Filled.Close, stringResource(R.string.stream_close_view)) } },
actions = { MediaAudioDeviceButton() })
}, contentWindowInsets = if (focused == null) ScaffoldDefaults.contentWindowInsets else WindowInsets(0, 0, 0, 0)) { padding ->
Column(Modifier.fillMaxSize().padding(padding).background(Color.Black)) {
if (unavailable && focused == null)
@@ -185,6 +188,9 @@ private fun StreamGridTile(group: Long, tile: GridStream, visible: Boolean, expa
role = Role.Button, onClickLabel = stringResource(if (tile.source.self) R.string.stream_manage
else if (expanded) R.string.collapse else R.string.expand), onClick = onClick)
.semantics { contentDescription = tile.source.displayName })
if (expanded) Box(Modifier.align(Alignment.TopEnd).windowInsetsPadding(WindowInsets.safeDrawing)) {
MediaAudioDeviceButton(tint = Color.White)
}
}
}
@@ -1,5 +1,7 @@
package com.utun.chat.ui.screens
import com.utun.chat.ui.components.MediaAudioDeviceButton
import android.app.Activity
import android.content.ContextWrapper
import android.graphics.SurfaceTexture
@@ -68,8 +70,9 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
onClose()
}
}
LaunchedEffect(controls, status, visible, hideTick) {
if (controls && status == R.string.stream_live && visible) { delay(3000); controls = false }
var audioMenu by remember { mutableStateOf(false) }
LaunchedEffect(controls, status, visible, hideTick, audioMenu) {
if (controls && status == R.string.stream_live && visible && !audioMenu) { delay(3000); controls = false }
}
LaunchedEffect(controls) { LogManager.addLog("DEBUG", "stream", "viewer controls source=$source visible=$controls") }
LaunchedEffect(zoom.scale, zoom.offset) {
@@ -116,6 +119,7 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
Text(name, color = Color.White, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(stringResource(status), color = Color.White, style = MaterialTheme.typography.bodySmall)
}
MediaAudioDeviceButton(tint = Color.White, onExpandedChange = { audioMenu = it })
IconButton(onClick = onClose) { Icon(Icons.Filled.Close, stringResource(R.string.stream_close_view), tint = Color.White) }
}
}