Handle stream capture permission failures and document preview release
This commit is contained in:
1 parent
45d1b50cb4
commit
36af8d077f
2 files changed
+39
-5
No files matched your search
@@ -6,6 +6,7 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.hardware.display.DisplayManager
|
||||
@@ -116,7 +117,8 @@ private fun StreamPublishScreen(group: Long, channelName: String, onBack: () ->
|
||||
var cameras by remember { mutableStateOf<List<StreamCamera>>(emptyList()) }
|
||||
var loadingModes by remember { mutableStateOf(true) }
|
||||
var menu by remember(group) { mutableStateOf<StreamOption?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
var permissionError by remember(group) { mutableStateOf<Int?>(null) }
|
||||
LaunchedEffect(group) {
|
||||
val saved = try { ChatApplication.instance.configProvider.getStreamCaptureSettings() }
|
||||
catch (e: CancellationException) { throw e }
|
||||
catch (e: Exception) {
|
||||
@@ -131,7 +133,22 @@ private fun StreamPublishScreen(group: Long, channelName: String, onBack: () ->
|
||||
loadingModes = false
|
||||
}
|
||||
var permissionVersion by remember { mutableStateOf(0) }
|
||||
val permissions = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissionVersion++ }
|
||||
val permissions = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted ->
|
||||
permissionVersion++
|
||||
val denied = granted.filterValues { !it }.keys
|
||||
permissionError = when {
|
||||
denied.isEmpty() -> null
|
||||
Manifest.permission.CAMERA in denied -> R.string.camera_permission_required
|
||||
else -> R.string.microphone_access_required
|
||||
}
|
||||
LogManager.addLog(if (denied.isEmpty()) "INFO" else "WARN", "stream", "capture permission result group=$group denied=$denied")
|
||||
}
|
||||
val lifecycle = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycle) {
|
||||
val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) permissionVersion++ }
|
||||
lifecycle.lifecycle.addObserver(observer)
|
||||
onDispose { lifecycle.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
val cameraGranted = permissionVersion.let { ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED }
|
||||
val micGranted = permissionVersion.let { ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED }
|
||||
|
||||
@@ -175,9 +192,15 @@ private fun StreamPublishScreen(group: Long, channelName: String, onBack: () ->
|
||||
StreamController.stop()
|
||||
} else if (!cameraGranted || (audio && !micGranted)) {
|
||||
LogManager.addLog("INFO", "stream", "screen permissions group=$group camera=$cameraGranted mic=$micGranted audio=$audio")
|
||||
permissions.launch(listOf(Manifest.permission.CAMERA)
|
||||
.plus(if (audio) listOf(Manifest.permission.RECORD_AUDIO) else emptyList()).toTypedArray())
|
||||
try {
|
||||
permissions.launch(listOf(Manifest.permission.CAMERA)
|
||||
.plus(if (audio) listOf(Manifest.permission.RECORD_AUDIO) else emptyList()).toTypedArray())
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "stream", "Cannot request capture permissions group=$group: $e")
|
||||
permissionError = R.string.stream_failed
|
||||
}
|
||||
} else config?.let {
|
||||
permissionError = null
|
||||
LogManager.addLog("INFO", "stream", "screen start group=$group audio=$audio config=$it")
|
||||
StreamController.start(group, it, audio)
|
||||
}
|
||||
@@ -189,6 +212,7 @@ private fun StreamPublishScreen(group: Long, channelName: String, onBack: () ->
|
||||
})
|
||||
}) { padding ->
|
||||
Column(Modifier.fillMaxSize().padding(padding).padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
permissionError?.let { Text(stringResource(it), color = MaterialTheme.colorScheme.error) }
|
||||
StreamCaptureToolbar(cameras, shownConfig, displayDegrees, Modifier.fillMaxWidth(),
|
||||
menu = menu, onMenuChange = { menu = it },
|
||||
enabled = if (ongoing != null) ongoing.phase == StreamPhase.LIVE && ongoing.pendingConfig == null else !loadingModes,
|
||||
@@ -401,7 +425,10 @@ internal fun StreamPreviewPlayback(localPreview: StreamPreview, displayDegrees:
|
||||
internal fun StreamDisplayDegrees(): Int {
|
||||
val context = LocalContext.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val display = (context as? Activity)?.windowManager?.defaultDisplay
|
||||
val activity = remember(context) {
|
||||
generateSequence(context) { (it as? ContextWrapper)?.baseContext }.filterIsInstance<Activity>().firstOrNull()
|
||||
}
|
||||
val display = activity?.windowManager?.defaultDisplay
|
||||
var degrees by remember(display) { mutableStateOf((display?.rotation ?: 0) * 90) }
|
||||
DisposableEffect(context, display, configuration) {
|
||||
degrees = (display?.rotation ?: 0) * 90
|
||||
|
||||
@@ -429,6 +429,9 @@ Video close ждёт worker до 250 мс и подтверждает release de
|
||||
Ошибки listener detach сохраняют отдельную неактивную регистрацию до SDK ACK, включая failed attach без lease;
|
||||
поздний routing callback не читает released track. Повтор очистки выполняет media control executor.
|
||||
Viewer UI передаёт физический Surface/texture cleanup идемпотентному StreamPreviewTarget.
|
||||
Прямой CameraPreviewView снимает свои leases при onRelease, не ожидая destroy TextureView;
|
||||
отдельная camera lease сохраняет output до ACK сессии. Поздний destroy возвращает false
|
||||
и не закрывает texture средствами framework. Смена имени источника обновляет плитку без смены reader.
|
||||
|
||||
Publisher service сохраняет пассивный capture до start, CPU lock — до acquire, foreground-флаг — до IPC.
|
||||
Capture stop=true подтверждает camera/session callbacks, encoder/input Surface, ориентацию, подписку микрофона и Opus worker.
|
||||
@@ -721,6 +724,10 @@ ACK показа/PTS и EOS drain без смены Surface. OEM/реально
|
||||
StreamPreviewLifecycleTest проверяет реальные preview/leases: потерю/частичный эффект
|
||||
callback, отменённые команды, независимые и конкурентные владельцы, физические
|
||||
отказы release до/после эффекта, retry Handler, detach/visibility и поздние update.
|
||||
Также проверяются onRelease без destroy, позднее появление texture после dispose и destroy прежнего output.
|
||||
QrScannerSessionTest проверяет настоящего владельца CameraX/ML Kit с управляемыми
|
||||
use cases, Task и SDK отказами. Dispose во время распознавания, поздняя camera future,
|
||||
частичный bind, отказ listener и независимый cleanup не требуют камеры для воспроизведения.
|
||||
RadioCallLifecycleTest использует настоящие RadioCallController, IncomingCallManager,
|
||||
CoreActivity и гудки. Управляемые Android/JNI/repository-границы проверяют все
|
||||
команды сигналинга, pending START, валидацию событий/полной identity, активацию,
|
||||
|
||||
Reference in new issue
Block a user