fix: video orientation handling

- 用 StateFlow 实现监听复用
This commit is contained in:
Miuzarte
2026-04-10 02:26:23 +08:00
parent 731612c16e
commit 026fa50be2
6 changed files with 78 additions and 109 deletions

View File

@@ -16,7 +16,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -36,7 +35,6 @@ import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.services.EventLogger
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
@@ -97,7 +95,7 @@ fun DeviceTabScreen(
onOpenVirtualButtonOrder: () -> Unit,
onOpenReorderDevices: () -> Unit,
onOpenAdvancedPage: () -> Unit,
onOpenFullscreenPage: (Scrcpy.Session.SessionInfo) -> Unit,
onOpenFullscreenPage: () -> Unit,
) {
var showThreePointMenu by rememberSaveable { mutableStateOf(false) }
@@ -159,7 +157,7 @@ fun DeviceTabPage(
snackbar: SnackbarController,
scrollBehavior: ScrollBehavior,
onOpenAdvancedPage: () -> Unit,
onOpenFullscreenPage: (Scrcpy.Session.SessionInfo) -> Unit,
onOpenFullscreenPage: () -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -241,39 +239,9 @@ fun DeviceTabPage(
var adbConnected by rememberSaveable { mutableStateOf(false) }
var isQuickConnected by rememberSaveable { mutableStateOf(false) }
var currentTargetHost by rememberSaveable { mutableStateOf("") }
var currentTargetPort by rememberSaveable { mutableIntStateOf(Defaults.ADB_PORT) }
var currentTargetPort by rememberSaveable { mutableStateOf(Defaults.ADB_PORT) }
var connectedDeviceLabel by rememberSaveable { mutableStateOf("未连接") }
var sessionInfoWidth by rememberSaveable { mutableIntStateOf(0) }
var sessionInfoHeight by rememberSaveable { mutableIntStateOf(0) }
var sessionInfoDeviceName by rememberSaveable { mutableStateOf("") }
var sessionInfoCodec by rememberSaveable { mutableStateOf<Codec?>(null) }
var sessionInfoControlEnabled by rememberSaveable { mutableStateOf(false) }
var sessionInfo by remember {
mutableStateOf<Scrcpy.Session.SessionInfo?>(null)
}
LaunchedEffect(
sessionInfoWidth,
sessionInfoHeight,
sessionInfoDeviceName,
sessionInfoCodec,
sessionInfoControlEnabled
) {
sessionInfo = if (sessionInfoDeviceName.isNotBlank()) {
Scrcpy.Session.SessionInfo(
width = sessionInfoWidth,
height = sessionInfoHeight,
deviceName = sessionInfoDeviceName,
codecId = 0,
codec = sessionInfoCodec,
audioCodecId = 0,
controlEnabled = sessionInfoControlEnabled,
host = currentTargetHost,
port = currentTargetPort,
)
} else {
null
}
}
val sessionInfo by scrcpy.currentSessionState.collectAsState()
var previewControlsVisible by rememberSaveable { mutableStateOf(false) }
var editingDeviceId by rememberSaveable { mutableStateOf<String?>(null) }
var activeDeviceActionId by rememberSaveable { mutableStateOf<String?>(null) }
@@ -349,7 +317,6 @@ fun DeviceTabPage(
currentTargetPort = Defaults.ADB_PORT
audioForwardingSupported = true
cameraMirroringSupported = true
sessionInfo = null
statusLine = "未连接"
connectedDeviceLabel = "未连接"
clearQuickOnlineForTarget?.let { target ->
@@ -702,32 +669,6 @@ fun DeviceTabPage(
}
}
DisposableEffect(nativeCore) {
val listener: (Int, Int) -> Unit = { width, height ->
sessionInfo = sessionInfo?.copy(width = width, height = height)
}
nativeCore.addVideoSizeListener(listener)
onDispose {
nativeCore.removeVideoSizeListener(listener)
}
}
LaunchedEffect(sessionInfo) {
if (sessionInfo != null) {
sessionInfoWidth = sessionInfo?.width ?: 0
sessionInfoHeight = sessionInfo?.height ?: 0
sessionInfoDeviceName = sessionInfo?.deviceName.orEmpty()
sessionInfoCodec = sessionInfo?.codec
sessionInfoControlEnabled = sessionInfo?.controlEnabled == true
} else {
sessionInfoWidth = 0
sessionInfoHeight = 0
sessionInfoDeviceName = ""
sessionInfoCodec = null
sessionInfoControlEnabled = false
}
}
fun sendVirtualButtonAction(action: VirtualButtonAction) {
val keycode = action.keycode ?: return
runBusy("发送 ${action.title}") {
@@ -942,10 +883,6 @@ fun DeviceTabPage(
if (options.disableScreensaver) {
setKeepScreenOn(true)
}
sessionInfo = session.copy(
host = currentTargetHost,
port = currentTargetPort
)
statusLine = "scrcpy 运行中"
@SuppressLint("DefaultLocale")
val videoDetail =
@@ -972,7 +909,6 @@ fun DeviceTabPage(
runBusy("停止 scrcpy") {
scrcpy.stop()
setKeepScreenOn(false)
sessionInfo = null
statusLine = "${currentTarget!!.host}:${currentTarget.port}"
logEvent("scrcpy 已停止")
snackbar.show("scrcpy 已停止")
@@ -1013,8 +949,9 @@ fun DeviceTabPage(
previewControlsVisible = !previewControlsVisible
},
onOpenFullscreen = {
val info = sessionInfo ?: return@PreviewCard
onOpenFullscreenPage(info)
if (sessionInfo != null) {
onOpenFullscreenPage()
}
},
)
}

View File

@@ -43,14 +43,12 @@ import top.yukonga.miuix.kmp.basic.Scaffold
@Composable
fun FullscreenControlScreen(
onBack: () -> Unit,
session: Scrcpy.Session.SessionInfo,
scrcpy: Scrcpy,
nativeCore: NativeCoreFacade,
onVideoSizeChanged: (width: Int, height: Int) -> Unit,
) {
// Disable predictive back handler temporarily to avoid decoding issues.
BackHandler(enabled = true, onBack = onBack)
val context = LocalContext.current
val haptics = rememberAppHaptics()
@@ -58,6 +56,7 @@ fun FullscreenControlScreen(
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val activity = remember(context) { context as? Activity }
val currentSession by scrcpy.currentSessionState.collectAsState()
val asBundleShared by appSettings.bundleState.collectAsState()
val asBundleSharedLatest by rememberUpdatedState(asBundleShared)
@@ -119,14 +118,9 @@ fun FullscreenControlScreen(
}
}
DisposableEffect(nativeCore) {
val listener: (Int, Int) -> Unit = { w, h ->
onVideoSizeChanged(w, h)
}
nativeCore.addVideoSizeListener(listener)
onDispose {
nativeCore.removeVideoSizeListener(listener)
}
LaunchedEffect(currentSession?.width, currentSession?.height) {
val session = currentSession ?: return@LaunchedEffect
onVideoSizeChanged(session.width, session.height)
}
DisposableEffect(nativeCore) {
@@ -156,6 +150,7 @@ fun FullscreenControlScreen(
.fillMaxSize()
.padding(contentPadding),
) {
val session = currentSession ?: return@Box
FullscreenControlScreen(
session = session,
nativeCore = nativeCore,

View File

@@ -82,7 +82,7 @@ sealed interface RootScreen : NavKey {
data object Home : RootScreen
data object Advanced : RootScreen
data object VirtualButtonOrder : RootScreen
data class Fullscreen(val session: Scrcpy.Session.SessionInfo) : RootScreen
data object Fullscreen : RootScreen
}
@Composable
@@ -215,6 +215,7 @@ fun MainScreen() {
serverRemotePath = serverRemotePath,
)
}
val currentSession by scrcpy.currentSessionState.collectAsState()
fun handleBackNavigation() {
if (rootBackStack.size > 1) {
@@ -247,6 +248,11 @@ fun MainScreen() {
}
}
var showReorderDevices by rememberSaveable { mutableStateOf(false) }
var fullscreenOrientation by rememberSaveable {
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
val canNavigateBack = rootBackStack.size > 1
|| pagerState.currentPage != MainBottomTabDestination.Device.ordinal
@@ -265,11 +271,6 @@ fun MainScreen() {
}
}
var showReorderDevices by rememberSaveable { mutableStateOf(false) }
var fullscreenOrientation by rememberSaveable {
mutableIntStateOf(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
}
// Fullscreen route can force orientation based on stream ratio; all other routes are portrait.
LaunchedEffect(activity, currentRootScreen, fullscreenOrientation) {
val targetOrientation = when (currentRootScreen) {
@@ -279,6 +280,28 @@ fun MainScreen() {
activity?.requestedOrientation = targetOrientation
}
DisposableEffect(nativeCore, scrcpy) {
val listener: (Int, Int) -> Unit = { width, height ->
scrcpy.updateCurrentSessionSize(width, height)
}
nativeCore.addVideoSizeListener(listener)
onDispose {
nativeCore.removeVideoSizeListener(listener)
}
}
LaunchedEffect(currentRootScreen, currentSession?.width, currentSession?.height) {
if (currentRootScreen is RootScreen.Fullscreen) {
val session = currentSession ?: return@LaunchedEffect
fullscreenOrientation =
if (session.width >= session.height) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
}
LaunchedEffect(asBundle.adbKeyName) {
adbService.keyName = asBundle.adbKeyName.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue }
}
@@ -341,16 +364,16 @@ fun MainScreen() {
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
onOpenReorderDevices = { showReorderDevices = true },
onOpenAdvancedPage = { rootBackStack.add(RootScreen.Advanced) },
onOpenFullscreenPage = { session ->
fullscreenOrientation =
if (session.width >= session.height) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
rootBackStack.add(
RootScreen.Fullscreen(session),
)
onOpenFullscreenPage = {
currentSession?.let { session ->
fullscreenOrientation =
if (session.width >= session.height) {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
rootBackStack.add(RootScreen.Fullscreen)
}
},
)
@@ -396,10 +419,10 @@ fun MainScreen() {
)
}
entry<RootScreen.Fullscreen> { screen ->
entry(RootScreen.Fullscreen) {
FullscreenControlScreen(
onBack = ::popRoot,
session = screen.session,
scrcpy = scrcpy,
nativeCore = nativeCore,
onVideoSizeChanged = { width, height ->
fullscreenOrientation =

View File

@@ -16,6 +16,9 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.EncoderType
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.ListOptions
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@@ -58,8 +61,8 @@ class Scrcpy(
private val session = Session(adbService)
private val nativeCore: NativeCoreFacade = NativeCoreFacade.get(appContext)
@Volatile
private var currentSession: Session.SessionInfo? = null
private val _currentSessionState = MutableStateFlow<Session.SessionInfo?>(null)
val currentSessionState: StateFlow<Session.SessionInfo?> = _currentSessionState.asStateFlow()
@Volatile
private var isRunning: Boolean = false
@@ -136,7 +139,7 @@ class Scrcpy(
}
// Create session info
currentSession = info
_currentSessionState.value = info
isRunning = true
// Setup video consumer (notify NativeCoreFacade to setup decoders)
@@ -175,7 +178,7 @@ class Scrcpy(
} catch (e: Exception) {
Log.e(TAG, "start(): Failed to start scrcpy session", e)
isRunning = false
currentSession = null
_currentSessionState.value = null
throw e
}
}
@@ -196,7 +199,7 @@ class Scrcpy(
audioPlayer?.release()
audioPlayer = null
isRunning = false
currentSession = null
_currentSessionState.value = null
Log.i(TAG, "stop(): Session stopped successfully")
true
} catch (e: Exception) {
@@ -212,7 +215,13 @@ class Scrcpy(
fun isStarted(): Boolean = isRunning && session.isStarted()
fun getCurrentSession(): Session.SessionInfo? = currentSession
fun getCurrentSession(): Session.SessionInfo? = currentSessionState.value
fun updateCurrentSessionSize(width: Int, height: Int) {
val current = _currentSessionState.value ?: return
if (current.width == width && current.height == height) return
_currentSessionState.value = current.copy(width = width, height = height)
}
sealed class ListResult {
data class Encoders(