Keep Android stream surfaces alive until decoder release and fit grid geometry
This commit is contained in:
1 parent
75806249bd
commit
14b152bb5a
3 files changed
+80
-14
No files matched your search
@@ -0,0 +1,25 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import kotlin.math.min
|
||||
|
||||
internal data class StreamGridShape(val columns: Int, val rows: Int)
|
||||
|
||||
/** Максимизирует видимую площадь видео; портретный экран не принуждается к квадратной сетке. */
|
||||
internal fun streamGridShape(count: Int, width: Double, height: Double, aspect: Double = 16.0 / 9): StreamGridShape {
|
||||
if (count <= 0 || width <= 0 || height <= 0) return StreamGridShape(1, 1)
|
||||
var best = StreamGridShape(1, count)
|
||||
var bestArea = -1.0
|
||||
var bestEmpty = count
|
||||
for (columns in 1..count) {
|
||||
val rows = (count + columns - 1) / columns
|
||||
val cellWidth = width / columns
|
||||
val cellHeight = height / rows
|
||||
val videoWidth = min(cellWidth, cellHeight * aspect)
|
||||
val area = count * videoWidth * videoWidth / aspect
|
||||
val empty = rows * columns - count
|
||||
if (area > bestArea + 0.001 || (kotlin.math.abs(area - bestArea) <= 0.001 && empty < bestEmpty)) {
|
||||
best = StreamGridShape(columns, rows); bestArea = area; bestEmpty = empty
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
@@ -49,7 +49,12 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
val status: StateFlow<Int> = mutableStatus
|
||||
private val id = StreamController.nextId()
|
||||
@Volatile private var running = false
|
||||
@Volatile var drainTimeoutMs = 2000L
|
||||
private set
|
||||
private var worker: Thread? = null
|
||||
private val releaseLock = Any()
|
||||
private var released = false
|
||||
private val releaseCallbacks = mutableListOf<() -> Unit>()
|
||||
|
||||
fun start(surface: Surface) {
|
||||
if (worker != null) return
|
||||
@@ -62,10 +67,24 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
running = true
|
||||
worker = Thread({
|
||||
try { if (running) play(surface) }
|
||||
finally { main.post { players.remove(this); mutableViewing.value = players.isNotEmpty() } }
|
||||
finally {
|
||||
val callbacks = synchronized(releaseLock) { released = true; releaseCallbacks.toList().also { releaseCallbacks.clear() } }
|
||||
for (callback in callbacks) try { callback() }
|
||||
catch (e: Exception) { LogManager.addLog("WARN", "stream", "viewer surface release id=$id: ${e.message}") }
|
||||
main.post { players.remove(this); mutableViewing.value = players.isNotEmpty() }
|
||||
}
|
||||
}, "stream-player").apply { start() }
|
||||
}
|
||||
fun stop() { running = false; NativeLib.streamStop(id) }
|
||||
/** Surface/SurfaceTexture освобождаются callback только после остановки decoder и native DSP. */
|
||||
fun stop(onReleased: (() -> Unit)? = null) {
|
||||
running = false; NativeLib.streamStop(id)
|
||||
if (onReleased != null) {
|
||||
val immediate = synchronized(releaseLock) {
|
||||
if (worker == null || released) true else { releaseCallbacks.add(onReleased); false }
|
||||
}
|
||||
if (immediate) onReleased()
|
||||
}
|
||||
}
|
||||
|
||||
private fun play(surface: Surface) {
|
||||
var decoder: MediaCodec? = null
|
||||
@@ -189,6 +208,7 @@ class StreamPlayerEngine(private val group: Long, private val source: Long, priv
|
||||
while (running && surface.isValid) {
|
||||
check(NativeLib.isReady()) { "Core stopped" }
|
||||
observeStatus(NativeLib.streamStatus(id))
|
||||
drainTimeoutMs = ((ready - position) / 1000 + 2000).coerceIn(2000, 30000)
|
||||
val deviceLead = playback?.leadUs ?: 0
|
||||
for (work in 0 until 4) {
|
||||
video.step(position, System.nanoTime() / 1000)
|
||||
|
||||
+33
-12
@@ -145,29 +145,50 @@ 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,
|
||||
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) }
|
||||
internal fun StreamViewerPlayback(group: Long, source: Long, epoch: Long, onAspect: (Float) -> Unit, onStatus: (Int) -> Unit,
|
||||
modifier: Modifier, onEngine: (StreamPlayerEngine) -> Unit = {}) {
|
||||
var engine by remember(group, source, epoch) { mutableStateOf<StreamPlayerEngine?>(null) }
|
||||
val aspect = engine?.aspect?.collectAsState()?.value ?: 0f
|
||||
val status = engine?.status?.collectAsState()?.value ?: R.string.stream_connecting
|
||||
val resources = remember(group, source, epoch) { StreamViewerSurface() }
|
||||
fun releaseSurface(texture: SurfaceTexture? = null) {
|
||||
val surface = resources.surface ?: return
|
||||
val currentTexture = resources.texture ?: return
|
||||
if (texture != null && texture !== currentTexture) return
|
||||
resources.surface = null; resources.texture = null
|
||||
val currentEngine = resources.engine
|
||||
resources.engine = null
|
||||
if (currentEngine != null) currentEngine.stop { surface.release(); currentTexture.release() }
|
||||
else { surface.release(); currentTexture.release() }
|
||||
}
|
||||
LaunchedEffect(aspect, status) { onAspect(aspect); onStatus(status) }
|
||||
DisposableEffect(engine) { onDispose { engine.stop() } }
|
||||
DisposableEffect(resources) { onDispose { resources.disposed = true; releaseSurface() } }
|
||||
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!!)
|
||||
if (resources.disposed) { texture.release(); return }
|
||||
releaseSurface()
|
||||
val player = StreamPlayerEngine(group, source, epoch)
|
||||
val surface = Surface(texture)
|
||||
resources.texture = texture; resources.surface = surface; resources.engine = player
|
||||
engine = player; onEngine(player); player.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
|
||||
releaseSurface(texture); return false
|
||||
}
|
||||
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
|
||||
}
|
||||
}
|
||||
}, modifier = modifier, onRelease = { view ->
|
||||
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
|
||||
})
|
||||
}, modifier = modifier, onRelease = { resources.disposed = true; releaseSurface() })
|
||||
}
|
||||
|
||||
/** TextureView передаёт освобождение texture приложению; repeated dispose не закрывает новый decoder. */
|
||||
private class StreamViewerSurface {
|
||||
var disposed = false
|
||||
var engine: StreamPlayerEngine? = null
|
||||
var surface: Surface? = null
|
||||
var texture: SurfaceTexture? = null
|
||||
}
|
||||
Reference in new issue
Block a user