Expose shared audio devices in stream viewing and document them
This commit is contained in:
1 parent
5d10a16baa
commit
44a3b33c3a
3 files changed
+28
-14
No files matched your search
+15
-11
@@ -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
|
||||
|
||||
+7
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user