Browse Source

Enable pinch zoom and bounded panning in Android live viewer

master
evgeny 4 days ago
parent
commit
8eef506eed
  1. 79
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamViewerScreen.kt

79
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamViewerScreen.kt

@ -8,6 +8,7 @@ import android.view.Surface
import android.view.TextureView
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
@ -18,9 +19,13 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@ -50,6 +55,8 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
var aspect by remember { mutableStateOf(0f) }
var status by remember { mutableStateOf(R.string.stream_connecting) }
var controls by remember { mutableStateOf(true) }
var hideTick by remember { mutableStateOf(0) }
val zoom = remember { VideoZoomState() }
DisposableEffect(owner) {
val observer = LifecycleEventObserver { _, event ->
@ -90,16 +97,38 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
}
}
LaunchedEffect(status) { controls = true }
LaunchedEffect(controls, status, visible) {
LaunchedEffect(controls, status, visible, hideTick) {
if (controls && status == R.string.stream_live && visible) { delay(3000); controls = false }
}
LaunchedEffect(controls) { LogManager.addLog("DEBUG", "stream", "viewer controls source=$source visible=$controls") }
LaunchedEffect(zoom.scale, zoom.offset) {
delay(300)
LogManager.addLog("DEBUG", "stream", "viewer zoom source=$source scale=${zoom.scale} offset=${zoom.offset}")
}
Box(Modifier.fillMaxSize().background(Color.Black)) {
if (visible) StreamViewerPlayback(group, source, epoch, { aspect = it }, { status = it })
Box(Modifier.matchParentSize().pointerInput(status) {
detectTapGestures { if (status == R.string.stream_live) controls = !controls else controls = true }
})
BoxWithConstraints(Modifier.fillMaxSize().background(Color.Black).clipToBounds()) {
val ratio = aspect.takeIf { it > 0f } ?: 16f / 9f
val width = minOf(maxWidth, maxHeight * ratio)
val height = width / ratio
val density = LocalDensity.current
val videoSize = with(density) { Size(width.toPx(), height.toPx()) }
val viewport = with(density) { Size(maxWidth.toPx(), maxHeight.toPx()) }
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 {
scaleX = zoom.scale; scaleY = zoom.scale
translationX = zoom.offset.x; translationY = zoom.offset.y
})
Box(Modifier.matchParentSize()
.pointerInput(status) {
detectTapGestures { if (status == R.string.stream_live) controls = !controls else controls = true }
}
.pointerInput(videoSize, viewport) {
detectTransformGestures { centroid, pan, factor, _ ->
controls = true; hideTick++
zoom.transform(centroid, pan, factor, videoSize, viewport)
}
})
if (controls) {
Row(Modifier.fillMaxWidth().align(Alignment.TopCenter).background(Color.Black.copy(alpha = 0.65f))
.windowInsetsPadding(WindowInsets.safeDrawing).padding(start = 16.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
@ -117,33 +146,29 @@ internal fun StreamViewerScreen(group: Long, source: Long, epoch: Long, title: S
/** Смена размеров окна и скрытие controls сохраняют TextureView, decoder и сетевой reader. */
@Composable
private fun StreamViewerPlayback(group: Long, source: Long, epoch: Long, onAspect: (Float) -> Unit, onStatus: (Int) -> Unit) {
private fun StreamViewerPlayback(group: Long, source: Long, epoch: Long, onAspect: (Float) -> Unit, onStatus: (Int) -> Unit,
modifier: Modifier) {
val engine = remember(group, source, epoch) { StreamPlayerEngine(group, source, epoch) }
val aspect by engine.aspect.collectAsState()
val status by engine.status.collectAsState()
var surface by remember { mutableStateOf<Surface?>(null) }
LaunchedEffect(aspect, status) { onAspect(aspect); onStatus(status) }
DisposableEffect(engine) { onDispose { engine.stop() } }
BoxWithConstraints(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
val ratio = aspect.takeIf { it > 0f } ?: 16f / 9f
val width = minOf(maxWidth, maxHeight * ratio)
val height = width / ratio
AndroidView(factory = { context ->
TextureView(context).apply {
keepScreenOn = true
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
surface = Surface(texture); engine.start(surface!!)
}
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {}
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
engine.stop(); surface?.release(); surface = null; return true
}
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
AndroidView(factory = { context ->
TextureView(context).apply {
keepScreenOn = true
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
surface = Surface(texture); engine.start(surface!!)
}
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {}
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
engine.stop(); surface?.release(); surface = null; return true
}
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
}
}, modifier = Modifier.size(width, height), onRelease = { view ->
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
})
}
}
}, modifier = modifier, onRelease = { view ->
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
})
}

Loading…
Cancel
Save