|
|
|
|
@ -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 |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|