diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFileMetadata.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFileMetadata.kt index 1d179c46..c6706ba4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFileMetadata.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFileMetadata.kt @@ -41,6 +41,7 @@ internal class VideoFileMetadata : AutoCloseable { try { current.release(); retriever = null main.removeCallbacks(retry); scheduled = false; retained.remove(this) + LogManager.addLog("DEBUG", "VideoPlayer", "Metadata reader released owner=${System.identityHashCode(this)}") } catch (e: Exception) { retained.add(this) LogManager.addLog("ERROR", "VideoPlayer", "Metadata release failed; owner retained: $e") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFilePlayer.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFilePlayer.kt index 5b9f0408..7fcea69f 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFilePlayer.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoFilePlayer.kt @@ -20,6 +20,7 @@ internal data class VideoFilePlayback(val prepared: Boolean = false, val playing internal class VideoFilePlayer(onCompleted: () -> Unit) : AutoCloseable { companion object { private val retained = ConcurrentHashMap.newKeySet() } private val main = Handler(Looper.getMainLooper()) + private val id = System.identityHashCode(this) private val mutable = MutableStateFlow(VideoFilePlayback()) val state: StateFlow = mutable private var completion: (() -> Unit)? = onCompleted @@ -45,7 +46,7 @@ internal class VideoFilePlayer(onCompleted: () -> Unit) : AutoCloseable { if (!retiring && player === current) try { current.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT) mutable.value = mutable.value.copy(prepared = true, durationMs = current.duration.coerceAtLeast(0)) - LogManager.addLog("INFO", "VideoPlayer", "Prepared ${current.videoWidth}x${current.videoHeight} durationMs=${current.duration}") + LogManager.addLog("INFO", "VideoPlayer", "Prepared owner=$id ${current.videoWidth}x${current.videoHeight} durationMs=${current.duration}") if (wantsPlay && bound != null) play() } catch (e: Exception) { fail("prepare callback", e) } } @@ -70,6 +71,7 @@ internal class VideoFilePlayer(onCompleted: () -> Unit) : AutoCloseable { if (uiTargets.containsKey(texture)) return val target = try { StreamPreviewTarget(texture) } catch (e: Exception) { fail("create Surface", e); return } uiTargets[texture] = target + LogManager.addLog("DEBUG", "VideoPlayer", "Texture adopted owner=$id texture=${System.identityHashCode(texture)} disposed=$disposed") if (disposed) { target.release(); return } if (retiring) return // UI сохраняет собственную texture до destroy/close даже после ошибки player. bound = texture @@ -135,7 +137,7 @@ internal class VideoFilePlayer(onCompleted: () -> Unit) : AutoCloseable { } private fun fail(stage: String, error: Exception, message: Int = R.string.video_playback_failed) { - LogManager.addLog("ERROR", "VideoPlayer", "Operation failed stage=$stage; resources retained through release: $error") + LogManager.addLog("ERROR", "VideoPlayer", "Operation failed stage=$stage owner=$id; resources retained through release: $error") retiring = true; wantsPlay = false; completion = null mutable.value = mutable.value.copy(playing = false, prepared = false, error = message) cleanup() @@ -152,12 +154,12 @@ internal class VideoFilePlayer(onCompleted: () -> Unit) : AutoCloseable { private fun releasePlayerTargets() { playerTargets.forEach { it.release() }; playerTargets.clear() } private fun cleanup() { if (player != null) try { player!!.release(); player = null } catch (e: Exception) { - LogManager.addLog("ERROR", "VideoPlayer", "MediaPlayer release failed; owner retained: $e") + LogManager.addLog("ERROR", "VideoPlayer", "MediaPlayer release failed owner=$id; retained: $e") } if (player == null) { releasePlayerTargets() main.removeCallbacks(retry); scheduled = false; retained.remove(this) - LogManager.addLog("INFO", "VideoPlayer", "Player released disposed=$disposed") + LogManager.addLog("INFO", "VideoPlayer", "Player released owner=$id disposed=$disposed") } else { retained.add(this) if (!scheduled) { diff --git a/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/VideoFilePlayerTest.kt b/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/VideoFilePlayerTest.kt index 5c7ee27b..df4bc82f 100644 --- a/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/VideoFilePlayerTest.kt +++ b/tools/chatgui-android/video-tests/src/test/kotlin/com/utun/chat/data/VideoFilePlayerTest.kt @@ -33,7 +33,7 @@ class VideoFilePlayerTest { assertTrue(MediaPlayer.created.isEmpty()) } @Test fun surfaceBeforeStartIsAdoptedAndPlaybackWaitsForPrepared() { - val player = owner(); val texture = texture(); player.attach(texture()); assertTrue(MediaPlayer.created.isEmpty()) + val player = owner(); val texture = texture(); player.attach(texture); assertTrue(MediaPlayer.created.isEmpty()) player.start("file.mp4"); assertNotNull(sdk.surface); assertFalse(player.state.value.playing) sdk.preparedCallback!!(sdk) assertTrue(player.state.value.playing); assertEquals(10_000, player.state.value.durationMs); assertEquals(1, sdk.starts) @@ -89,7 +89,7 @@ class VideoFilePlayerTest { } @Test fun partialSurfaceBindFailureKeepsUiOutputAndUsesPlayerReleaseBarrier() { val player = owner(); player.start("file.mp4"); sdk.failures = setOf("surface after", "release") - val texture = texture(); player.attach(texture()); player.close() + val texture = texture(); player.attach(texture); player.close() assertFalse(texture.released) sdk.failures = emptySet(); main.advanceBy(250) assertTrue(texture.released); diagnostic("stage=attach Surface")