fix: video orientation handling
- 用 StateFlow 实现监听复用
This commit is contained in:
@@ -23,15 +23,16 @@
|
||||
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
||||
- 自动横竖屏切换(算吗
|
||||
|
||||
## 已知问题 / TODO
|
||||
## 已知问题
|
||||
|
||||
- 退出全屏时的横竖屏状态可能不对,断开 scrcpy 重连就好
|
||||
- 如果受控机在锁屏时处理网络连接较慢,会导致应用界面无响应过长时间被系统杀掉(点名你米)
|
||||
- 虚拟按键的截图实现方式为发送
|
||||
`keycode 120`,安卓官方([keycodes.h#349](https://android.googlesource.com/platform/frameworks/native/+/master/include/android/keycodes.h#349))的定义为
|
||||
`System Request / Print Screen key.`,不同的厂商有不同的实现,在某些类原生(`AxionOS`) 上的行为是软重启
|
||||
- I18N
|
||||
- More: [TODO.md](TODO.md)
|
||||
|
||||
## [TODO](TODO.md)
|
||||
|
||||
\> [TODO.md](TODO.md)
|
||||
|
||||
## 建议搭配模块
|
||||
|
||||
|
||||
4
TODO.md
4
TODO.md
@@ -1,5 +1,8 @@
|
||||
# TODO
|
||||
|
||||
- miuix v0.9
|
||||
- 在线更新检查
|
||||
|
||||
## PARAMS
|
||||
|
||||
- orientation locking
|
||||
@@ -18,3 +21,4 @@
|
||||
- [音频延迟](https://developer.android.com/ndk/guides/audio/audio-latency)
|
||||
- 横屏布局
|
||||
- 原生悬浮窗 [Petterpx/FloatingX](https://github.com/Petterpx/FloatingX)
|
||||
- I18N
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
onOpenFullscreenPage = {
|
||||
currentSession?.let { session ->
|
||||
fullscreenOrientation =
|
||||
if (session.width >= session.height) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
rootBackStack.add(
|
||||
RootScreen.Fullscreen(session),
|
||||
)
|
||||
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 =
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user