From 65d5f12344b95d074e3a809f5cd5b8769497c32d Mon Sep 17 00:00:00 2001 From: Miuzarte <982809597@qq.com> Date: Sat, 11 Apr 2026 15:33:15 +0800 Subject: [PATCH] fix: SurfaceView rather than TextureView --- .../scrcpyforandroid/StreamActivity.kt | 6 +- .../scrcpyforandroid/widgets/DeviceWidgets.kt | 60 +++++++++++-------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt index fcdfb8e..ce3c92d 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt @@ -29,9 +29,9 @@ class StreamActivity : ComponentActivity() { .setEnabled(false) .build() - // 是否处于 PiP。 - // 这台设备上进入 PiP 时,动画事件比 onPictureInPictureModeChanged() 更稳定, - // 所以进入 PiP 直接由动画事件置为 true。 + // 是否处于 PiP + // MIUI 上进入 PiP 时,动画事件比 onPictureInPictureModeChanged() 更稳定, + // 所以进入 PiP 直接由动画事件置为 true private val _pipModeState = MutableStateFlow(false) val pipModeState: StateFlow = _pipModeState diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 815d276..51db833 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -1,9 +1,9 @@ package io.github.miuzarte.scrcpyforandroid.widgets import android.annotation.SuppressLint -import android.graphics.SurfaceTexture import android.view.Surface -import android.view.TextureView +import android.view.SurfaceHolder +import android.view.SurfaceView import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -742,16 +742,16 @@ private fun PairingDialog( * ScrcpyVideoSurface * * Purpose: - * - Hosts a `TextureView` and bridges its `Surface` to `nativeCore` for video rendering. + * - Hosts a `SurfaceView` and bridges its `Surface` to `nativeCore` for video rendering. * - Ensures only a single `Surface` instance is registered at any time under the * stable `surfaceTag` ("video-main"). This reduces surface recreation bugs seen * when preview/fullscreen used separate tags. * * Concurrency / lifecycle: - * - `currentSurface` is only mutated on the UI thread via TextureView callbacks. + * - `currentSurface` is only mutated on the UI thread via SurfaceHolder callbacks. * - Registration to `nativeCore` is triggered from a [LaunchedEffect] when both * `session` and `currentSurface` are available. Unregistration happens in - * `onSurfaceTextureDestroyed` and `DisposableEffect.onDispose` to guarantee + * `surfaceDestroyed` and `DisposableEffect.onDispose` to guarantee * cleanup even if the composable leaves composition. * * Reliability notes: @@ -765,10 +765,21 @@ fun ScrcpyVideoSurface( target: VideoOutputTarget, ) { var currentSurface by remember { mutableStateOf(null) } + var currentSurfaceView by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val lifecycleOwner = LocalLifecycleOwner.current val currentTarget by VideoOutputTargetState.current.collectAsState() + val latestSession by rememberUpdatedState(session) + val latestCurrentTarget by rememberUpdatedState(currentTarget) + + LaunchedEffect(session?.width, session?.height, currentSurfaceView) { + val surfaceView = currentSurfaceView ?: return@LaunchedEffect + val currentSession = session ?: return@LaunchedEffect + if (currentSession.width > 0 && currentSession.height > 0) { + surfaceView.holder.setFixedSize(currentSession.width, currentSession.height) + } + } LaunchedEffect(session, currentSurface, currentTarget, target) { val surface = currentSurface @@ -811,44 +822,43 @@ fun ScrcpyVideoSurface( AndroidView( modifier = modifier, factory = { context -> - TextureView(context).apply { - surfaceTextureListener = object : TextureView.SurfaceTextureListener { - override fun onSurfaceTextureAvailable( - surfaceTexture: SurfaceTexture, - width: Int, - height: Int - ) { - @SuppressLint("Recycle") - val newSurface = Surface(surfaceTexture) + SurfaceView(context).apply { + currentSurfaceView = this + holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + val newSurface = holder.surface + if (!newSurface.isValid) return currentSurface = newSurface // Register immediately when surface becomes available - if (currentTarget == target && session != null) { + if (latestCurrentTarget == target && latestSession != null) { scope.launch { NativeCoreFacade.attachVideoSurface(newSurface) } } } - override fun onSurfaceTextureSizeChanged( - surfaceTexture: SurfaceTexture, + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, width: Int, - height: Int - ) = Unit + height: Int, + ) { + if (width <= 0 || height <= 0) return + val surface = holder.surface + if (!surface.isValid) return + currentSurface = surface + } - override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean { + override fun surfaceDestroyed(holder: SurfaceHolder) { val surface = currentSurface if (surface != null) { taskScope.launch { NativeCoreFacade.detachVideoSurface(surface, releaseDecoder = false) } - surface.release() currentSurface = null } - return true } - - override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) = Unit - } + }) } }, update = {},