|
|
|
|
@ -2,7 +2,6 @@ package com.utun.chat.ui.screens
|
|
|
|
|
|
|
|
|
|
import android.app.Activity |
|
|
|
|
import android.content.ContextWrapper |
|
|
|
|
import android.content.pm.ActivityInfo |
|
|
|
|
import android.graphics.SurfaceTexture |
|
|
|
|
import android.view.Surface |
|
|
|
|
import android.view.TextureView |
|
|
|
|
@ -42,14 +41,13 @@ import com.utun.chat.data.LogManager
|
|
|
|
|
import com.utun.chat.data.StreamPlayerEngine |
|
|
|
|
import kotlinx.coroutines.delay |
|
|
|
|
|
|
|
|
|
/** Полноэкранный viewer: геометрия видео не зависит от видимости управления. */ |
|
|
|
|
/** Полноэкранный viewer: видео разворачивается вдоль окна, ориентацию экрана не меняем. */ |
|
|
|
|
@Composable |
|
|
|
|
internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: String, name: String, onClose: () -> Unit) { |
|
|
|
|
val context = LocalContext.current |
|
|
|
|
val activity = remember(context) { |
|
|
|
|
generateSequence(context) { (it as? ContextWrapper)?.baseContext }.filterIsInstance<Activity>().firstOrNull() |
|
|
|
|
} |
|
|
|
|
val previousOrientation = remember(activity) { activity?.requestedOrientation } |
|
|
|
|
val owner = LocalLifecycleOwner.current |
|
|
|
|
var visible by remember(owner) { mutableStateOf(owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) } |
|
|
|
|
var aspect by remember { mutableStateOf(0f) } |
|
|
|
|
@ -75,25 +73,13 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
|
|
|
|
|
val behavior = controller?.systemBarsBehavior |
|
|
|
|
controller?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE |
|
|
|
|
controller?.hide(WindowInsetsCompat.Type.systemBars()) |
|
|
|
|
LogManager.addLog("INFO", "stream", "viewer fullscreen source=$source epoch=$epoch orientation=$previousOrientation") |
|
|
|
|
LogManager.addLog("INFO", "stream", "viewer fullscreen source=$source epoch=$epoch " + |
|
|
|
|
"screenOrientation=${activity?.requestedOrientation}") |
|
|
|
|
onDispose { |
|
|
|
|
if (previousOrientation != null) activity?.requestedOrientation = previousOrientation |
|
|
|
|
if (behavior != null) controller?.systemBarsBehavior = behavior |
|
|
|
|
if (statusVisible) controller?.show(WindowInsetsCompat.Type.statusBars()) else controller?.hide(WindowInsetsCompat.Type.statusBars()) |
|
|
|
|
if (navigationVisible) controller?.show(WindowInsetsCompat.Type.navigationBars()) else controller?.hide(WindowInsetsCompat.Type.navigationBars()) |
|
|
|
|
LogManager.addLog("INFO", "stream", "viewer window restored source=$source orientation=$previousOrientation") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
LaunchedEffect(activity, aspect) { |
|
|
|
|
// Формат уже учитывает rotation источника. До первого KEY ориентацию не угадываем. |
|
|
|
|
val orientation = when { |
|
|
|
|
aspect > 1f -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE |
|
|
|
|
aspect > 0f && aspect < 1f -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT |
|
|
|
|
else -> null |
|
|
|
|
} |
|
|
|
|
if (activity != null && orientation != null && activity.requestedOrientation != orientation) { |
|
|
|
|
activity.requestedOrientation = orientation |
|
|
|
|
LogManager.addLog("INFO", "stream", "viewer orientation source=$source aspect=$aspect requested=$orientation") |
|
|
|
|
LogManager.addLog("INFO", "stream", "viewer window restored source=$source") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
LaunchedEffect(status) { |
|
|
|
|
@ -114,14 +100,21 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
|
|
|
|
|
|
|
|
|
|
BoxWithConstraints(Modifier.fillMaxSize().background(Color.Black).clipToBounds()) { |
|
|
|
|
val ratio = aspect.takeIf { it > 0f } ?: 16f / 9f |
|
|
|
|
val width = minOf(maxWidth, maxHeight * ratio) |
|
|
|
|
val turn = aspect > 0f && minOf(maxHeight, maxWidth * ratio) > minOf(maxWidth, maxHeight * ratio) |
|
|
|
|
val width = if (turn) minOf(maxHeight, maxWidth * ratio) else minOf(maxWidth, maxHeight * ratio) |
|
|
|
|
val height = width / ratio |
|
|
|
|
val density = LocalDensity.current |
|
|
|
|
val videoSize = with(density) { Size(width.toPx(), height.toPx()) } |
|
|
|
|
// Жесты работают в координатах окна, поэтому учитывают размеры после поворота видео. |
|
|
|
|
val videoSize = with(density) { if (turn) Size(height.toPx(), width.toPx()) else Size(width.toPx(), height.toPx()) } |
|
|
|
|
val viewport = with(density) { Size(maxWidth.toPx(), maxHeight.toPx()) } |
|
|
|
|
LaunchedEffect(aspect, turn, videoSize, viewport) { |
|
|
|
|
LogManager.addLog("INFO", "stream", "viewer fit source=$source aspect=$aspect viewport=$viewport " + |
|
|
|
|
"video=$videoSize videoRotation=${if (turn) 90 else 0}") |
|
|
|
|
} |
|
|
|
|
LaunchedEffect(videoSize, viewport) { zoom.constrain(videoSize, viewport) } |
|
|
|
|
if (visible) StreamViewerPlayback(group, source, epoch, { aspect = it }, { status = it }, |
|
|
|
|
Modifier.size(width, height).align(Alignment.Center).graphicsLayer { |
|
|
|
|
Modifier.requiredSize(width, height).align(Alignment.Center).graphicsLayer { |
|
|
|
|
rotationZ = if (turn) 90f else 0f |
|
|
|
|
scaleX = zoom.scale; scaleY = zoom.scale |
|
|
|
|
translationX = zoom.offset.x; translationY = zoom.offset.y |
|
|
|
|
}) |
|
|
|
|
@ -136,7 +129,7 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
|
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
if (controls) { |
|
|
|
|
Row(Modifier.fillMaxWidth().align(Alignment.TopCenter).background(Color.Black.copy(alpha = 0.65f)) |
|
|
|
|
Row(Modifier.fillMaxWidth().align(Alignment.TopCenter).background(Color.Black.copy(alpha = 0.325f)) |
|
|
|
|
.windowInsetsPadding(WindowInsets.safeDrawing).padding(start = 16.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), |
|
|
|
|
verticalAlignment = Alignment.CenterVertically) { |
|
|
|
|
Column(Modifier.weight(1f)) { |
|
|
|
|
|