From a27d8638b4ce978663fcdd385a172fc3e2815db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AC=AC=E7=B4=97=E7=89=B9?= <66856838+Miuzarte@users.noreply.github.com> Date: Fri, 15 May 2026 19:35:03 +0800 Subject: [PATCH] upstream: scrcpy v4 (#27) * feat: download scrcpy-server in pre-build * protocol alignment * implement new options * fix: switching after creating a new conf * feat: implement `--new-display` --- .gitignore | 3 + TODO.md | 39 -- app/build.gradle.kts | 78 +++- app/src/main/assets/bin/.gitkeep | 0 app/src/main/assets/bin/scrcpy-server-v3.3.4 | Bin 90980 -> 0 bytes .../scrcpyforandroid/NativeCoreFacade.kt | 61 ++- .../constants/ScrcpyPresets.kt | 22 +- .../pages/DeviceTabViewModel.kt | 10 +- .../pages/FullscreenControlScreen.kt | 29 +- .../pages/ScrcpyAllOptionsScreen.kt | 88 ++++ .../scrcpyforandroid/scrcpy/ClientOptions.kt | 45 ++ .../scrcpyforandroid/scrcpy/Scrcpy.kt | 153 +++++-- .../scrcpyforandroid/scrcpy/ServerParams.kt | 127 +++--- .../scrcpyforandroid/storage/ScrcpyOptions.kt | 401 +++++++++++++++++- .../storage/ScrcpyProfiles.kt | 333 +-------------- .../scrcpyforandroid/util/Debouncer.kt | 35 ++ app/src/main/res/values-zh/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 18 files changed, 951 insertions(+), 483 deletions(-) create mode 100644 app/src/main/assets/bin/.gitkeep delete mode 100644 app/src/main/assets/bin/scrcpy-server-v3.3.4 create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/util/Debouncer.kt diff --git a/.gitignore b/.gitignore index be25ffe..26ed072 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ scrcpy.help* *.mp4 abandoned_assets/ + +# scrcpy-server binary (downloaded by Gradle during build) +app/src/main/assets/bin/scrcpy-server-v* diff --git a/TODO.md b/TODO.md index efbe94d..9f41972 100644 --- a/TODO.md +++ b/TODO.md @@ -4,45 +4,6 @@ - 新的原创图标而非从 scrcpy 的图标派生 -## scrcpy v4.0 - -### 协议变更 (breaking) - -- 会话包 (session packet):视频流新增 12 字节会话包 (替代旧的 codec meta 中包含的 width/height),携带帧宽高 + `client_resized` 标志位 - - `Scrcpy.kt:1091-1095` 当前读视频 meta 12 字节 (codec+width+height),需改为只读 4 字节 codec id - - `Scrcpy.kt:1157-1166` 视频读取循环需新增分支:bit 63=1 → 解析 session packet (width/height/flags);bit 63=0 → 媒体包 - - `SessionInfo` 的 width/height 不再从 init meta 获得,需从首个 session packet 或第一帧捕获后填充 - - 音频流无 session packet,`attachAudioConsumer()` 不受影响 - -- 帧头 (frame header):PTS 从 `u62` 改为 `u61`,新增 `media_packet_flag` 位 (最高 bit = 1 表示会话包,= 0 表示媒体包) - - `Scrcpy.kt:1728` `PACKET_FLAG_CONFIG`: `1L shl 63` → `1L shl 62` - - `Scrcpy.kt:1729` `PACKET_FLAG_KEY_FRAME`: `1L shl 62` → `1L shl 61` - - `Scrcpy.kt:1730` `PACKET_PTS_MASK`: `(1L shl 62) - 1` → `(1L shl 61) - 1` - - 新增 `PACKET_FLAG_SESSION = 1L shl 63` - - 视频读取 `Scrcpy.kt:1168` 和音频读取 `Scrcpy.kt:1215` 的 flag 解析逻辑须同步更新 - -- 流元数据:`sendCodecMeta` → `sendStreamMeta`,`writeVideoHeader()` 不再发送 width/height (只发送 codec id 4 字节) - - `ServerParams.kt` 当前未传递此参数,无需改动 (v4.0 server 默认 `sendStreamMeta=true`) - - `Scrcpy.kt:1091-1095` 改为只读 `vInput.readInt()` (仅 codec id) - -- 新增 4 个控制消息类型: - - `TYPE_CAMERA_SET_TORCH` (18) — 1 字节 bool - - `Scrcpy.kt:1739` 需新增常量 + `ControlWriter` 新增 `setCameraTorch(on: Boolean)` 方法 - - `TYPE_CAMERA_ZOOM_IN` (19) / `TYPE_CAMERA_ZOOM_OUT` (20) — 无参数 - - 同上,各一个无参方法 - - `TYPE_RESIZE_DISPLAY` (21) — 2×uint16 (width, height) - - 同上,新增 `resizeDisplay(w: Int, h: Int)` 方法;配合 flex display 功能使用 - -- 版本号:`Scrcpy.kt:121` `DEFAULT_SERVER_VERSION` 需从 `"3.3.4"` 改为 `"4.0"`,否则 v4.0 server 拒绝连接 - -### 新参数 - -- (`-x` / `--flex-display`):Flex display 虚拟显示器大小连续跟随窗口缩放 -- `--keep-active`:每 4s 模拟用户活动防止息屏 -- `--camera-zoom`:镜头初始缩放值 (maybeTODO: 虚拟按键调整缩放) -- `--camera-torch`:手电筒初始启用 -- `--min-size-alignment`:强制视频尺寸对齐倍数 (power-of-2) - ## PARAMS - ~~orientation locking~~ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 898278f..f555116 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.net.URI +import java.security.MessageDigest import java.util.Properties plugins { @@ -56,8 +58,8 @@ android { applicationId = "io.github.miuzarte.scrcpyforandroid" minSdk = 26 targetSdk = 37 - versionCode = 30 - versionName = "0.3.4" + versionCode = 31 + versionName = "0.4.0" externalNativeBuild { cmake { @@ -162,3 +164,75 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) } + +val scrcpyServerAssetDir = "${project.projectDir}/src/main/assets/bin" +val scrcpyServerAssetFile = "$scrcpyServerAssetDir/scrcpy-server-v4.0" +val scrcpyServerDownloadUrl = "https://github.com/Genymobile/scrcpy/releases/download/v4.0/scrcpy-server-v4.0" +val scrcpyServerSha256 = "84924bd564a1eb6089c872c7521f968058977f91f5ff02514a8c74aff3210f3a" + +val downloadScrcpyServer by tasks.registering { + description = "Download scrcpy-server binary from GitHub releases if absent or SHA256 mismatch" + group = "build setup" + + inputs.property("downloadUrl", scrcpyServerDownloadUrl) + inputs.property("expectedSha256", scrcpyServerSha256) + outputs.file(scrcpyServerAssetFile) + + doLast { + val file = outputs.files.singleFile + val url = inputs.properties["downloadUrl"] as String + val expectedSha = inputs.properties["expectedSha256"] as String + val dir = file.parentFile + + if (!dir.exists()) dir.mkdirs() + + fun computeSha256(f: File): String { + return f.inputStream().use { input -> + val digest = MessageDigest.getInstance("SHA-256") + val buffer = ByteArray(8192) + var count: Int + while (input.read(buffer).also { count = it } >= 0) { + digest.update(buffer, 0, count) + } + digest.digest().joinToString("") { "%02x".format(it) } + } + } + + val needsDownload = !file.exists() || computeSha256(file) != expectedSha + + if (needsDownload) { + logger.lifecycle("Downloading scrcpy-server-v4.0 from GitHub releases...") + try { + URI(url).toURL().openStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + } catch (e: Exception) { + throw GradleException( + "Failed to download scrcpy-server-v4.0 from GitHub releases.\n" + + " URL: $url\n" + + " You may download it manually and place it at: ${file.absolutePath}\n" + + " If you are behind a proxy, check your Gradle proxy settings\n" + + " (gradle.properties: systemProp.https.proxyHost / systemProp.https.proxyPort).", + e + ) + } + + val actualSha = computeSha256(file) + require(actualSha == expectedSha) { + "SHA256 mismatch for scrcpy-server-v4.0!\n" + + " Expected: $expectedSha\n" + + " Got: $actualSha\n" + + " Delete ${file.absolutePath} to retry download." + } + logger.lifecycle("scrcpy-server-v4.0 downloaded and verified.") + } else { + logger.lifecycle("scrcpy-server-v4.0 exists with correct SHA256, skip download.") + } + } +} + +tasks.named("preBuild") { + dependsOn(downloadScrcpyServer) +} diff --git a/app/src/main/assets/bin/.gitkeep b/app/src/main/assets/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/assets/bin/scrcpy-server-v3.3.4 b/app/src/main/assets/bin/scrcpy-server-v3.3.4 deleted file mode 100644 index 89054b7ecfa6735b93ee7b293e91abe7ecd8d26e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90980 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ŕhu8Y%@FGhFErLoLjI!7Xx= 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 z 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 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt index a854cff..8dd7326 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt @@ -8,6 +8,11 @@ import io.github.miuzarte.scrcpyforandroid.nativecore.AnnexBDecoder import io.github.miuzarte.scrcpyforandroid.nativecore.PersistentVideoRenderer import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.util.ArrayDeque @@ -22,6 +27,7 @@ import java.util.concurrent.CopyOnWriteArraySet */ object NativeCoreFacade { private val sessionLifecycleMutex = Mutex() + private val lifecycleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private val renderer = PersistentVideoRenderer() private var activeSurfaceId: Int? = null private var decoder: AnnexBDecoder? = null @@ -41,6 +47,10 @@ object NativeCoreFacade { @Volatile private var currentSessionInfo: Scrcpy.Session.SessionInfo? = null + // Reference to Scrcpy for reading currentSessionState (set by onScrcpySessionStarted) + @Volatile + private var scrcpyRef: Scrcpy? = null + suspend fun close() { sessionLifecycleMutex.withLock { releaseAllDecoders() @@ -78,6 +88,10 @@ object NativeCoreFacade { return } } + if (session.width <= 0 || session.height <= 0) { + Log.i(TAG, "attachVideoSurface(): defer decoder, session size not yet known (${session.width}x${session.height})") + return + } createOrReplaceDecoder(session) } } @@ -157,8 +171,10 @@ object NativeCoreFacade { */ suspend fun onScrcpySessionStarted( session: Scrcpy.Session.SessionInfo, - sessionMgr: Scrcpy.Session + sessionMgr: Scrcpy.Session, + scrcpy: Scrcpy, ) = sessionLifecycleMutex.withLock { + scrcpyRef = scrcpy currentSessionInfo = session releaseAllDecoders() synchronized(bootstrapLock) { @@ -166,8 +182,13 @@ object NativeCoreFacade { latestConfigPacket = null } if (activeSurfaceId != null || recordingSurfaceAttached) { - Log.i(TAG, "onScrcpySessionStarted(): bind decoder to persistent surface") - createOrReplaceDecoder(session) + // v4.0: width/height come from first video session packet, not from initial metadata + if (session.width > 0 && session.height > 0) { + Log.i(TAG, "onScrcpySessionStarted(): bind decoder to persistent surface") + createOrReplaceDecoder(session) + } else { + Log.i(TAG, "onScrcpySessionStarted(): defer decoder until first video session packet (v4.0)") + } } packetCount = 0 sessionMgr.attachVideoConsumer { packet -> @@ -179,9 +200,10 @@ object NativeCoreFacade { "videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoder=${decoder != null}" ) } - val currentDecoder = decoder ?: return@attachVideoConsumer + + val dec = decoder ?: return@attachVideoConsumer runCatching { - currentDecoder.feedAnnexB( + dec.feedAnnexB( packet.data, packet.ptsUs, packet.isKeyFrame, @@ -191,6 +213,34 @@ object NativeCoreFacade { } } + /** + * Called by Scrcpy when a video session packet arrives with new dimensions. + * Launches a coroutine to create or rebuild the decoder under the lifecycle mutex. + */ + fun onVideoSizeChanged(width: Int, height: Int) { + lifecycleScope.launch { + sessionLifecycleMutex.withLock { + val scrcpy = scrcpyRef ?: return@withLock + val info = scrcpy.currentSessionState.value ?: return@withLock + if (info.width <= 0 || info.height <= 0) return@withLock + + if (decoder == null) { + // Initial creation (v4.0: deferred until first session packet) + Log.i(TAG, "onVideoSizeChanged(): create decoder ${info.width}x${info.height}") + currentSessionInfo = info + createOrReplaceDecoder(info) + } else if (currentSessionInfo != null && + (info.width != currentSessionInfo!!.width || info.height != currentSessionInfo!!.height) + ) { + // Flex display: rebuild decoder on size change + Log.i(TAG, "onVideoSizeChanged(): rebuild decoder ${currentSessionInfo!!.width}x${currentSessionInfo!!.height} → ${info.width}x${info.height}") + currentSessionInfo = info + createOrReplaceDecoder(info) + } + } + } + } + /** * Called by Scrcpy.kt when a session stops. * Cleans up decoders and resets state. @@ -201,6 +251,7 @@ object NativeCoreFacade { bootstrapPackets.clear() latestConfigPacket = null } + scrcpyRef = null currentSessionInfo = null recordingSurfaceAttached = false } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt index 8738851..7532014 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt @@ -35,28 +35,8 @@ class Preset>(val values: List) { } } -/** - * Extension function for Int presets to find index from Int value. - */ -fun Preset.indexOfOrNearest(raw: Int): Int { - val exact = values.indexOf(raw) - if (exact >= 0) return exact - val nearest = values.withIndex().minByOrNull { (_, preset) -> - kotlin.math.abs(preset - raw) - } - return nearest?.index ?: 0 -} - -/** - * Extension function for Int presets to find index from String value. - */ -fun Preset.indexOfOrNearest(raw: String): Int { - if (raw.isBlank()) return 0 - val value = raw.toIntOrNull() ?: return 0 - return indexOfOrNearest(value) -} - object ScrcpyPresets { + val MinSizeAlignment = Preset(listOf(1, 2, 4, 8, 16)) // power-of-2 val MaxSize = Preset(listOf(0, 720, 1080, 1280, 1600, 1920, 2160, 2560, 3200, 3840)) // px val MaxFPS = Preset(listOf(0, 24, 30, 45, 60, 90, 120)) // fps val ScreenOffTimeout = Preset(listOf(0, 15, 30, 60, 120, 300, 600)) // sec diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabViewModel.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabViewModel.kt index f2a3628..8de1379 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabViewModel.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabViewModel.kt @@ -605,9 +605,13 @@ internal class DeviceTabViewModel( val videoDetail = if (!resolvedOptions.video) "off" - else if (activeBundle.videoBitRate <= 0) "${session.codec?.string ?: "null"} ${session.width}x${session.height} @default" - else "${session.codec?.string ?: "null"} ${session.width}x${session.height} " + - "@%.1fMbps".format(activeBundle.videoBitRate / 1_000_000f) + else { + val codec = session.codec?.string ?: "null" + val sizeHint = if (session.width > 0 && session.height > 0) " ${session.width}x${session.height}" else "" + val bitrateSuffix = if (activeBundle.videoBitRate <= 0) " @default" + else " @%.1fMbps".format(activeBundle.videoBitRate / 1_000_000f) + "$codec$sizeHint$bitrateSuffix" + } val audioDetail = if (!activeBundle.audio) "off" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt index 80058f2..aa14fbd 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt @@ -59,11 +59,12 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler import io.github.miuzarte.scrcpyforandroid.services.AppRuntime -import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController import io.github.miuzarte.scrcpyforandroid.services.LocalInputService +import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings +import io.github.miuzarte.scrcpyforandroid.util.Debouncer import io.github.miuzarte.scrcpyforandroid.widgets.AppListBottomSheet import io.github.miuzarte.scrcpyforandroid.widgets.AppListEntry import io.github.miuzarte.scrcpyforandroid.widgets.ScrcpyVideoSurface @@ -683,6 +684,20 @@ fun FullscreenControlPage( ) } + val resizeDebouncer = remember { + Debouncer(300L) { w, h -> + coroutineScope.launch(Dispatchers.IO) { + scrcpy.resizeDisplay(w, h) + } + } + } + + DisposableEffect(resizeDebouncer) { + onDispose { + resizeDebouncer.cancel() + } + } + BoxWithConstraints( modifier = Modifier .fillMaxSize() @@ -694,7 +709,17 @@ fun FullscreenControlPage( } else Modifier ) - .onSizeChanged { touchAreaSize = it }, + .onSizeChanged { size -> + touchAreaSize = size + if (scrcpy.flexDisplay) { + val sessionKnown = scrcpy.currentSessionState.value?.let { + it.width > 0 && it.height > 0 + } ?: false + if (sessionKnown) { + resizeDebouncer.invoke(size.width, size.height) + } + } + }, ) { val sessionAspect = if (session.height == 0) 16f / 9f diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt index 64901e2..883b1bd 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt @@ -331,6 +331,7 @@ internal fun ScrcpyAllOptionsScreen( bundle = copySourceBundle, ) selectedProfileId = created.id + bindCurrentConnectedDevice(created.id) } ProfileDialogMode.Rename -> { @@ -597,6 +598,9 @@ internal fun ScrcpyAllOptionsPage( var cameraArInput by rememberSaveable(soBundle.cameraAr) { mutableStateOf(soBundle.cameraAr) } + var cameraZoomInput by rememberSaveable(soBundle.cameraZoom) { + mutableStateOf(soBundle.cameraZoom) + } val cameraFpsPresetIndex = rememberSaveable(soBundle.cameraFps) { ScrcpyPresets.CameraFps.indexOfOrNearest(soBundle.cameraFps) @@ -639,6 +643,10 @@ internal fun ScrcpyAllOptionsPage( } } + val minSizeAlignmentPresetIndex = rememberSaveable(soBundle.minSizeAlignment) { + ScrcpyPresets.MinSizeAlignment.indexOfOrNearest(soBundle.minSizeAlignment) + } + val maxSizePresetIndex = rememberSaveable(soBundle.maxSize) { ScrcpyPresets.MaxSize.indexOfOrNearest(soBundle.maxSize) } @@ -1017,6 +1025,16 @@ internal fun ScrcpyAllOptionsPage( ) }, ) + SwitchPreference( + title = stringResource(R.string.scrcpyopt_keep_active), + summary = "--keep-active", + checked = soBundle.keepActive, + onCheckedChange = { + soBundle = soBundle.copy( + keepActive = it + ) + }, + ) SwitchPreference( title = stringResource(R.string.scrcpyopt_show_touches), summary = "--show-touches", @@ -1224,6 +1242,39 @@ internal fun ScrcpyAllOptionsPage( ) }, ) + SuperSlider( + title = stringResource(R.string.scrcpyopt_min_size_alignment), + summary = "--min-size-alignment", + value = minSizeAlignmentPresetIndex.toFloat(), + onValueChange = { + val idx = it.roundToInt() + .coerceIn(0, ScrcpyPresets.MinSizeAlignment.lastIndex) + soBundle = soBundle.copy( + minSizeAlignment = ScrcpyPresets.MinSizeAlignment[idx] + ) + }, + valueRange = 0f..ScrcpyPresets.MinSizeAlignment.lastIndex.toFloat(), + steps = (ScrcpyPresets.MinSizeAlignment.size - 2).coerceAtLeast(0), + showKeyPoints = true, + keyPoints = ScrcpyPresets.MinSizeAlignment.indices.map { it.toFloat() }, + displayText = soBundle.minSizeAlignment.toString(), + inputInitialValue = soBundle.minSizeAlignment + .takeIf { it != 1 } + ?.toString() + ?: "", + inputFilter = { it.filter(Char::isDigit) }, + inputValueRange = 1f..16f, + onInputConfirm = { raw -> + val parsed = raw.toIntOrNull() ?: return@SuperSlider + if (parsed and (parsed - 1) != 0) { + AppRuntime.snackbar(R.string.scrcpyopt_min_size_alignment_invalid) + return@SuperSlider + } + soBundle = soBundle.copy( + minSizeAlignment = parsed + ) + }, + ) SuperSlider( title = stringResource(R.string.scrcpyopt_max_size), summary = "--max-size", @@ -1433,6 +1484,20 @@ internal fun ScrcpyAllOptionsPage( .fillMaxWidth() .padding(all = UiSpacing.Large), ) + SuperTextField( + value = cameraZoomInput, + onValueChange = { cameraZoomInput = it }, + onFocusLost = { + soBundle = soBundle.copy( + cameraZoom = cameraZoomInput + ) + }, + label = "--camera-zoom", + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(all = UiSpacing.Large), + ) SuperSlider( title = stringResource(R.string.scrcpyopt_camera_fps), summary = "--camera-fps", @@ -1472,6 +1537,16 @@ internal fun ScrcpyAllOptionsPage( ) }, ) + SwitchPreference( + title = stringResource(R.string.scrcpyopt_camera_torch), + summary = "--camera-torch", + checked = soBundle.cameraTorch, + onCheckedChange = { + soBundle = soBundle.copy( + cameraTorch = it + ) + }, + ) } } @@ -1736,6 +1811,19 @@ internal fun ScrcpyAllOptionsPage( ) }, ) + SwitchPreference( + title = stringResource(R.string.scrcpyopt_flex_display), + summary = "--flex-display", + checked = soBundle.flexDisplay, + onCheckedChange = { + soBundle = soBundle.copy( + flexDisplay = it + ) + if (it) AppRuntime.snackbar( + "untested" + ) + }, + ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt index 8719cf4..78c0f4b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt @@ -40,6 +40,8 @@ data class ClientOptions( var cameraSize: String = "", // to server // --camera-ar var cameraAr: String = "", // to server + // --camera-zoom + var cameraZoom: String = "", // to server // --camera-fps var cameraFps: UShort = 0u, // to server @@ -74,6 +76,9 @@ data class ClientOptions( // OR of enum sc_shortcut_mod values // var shortcutMods: ShortcutMod, + // --min-size-alignment + var minSizeAlignment: UByte = 1u, // to server + // --max-size var maxSize: UShort = 0u, // to server @@ -207,6 +212,15 @@ data class ClientOptions( var vdDestroyContent: Boolean = true, // to server // --no-vd-system-decorations var vdSystemDecorations: Boolean = true, // to server + + // --camera-torch + var cameraTorch: Boolean = false, // to server + + // --keep-active + var keepActive: Boolean = false, // to server + + // --flex-display + var flexDisplay: Boolean = false, // to server ) { enum class KeyInjectMode(val string: String) { MIXED("mixed"), @@ -372,6 +386,27 @@ data class ClientOptions( ) } + if (flexDisplay) { + if (videoSource != VideoSource.DISPLAY || newDisplay.isBlank()) { + throw IllegalArgumentException( + "-x/--flex-display can only be applied to displays created " + + "with --new-display" + ) + } + + if (!control) { + throw IllegalArgumentException( + "-n/--no-control is not compatible with -x/--flex-display" + ) + } + + if (crop.isNotBlank()) { + throw IllegalArgumentException( + "--crop is not compatible with -x/--flex-display" + ) + } + } + if (displayImePolicy != DisplayImePolicy.UNDEFINED && displayId == 0 && newDisplay.isBlank() ) { @@ -542,6 +577,11 @@ data class ClientOptions( "Cannot start an Android app if control is disabled" ) } + if (keepActive) { + throw IllegalArgumentException( + "Cannot keep device active if control is disabled" + ) + } } return this @@ -560,6 +600,7 @@ data class ClientOptions( crop = crop, maxSize = maxSize, + minSizeAlignment = minSizeAlignment, videoBitRate = videoBitRate, audioBitRate = audioBitRate, maxFps = maxFps, @@ -583,6 +624,7 @@ data class ClientOptions( cameraId = cameraId, cameraSize = cameraSize, cameraAr = cameraAr, + cameraZoom = cameraZoom, cameraFps = cameraFps, powerOffOnClose = powerOffOnClose, @@ -595,8 +637,11 @@ data class ClientOptions( powerOn = powerOn, cameraHighSpeed = cameraHighSpeed, + cameraTorch = cameraTorch, vdDestroyContent = vdDestroyContent, vdSystemDecorations = vdSystemDecorations, + keepActive = keepActive, + flexDisplay = flexDisplay, list = list, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt index c7e7938..53daf84 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt @@ -42,6 +42,8 @@ import java.io.DataOutputStream import java.io.EOFException import java.io.File import java.io.InputStreamReader +import java.nio.ByteBuffer +import java.nio.ByteOrder import java.util.ArrayDeque import java.util.Locale import kotlin.concurrent.thread @@ -74,7 +76,10 @@ class Scrcpy( private val lowLatency: Boolean = false, ) { - private val session = Session(::handleRemoteClipboardText) + private val session = Session( + ::handleRemoteClipboardText, + ::updateCurrentSessionSize, + ) private val backgroundScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val clipboardSyncLock = Any() private val clipboardManager by lazy { @@ -99,6 +104,10 @@ class Scrcpy( @Volatile private var isRunning: Boolean = false + @Volatile + @JvmField + var flexDisplay: Boolean = false + @Volatile private var audioPlayer: ScrcpyAudioPlayer? = null @@ -116,9 +125,9 @@ class Scrcpy( companion object { private const val TAG = "Scrcpy" - const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v3.3.4" - const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v3.3.4" - const val DEFAULT_SERVER_VERSION = "3.3.4" + const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v4.0" + const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v4.0" + const val DEFAULT_SERVER_VERSION = "4.0" const val DEFAULT_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar" // Regex patterns for parsing server output @@ -197,11 +206,12 @@ class Scrcpy( forwardKeyRepeat = options.forwardKeyRepeat, ) isRunning = true + flexDisplay = options.flexDisplay startClipboardSync() // Setup video consumer (notify NativeCoreFacade to setup decoders) if (options.video) { - NativeCoreFacade.onScrcpySessionStarted(info, session) + NativeCoreFacade.onScrcpySessionStarted(info, session, this@Scrcpy) } // Setup audio player @@ -287,7 +297,10 @@ class Scrcpy( Log.i( TAG, "start(): Session started successfully - device=${info.deviceName}, " + - "video=${if (options.video) "${info.codec?.string ?: "null"} ${info.width}x${info.height}" else "off"}, " + + "video=${if (options.video) buildString { + append(info.codec?.string ?: "null") + if (info.width > 0 && info.height > 0) append(" ${info.width}x${info.height}") + } else "off"}, " + "audio=${if (options.audio) options.audioCodec.string else "off"}, " + "control=${options.control}" ) @@ -305,6 +318,7 @@ class Scrcpy( runCatching { aacRecorder?.release() } aacRecorder = null isRunning = false + flexDisplay = false _currentSessionState.value = null throw e } @@ -332,6 +346,7 @@ class Scrcpy( audioPlayer?.release() audioPlayer = null isRunning = false + flexDisplay = false _currentSessionState.value = null stopClipboardSync() Log.i(TAG, "stop(): Session stopped successfully") @@ -348,6 +363,10 @@ class Scrcpy( session.startApp(name) } + suspend fun resizeDisplay(width: Int, height: Int) = withContext(Dispatchers.IO) { + session.resizeDisplay(width, height) + } + suspend fun injectKeycode( action: Int, keycode: Int, @@ -423,6 +442,7 @@ class Scrcpy( val current = _currentSessionState.value ?: return if (current.width == width && current.height == height) return _currentSessionState.value = current.copy(width = width, height = height) + NativeCoreFacade.onVideoSizeChanged(width, height) } private fun startClipboardSync() { @@ -970,6 +990,7 @@ class Scrcpy( */ class Session( private val onRemoteClipboardText: (String) -> Unit, + private val onVideoSessionSize: (Int, Int) -> Unit, ) { private val mutex = Mutex() @@ -1085,20 +1106,17 @@ class Scrcpy( } else { 0 } - val videoCodecId: Int - val width: Int - val height: Int - if (options.video) { - val vInput = checkNotNull(videoInput) - videoCodecId = vInput.readInt() - width = vInput.readInt() - height = vInput.readInt() + val videoCodecId = if (options.video) { + checkNotNull(videoInput).readInt() } else { - videoCodecId = 0 - width = 0 - height = 0 + 0 } + // Video dimensions now come from the first session packet in the stream, + // not from stream metadata (v4.0 protocol change). + val width = 0 + val height = 0 + val sessionInfo = SessionInfo( deviceName = deviceName, codecId = videoCodecId, @@ -1154,11 +1172,37 @@ class Scrcpy( videoReaderThread = thread(start = true, name = "scrcpy-video-reader") { try { + val sessionFlagByte = (PACKET_FLAG_SESSION ushr 56).toInt() + val headerBuf = ByteArray(12) while (activeSession === session && !vStream.closed) { try { - val ptsAndFlags = vInput.readLong() - val packetSize = vInput.readInt() - if (packetSize <= 0) { + vInput.readFully(headerBuf) + + if ((headerBuf[0].toInt() and sessionFlagByte) != 0) { + // Session packet: parse width/height, update state, no payload + val clientResized = (headerBuf[3].toInt() and 1) != 0 + val sw = ((headerBuf[4].toInt() and 0xFF) shl 24) or + ((headerBuf[5].toInt() and 0xFF) shl 16) or + ((headerBuf[6].toInt() and 0xFF) shl 8) or + (headerBuf[7].toInt() and 0xFF) + val sh = ((headerBuf[8].toInt() and 0xFF) shl 24) or + ((headerBuf[9].toInt() and 0xFF) shl 16) or + ((headerBuf[10].toInt() and 0xFF) shl 8) or + (headerBuf[11].toInt() and 0xFF) + Log.i( + TAG, + "video session packet: ${sw}x${sh} clientResized=$clientResized" + ) + onVideoSessionSize(sw, sh) + continue + } + + // Media packet: parse ptsAndFlags + packetSize from header + val bb = ByteBuffer.wrap(headerBuf) + .order(ByteOrder.BIG_ENDIAN) + val ptsAndFlags = bb.long + val packetSize = bb.int + if (packetSize !in 1..10_000_000) { continue } @@ -1244,7 +1288,14 @@ class Scrcpy( requireControlWriter().startApp(name) } catch (e: IllegalStateException) { Log.w(TAG, "startApp(): control channel not available", e) - throw e + } + } + + suspend fun resizeDisplay(width: Int, height: Int) = mutex.withLock { + try { + requireControlWriter().resizeDisplay(width, height) + } catch (e: IllegalStateException) { + Log.w(TAG, "resizeDisplay(): control channel not available", e) } } @@ -1625,7 +1676,7 @@ class Scrcpy( fun setClipboard(text: String, paste: Boolean) { val bytes = text.toByteArray(Charsets.UTF_8) output.writeByte(TYPE_SET_CLIPBOARD) - output.writeLong(CLIPBOARD_SEQUENCE_INVALID) + output.writeLong(SEQUENCE_INVALID) output.writeByte(if (paste) 1 else 0) output.writeInt(bytes.size) output.write(bytes) @@ -1698,6 +1749,36 @@ class Scrcpy( output.flush() } + @Synchronized + fun setCameraTorch(on: Boolean) { + output.writeByte(TYPE_CAMERA_SET_TORCH) + output.writeBoolean(on) + output.flush() + } + + @Synchronized + fun cameraZoomIn() { + output.writeByte(TYPE_CAMERA_ZOOM_IN) + output.flush() + } + + @Synchronized + fun cameraZoomOut() { + output.writeByte(TYPE_CAMERA_ZOOM_OUT) + output.flush() + } + + @Synchronized + fun resizeDisplay(width: Int, height: Int) { + require(width in 1..0xFFFF && height in 1..0xFFFF) { + "resizeDisplay dimensions must be in 1..65535, got ${width}x${height}" + } + output.writeByte(TYPE_RESIZE_DISPLAY) + output.writeShort(width) + output.writeShort(height) + output.flush() + } + private fun writePosition(x: Int, y: Int, screenWidth: Int, screenHeight: Int) { output.writeInt(x) output.writeInt(y) @@ -1725,9 +1806,10 @@ class Scrcpy( private const val CONNECT_RETRY_COUNT = 100 private const val CONNECT_RETRY_DELAY_MS = 100L private const val DEVICE_NAME_FIELD_LENGTH = 64 - private const val PACKET_FLAG_CONFIG = 1L shl 63 - private const val PACKET_FLAG_KEY_FRAME = 1L shl 62 - private const val PACKET_PTS_MASK = (1L shl 62) - 1 + private const val PACKET_FLAG_SESSION = 1L shl 63 + private const val PACKET_FLAG_CONFIG = 1L shl 62 + private const val PACKET_FLAG_KEY_FRAME = 1L shl 61 + private const val PACKET_PTS_MASK = (1L shl 61) - 1 private const val AUDIO_DISABLED = 0 private const val AUDIO_ERROR = 1 @@ -1741,10 +1823,29 @@ class Scrcpy( private const val TYPE_INJECT_TOUCH_EVENT = 2 private const val TYPE_INJECT_SCROLL_EVENT = 3 private const val TYPE_BACK_OR_SCREEN_ON = 4 + private const val TYPE_EXPAND_NOTIFICATION_PANEL = 5 + private const val TYPE_EXPAND_SETTINGS_PANEL = 6 + private const val TYPE_COLLAPSE_PANELS = 7 + private const val TYPE_GET_CLIPBOARD = 8 private const val TYPE_SET_CLIPBOARD = 9 private const val TYPE_SET_DISPLAY_POWER = 10 + private const val TYPE_ROTATE_DEVICE = 11 + private const val TYPE_UHID_CREATE = 12 + private const val TYPE_UHID_INPUT = 13 + private const val TYPE_UHID_DESTROY = 14 + private const val TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15 private const val TYPE_START_APP = 16 - private const val CLIPBOARD_SEQUENCE_INVALID = 0L + private const val TYPE_RESET_VIDEO = 17 + private const val TYPE_CAMERA_SET_TORCH = 18 + private const val TYPE_CAMERA_ZOOM_IN = 19 + private const val TYPE_CAMERA_ZOOM_OUT = 20 + private const val TYPE_RESIZE_DISPLAY = 21 + + private const val SEQUENCE_INVALID = 0L + + private const val COPY_KEY_NONE = 0 + private const val COPY_KEY_COPY = 1 + private const val COPY_KEY_CUT = 2 private fun socketNameFor(scid: Int): String { return "scrcpy_%08x".format(scid) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt index b79b5b2..f9897b5 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt @@ -14,84 +14,89 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource // 启动 scrcpy 前直接继承自 [ClientOptions], // 不需要默认值 data class ServerParams( - var scid: UInt, // (random uint32) & 0x7FFFFFFF + val scid: UInt, // (random uint32) & 0x7FFFFFFF - // var reqSerial: String, + // val reqSerial: String, - var logLevel: LogLevel, + val logLevel: LogLevel, - var videoCodec: Codec, - var audioCodec: Codec, - var videoSource: VideoSource, - var audioSource: AudioSource, + val videoCodec: Codec, + val audioCodec: Codec, + val videoSource: VideoSource, + val audioSource: AudioSource, - var cameraFacing: CameraFacing, + val cameraFacing: CameraFacing, - var crop: String, + val crop: String, - var videoCodecOptions: String, - var audioCodecOptions: String, - var videoEncoder: String, - var audioEncoder: String, + val videoCodecOptions: String, + val audioCodecOptions: String, + val videoEncoder: String, + val audioEncoder: String, - var cameraId: String, - var cameraSize: String, - var cameraAr: String, // aspect ratio - var cameraFps: UShort, + val cameraId: String, + val cameraSize: String, + val cameraAr: String, // aspect ratio + val cameraZoom: String, + val cameraFps: UShort, - // var portRange: PortRange, // sc_port_range(first, last) - // var tunnelHost: UInt, - // var tunnelPort: UShort, + // val portRange: PortRange, // sc_port_range(first, last) + // val tunnelHost: UInt, + // val tunnelPort: UShort, - var maxSize: UShort, + val maxSize: UShort, + val minSizeAlignment: UByte, - var videoBitRate: Int, - var audioBitRate: Int, + val videoBitRate: Int, + val audioBitRate: Int, - var maxFps: String, // float to be parsed by the server - var angle: String, // float to be parsed by the server + val maxFps: String, // float to be parsed by the server + val angle: String, // float to be parsed by the server - var screenOffTimeout: Tick, + val screenOffTimeout: Tick, - var captureOrientation: Orientation, - var captureOrientationLock: OrientationLock, + val captureOrientation: Orientation, + val captureOrientationLock: OrientationLock, - var control: Boolean, + val control: Boolean, - var displayId: Int, - var newDisplay: String, + val displayId: Int, + val newDisplay: String, - var displayImePolicy: DisplayImePolicy, + val displayImePolicy: DisplayImePolicy, - var video: Boolean, - var audio: Boolean, - var audioDup: Boolean, - var showTouches: Boolean, - var stayAwake: Boolean, + val video: Boolean, + val audio: Boolean, + val audioDup: Boolean, + val showTouches: Boolean, + val stayAwake: Boolean, - // var forceAdbForward: Boolean, + // val forceAdbForward: Boolean, - var powerOffOnClose: Boolean, - var legacyPaste: Boolean, - var clipboardAutosync: Boolean, + val powerOffOnClose: Boolean, + val legacyPaste: Boolean, + val clipboardAutosync: Boolean, - var downsizeOnError: Boolean, - // var tcpip: Boolean, - // var tcpipDst: String, - // var selectUsb: Boolean, - // var selectTcpip: Boolean, + val downsizeOnError: Boolean, + // val tcpip: Boolean, + // val tcpipDst: String, + // val selectUsb: Boolean, + // val selectTcpip: Boolean, - var cleanUp: Boolean, - var powerOn: Boolean, + val cleanUp: Boolean, + val powerOn: Boolean, - // var killAdbOnClose: Boolean, + // val killAdbOnClose: Boolean, - var cameraHighSpeed: Boolean, + val cameraHighSpeed: Boolean, + val cameraTorch: Boolean, - var vdDestroyContent: Boolean, - var vdSystemDecorations: Boolean, + val vdDestroyContent: Boolean, + val vdSystemDecorations: Boolean, + val keepActive: Boolean, + val flexDisplay: Boolean, - var list: ListOptions, + val list: ListOptions, ) { companion object { const val SEPARATOR: String = " " @@ -155,6 +160,9 @@ data class ServerParams( validate(maxFps) cmd.add("max_fps=${maxFps.trim()}") } + if (minSizeAlignment != 1.toUByte()) { + cmd.add("min_size_alignment=${minSizeAlignment}") + } if (angle.isNotBlank()) { validate(angle) cmd.add("angle=${angle.trim()}") @@ -207,6 +215,13 @@ data class ServerParams( if (cameraHighSpeed) { cmd.add("camera_high_speed=true") } + if (cameraTorch) { + cmd.add("camera_torch=true") + } + if (cameraZoom.isNotBlank()) { + validate(cameraZoom) + cmd.add("camera_zoom=${cameraZoom.trim()}") + } if (showTouches) { cmd.add("show_touches=true") } @@ -259,6 +274,9 @@ data class ServerParams( validate(newDisplay) cmd.add("new_display=${newDisplay.trim()}") } + if (flexDisplay) { + cmd.add("flex_display=true") + } if (displayImePolicy != DisplayImePolicy.UNDEFINED) { cmd.add("display_ime_policy=${displayImePolicy.string}") } @@ -268,6 +286,9 @@ data class ServerParams( if (!vdSystemDecorations) { cmd.add("vd_system_decorations=false") } + if (keepActive) { + cmd.add("keep_active=true") + } if (list has ListOptions.ENCODERS) { cmd.add("list_encoders=true") } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt index 4af691a..fbc6f1f 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt @@ -24,6 +24,7 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.runBlocking import kotlinx.parcelize.Parcelize +import org.json.JSONObject class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { companion object { @@ -75,6 +76,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { stringPreferencesKey("camera_ar"), "", ) + val CAMERA_ZOOM = Pair( + stringPreferencesKey("camera_zoom"), + "", + ) val CAMERA_FPS = Pair( intPreferencesKey("camera_fps"), 0, @@ -107,6 +112,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { stringPreferencesKey("camera_facing"), "any", ) + val MIN_SIZE_ALIGNMENT = Pair( + intPreferencesKey("min_size_alignment"), + 1, + ) val MAX_SIZE = Pair( intPreferencesKey("max_size"), 0, @@ -275,6 +284,18 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { booleanPreferencesKey("vd_system_decorations"), true, ) + val CAMERA_TORCH = Pair( + booleanPreferencesKey("camera_torch"), + false, + ) + val KEEP_ACTIVE = Pair( + booleanPreferencesKey("keep_active"), + false, + ) + val FLEX_DISPLAY = Pair( + booleanPreferencesKey("flex_display"), + false, + ) fun defaultBundle() = Bundle( crop = CROP.defaultValue, @@ -288,6 +309,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { cameraSizeCustom = CAMERA_SIZE_CUSTOM.defaultValue, cameraSizeUseCustom = CAMERA_SIZE_USE_CUSTOM.defaultValue, cameraAr = CAMERA_AR.defaultValue, + cameraZoom = CAMERA_ZOOM.defaultValue, cameraFps = CAMERA_FPS.defaultValue, logLevel = LOG_LEVEL.defaultValue, videoCodec = VIDEO_CODEC.defaultValue, @@ -296,6 +318,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { audioSource = AUDIO_SOURCE.defaultValue, recordFormat = RECORD_FORMAT.defaultValue, cameraFacing = CAMERA_FACING.defaultValue, + minSizeAlignment = MIN_SIZE_ALIGNMENT.defaultValue, maxSize = MAX_SIZE.defaultValue, videoBitRate = VIDEO_BIT_RATE.defaultValue, audioBitRate = AUDIO_BIT_RATE.defaultValue, @@ -338,6 +361,9 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { startAppUseCustom = START_APP_USE_CUSTOM.defaultValue, vdDestroyContent = VD_DESTROY_CONTENT.defaultValue, vdSystemDecorations = VD_SYSTEM_DECORATIONS.defaultValue, + cameraTorch = CAMERA_TORCH.defaultValue, + keepActive = KEEP_ACTIVE.defaultValue, + flexDisplay = FLEX_DISPLAY.defaultValue, ) } @@ -354,6 +380,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val cameraSizeCustom: String, val cameraSizeUseCustom: Boolean, val cameraAr: String, + val cameraZoom: String, val cameraFps: Int, val logLevel: String, val videoCodec: String, @@ -362,6 +389,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val audioSource: String, val recordFormat: String, val cameraFacing: String, + val minSizeAlignment: Int, val maxSize: Int, val videoBitRate: Int, val audioBitRate: Int, @@ -403,7 +431,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { val startAppCustom: String, val startAppUseCustom: Boolean, val vdDestroyContent: Boolean, - val vdSystemDecorations: Boolean + val vdSystemDecorations: Boolean, + val cameraTorch: Boolean, + val keepActive: Boolean, + val flexDisplay: Boolean, ) : Parcelable { } @@ -419,6 +450,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { bundleField(CAMERA_SIZE_CUSTOM) { it.cameraSizeCustom }, bundleField(CAMERA_SIZE_USE_CUSTOM) { it.cameraSizeUseCustom }, bundleField(CAMERA_AR) { it.cameraAr }, + bundleField(CAMERA_ZOOM) { it.cameraZoom }, bundleField(CAMERA_FPS) { it.cameraFps }, bundleField(LOG_LEVEL) { it.logLevel }, bundleField(VIDEO_CODEC) { it.videoCodec }, @@ -427,6 +459,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { bundleField(AUDIO_SOURCE) { it.audioSource }, bundleField(RECORD_FORMAT) { it.recordFormat }, bundleField(CAMERA_FACING) { it.cameraFacing }, + bundleField(MIN_SIZE_ALIGNMENT) { it.minSizeAlignment }, bundleField(MAX_SIZE) { it.maxSize }, bundleField(VIDEO_BIT_RATE) { it.videoBitRate }, bundleField(AUDIO_BIT_RATE) { it.audioBitRate }, @@ -468,6 +501,9 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { bundleField(START_APP_USE_CUSTOM) { it.startAppUseCustom }, bundleField(VD_DESTROY_CONTENT) { it.vdDestroyContent }, bundleField(VD_SYSTEM_DECORATIONS) { it.vdSystemDecorations }, + bundleField(CAMERA_TORCH) { it.cameraTorch }, + bundleField(KEEP_ACTIVE) { it.keepActive }, + bundleField(FLEX_DISPLAY) { it.flexDisplay }, ) val bundleState: StateFlow = createBundleState(::bundleFromPreferences) @@ -484,6 +520,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { cameraSizeCustom = preferences.read(CAMERA_SIZE_CUSTOM), cameraSizeUseCustom = preferences.read(CAMERA_SIZE_USE_CUSTOM), cameraAr = preferences.read(CAMERA_AR), + cameraZoom = preferences.read(CAMERA_ZOOM), cameraFps = preferences.read(CAMERA_FPS), logLevel = preferences.read(LOG_LEVEL), videoCodec = preferences.read(VIDEO_CODEC), @@ -492,6 +529,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { audioSource = preferences.read(AUDIO_SOURCE), recordFormat = preferences.read(RECORD_FORMAT), cameraFacing = preferences.read(CAMERA_FACING), + minSizeAlignment = preferences.read(MIN_SIZE_ALIGNMENT), maxSize = preferences.read(MAX_SIZE), videoBitRate = preferences.read(VIDEO_BIT_RATE), audioBitRate = preferences.read(AUDIO_BIT_RATE), @@ -534,6 +572,9 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { startAppUseCustom = preferences.read(START_APP_USE_CUSTOM), vdDestroyContent = preferences.read(VD_DESTROY_CONTENT), vdSystemDecorations = preferences.read(VD_SYSTEM_DECORATIONS), + cameraTorch = preferences.read(CAMERA_TORCH), + keepActive = preferences.read(KEEP_ACTIVE), + flexDisplay = preferences.read(FLEX_DISPLAY), ) suspend fun loadBundle() = loadBundle(::bundleFromPreferences) @@ -564,6 +605,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { cameraId = bundle.cameraId, cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom, cameraAr = bundle.cameraAr, + cameraZoom = bundle.cameraZoom, cameraFps = bundle.cameraFps.toUShort(), logLevel = LogLevel.fromString(bundle.logLevel), videoCodec = Codec.fromString(bundle.videoCodec), @@ -572,6 +614,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { audioSource = AudioSource.fromString(bundle.audioSource), recordFormat = RecordFormat.fromString(bundle.recordFormat), cameraFacing = CameraFacing.fromString(bundle.cameraFacing), + minSizeAlignment = bundle.minSizeAlignment.toUByte(), maxSize = bundle.maxSize.toUShort(), videoBitRate = bundle.videoBitRate, audioBitRate = bundle.audioBitRate, @@ -611,6 +654,360 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") { newDisplay = bundle.newDisplay, startApp = if (!bundle.startAppUseCustom) bundle.startApp else bundle.startAppCustom, vdDestroyContent = bundle.vdDestroyContent, - vdSystemDecorations = bundle.vdSystemDecorations + vdSystemDecorations = bundle.vdSystemDecorations, + cameraTorch = bundle.cameraTorch, + keepActive = bundle.keepActive, + flexDisplay = bundle.flexDisplay, ) } + +internal fun encodeBundleToJson(bundle: ScrcpyOptions.Bundle): JSONObject = + JSONObject() + .put("crop", bundle.crop) + .put("recordFilename", bundle.recordFilename) + .put("videoCodecOptions", bundle.videoCodecOptions) + .put("audioCodecOptions", bundle.audioCodecOptions) + .put("videoEncoder", bundle.videoEncoder) + .put("audioEncoder", bundle.audioEncoder) + .put("cameraId", bundle.cameraId) + .put("cameraSize", bundle.cameraSize) + .put("cameraSizeCustom", bundle.cameraSizeCustom) + .put("cameraSizeUseCustom", bundle.cameraSizeUseCustom) + .put("cameraAr", bundle.cameraAr) + .put("cameraZoom", bundle.cameraZoom) + .put("cameraFps", bundle.cameraFps) + .put("logLevel", bundle.logLevel) + .put("videoCodec", bundle.videoCodec) + .put("audioCodec", bundle.audioCodec) + .put("videoSource", bundle.videoSource) + .put("audioSource", bundle.audioSource) + .put("recordFormat", bundle.recordFormat) + .put("cameraFacing", bundle.cameraFacing) + .put("minSizeAlignment", bundle.minSizeAlignment) + .put("maxSize", bundle.maxSize) + .put("videoBitRate", bundle.videoBitRate) + .put("audioBitRate", bundle.audioBitRate) + .put("maxFps", bundle.maxFps) + .put("angle", bundle.angle) + .put("captureOrientation", bundle.captureOrientation) + .put("captureOrientationLock", bundle.captureOrientationLock) + .put("displayOrientation", bundle.displayOrientation) + .put("recordOrientation", bundle.recordOrientation) + .put("displayImePolicy", bundle.displayImePolicy) + .put("displayId", bundle.displayId) + .put("screenOffTimeout", bundle.screenOffTimeout) + .put("showTouches", bundle.showTouches) + .put("fullscreen", bundle.fullscreen) + .put("control", bundle.control) + .put("videoPlayback", bundle.videoPlayback) + .put("audioPlayback", bundle.audioPlayback) + .put("turnScreenOff", bundle.turnScreenOff) + .put("keyInjectMode", bundle.keyInjectMode) + .put("forwardKeyRepeat", bundle.forwardKeyRepeat) + .put("stayAwake", bundle.stayAwake) + .put("disableScreensaver", bundle.disableScreensaver) + .put("powerOffOnClose", bundle.powerOffOnClose) + .put("legacyPaste", bundle.legacyPaste) + .put("clipboardAutosync", bundle.clipboardAutosync) + .put("downsizeOnError", bundle.downsizeOnError) + .put("mouseHover", bundle.mouseHover) + .put("cleanup", bundle.cleanup) + .put("powerOn", bundle.powerOn) + .put("video", bundle.video) + .put("audio", bundle.audio) + .put("requireAudio", bundle.requireAudio) + .put("killAdbOnClose", bundle.killAdbOnClose) + .put("cameraHighSpeed", bundle.cameraHighSpeed) + .put("list", bundle.list) + .put("audioDup", bundle.audioDup) + .put("newDisplay", bundle.newDisplay) + .put("startApp", bundle.startApp) + .put("startAppCustom", bundle.startAppCustom) + .put("startAppUseCustom", bundle.startAppUseCustom) + .put("vdDestroyContent", bundle.vdDestroyContent) + .put("vdSystemDecorations", bundle.vdSystemDecorations) + .put("cameraTorch", bundle.cameraTorch) + .put("keepActive", bundle.keepActive) + .put("flexDisplay", bundle.flexDisplay) + +internal fun decodeBundleFromJson(bundleJson: JSONObject?): ScrcpyOptions.Bundle { + val json = bundleJson ?: return ScrcpyOptions.defaultBundle() + return ScrcpyOptions.defaultBundle().copy( + crop = json.optStringOrDefault( + "crop", + ScrcpyOptions.CROP.defaultValue, + ), + recordFilename = json.optStringOrDefault( + "recordFilename", + ScrcpyOptions.RECORD_FILENAME.defaultValue, + ), + videoCodecOptions = json.optStringOrDefault( + "videoCodecOptions", + ScrcpyOptions.VIDEO_CODEC_OPTIONS.defaultValue, + ), + audioCodecOptions = json.optStringOrDefault( + "audioCodecOptions", + ScrcpyOptions.AUDIO_CODEC_OPTIONS.defaultValue, + ), + videoEncoder = json.optStringOrDefault( + "videoEncoder", + ScrcpyOptions.VIDEO_ENCODER.defaultValue, + ), + audioEncoder = json.optStringOrDefault( + "audioEncoder", + ScrcpyOptions.AUDIO_ENCODER.defaultValue, + ), + cameraId = json.optStringOrDefault( + "cameraId", + ScrcpyOptions.CAMERA_ID.defaultValue, + ), + cameraSize = json.optStringOrDefault( + "cameraSize", + ScrcpyOptions.CAMERA_SIZE.defaultValue, + ), + cameraSizeCustom = json.optStringOrDefault( + "cameraSizeCustom", + ScrcpyOptions.CAMERA_SIZE_CUSTOM.defaultValue, + ), + cameraSizeUseCustom = json.optBooleanOrDefault( + "cameraSizeUseCustom", + ScrcpyOptions.CAMERA_SIZE_USE_CUSTOM.defaultValue, + ), + cameraAr = json.optStringOrDefault( + "cameraAr", + ScrcpyOptions.CAMERA_AR.defaultValue, + ), + cameraZoom = json.optStringOrDefault( + "cameraZoom", + ScrcpyOptions.CAMERA_ZOOM.defaultValue, + ), + cameraFps = json.optIntOrDefault( + "cameraFps", + ScrcpyOptions.CAMERA_FPS.defaultValue, + ), + logLevel = json.optStringOrDefault( + "logLevel", + ScrcpyOptions.LOG_LEVEL.defaultValue, + ), + videoCodec = json.optStringOrDefault( + "videoCodec", + ScrcpyOptions.VIDEO_CODEC.defaultValue, + ), + audioCodec = json.optStringOrDefault( + "audioCodec", + ScrcpyOptions.AUDIO_CODEC.defaultValue, + ), + videoSource = json.optStringOrDefault( + "videoSource", + ScrcpyOptions.VIDEO_SOURCE.defaultValue, + ), + audioSource = json.optStringOrDefault( + "audioSource", + ScrcpyOptions.AUDIO_SOURCE.defaultValue, + ), + recordFormat = json.optStringOrDefault( + "recordFormat", + ScrcpyOptions.RECORD_FORMAT.defaultValue, + ), + cameraFacing = json.optStringOrDefault( + "cameraFacing", + ScrcpyOptions.CAMERA_FACING.defaultValue, + ), + minSizeAlignment = json.optIntOrDefault( + "minSizeAlignment", + ScrcpyOptions.MIN_SIZE_ALIGNMENT.defaultValue, + ), + maxSize = json.optIntOrDefault( + "maxSize", + ScrcpyOptions.MAX_SIZE.defaultValue, + ), + videoBitRate = json.optIntOrDefault( + "videoBitRate", + ScrcpyOptions.VIDEO_BIT_RATE.defaultValue, + ), + audioBitRate = json.optIntOrDefault( + "audioBitRate", + ScrcpyOptions.AUDIO_BIT_RATE.defaultValue, + ), + maxFps = json.optStringOrDefault( + "maxFps", + ScrcpyOptions.MAX_FPS.defaultValue, + ), + angle = json.optStringOrDefault( + "angle", + ScrcpyOptions.ANGLE.defaultValue, + ), + captureOrientation = json.optIntOrDefault( + "captureOrientation", + ScrcpyOptions.CAPTURE_ORIENTATION.defaultValue, + ), + captureOrientationLock = json.optStringOrDefault( + "captureOrientationLock", + ScrcpyOptions.CAPTURE_ORIENTATION_LOCK.defaultValue, + ), + displayOrientation = json.optIntOrDefault( + "displayOrientation", + ScrcpyOptions.DISPLAY_ORIENTATION.defaultValue, + ), + recordOrientation = json.optIntOrDefault( + "recordOrientation", + ScrcpyOptions.RECORD_ORIENTATION.defaultValue, + ), + displayImePolicy = json.optStringOrDefault( + "displayImePolicy", + ScrcpyOptions.DISPLAY_IME_POLICY.defaultValue, + ), + displayId = json.optIntOrDefault( + "displayId", + ScrcpyOptions.DISPLAY_ID.defaultValue, + ), + screenOffTimeout = json.optLongOrDefault( + "screenOffTimeout", + ScrcpyOptions.SCREEN_OFF_TIMEOUT.defaultValue, + ), + showTouches = json.optBooleanOrDefault( + "showTouches", + ScrcpyOptions.SHOW_TOUCHES.defaultValue, + ), + fullscreen = json.optBooleanOrDefault( + "fullscreen", + ScrcpyOptions.FULLSCREEN.defaultValue, + ), + control = json.optBooleanOrDefault( + "control", + ScrcpyOptions.CONTROL.defaultValue, + ), + videoPlayback = json.optBooleanOrDefault( + "videoPlayback", + ScrcpyOptions.VIDEO_PLAYBACK.defaultValue, + ), + audioPlayback = json.optBooleanOrDefault( + "audioPlayback", + ScrcpyOptions.AUDIO_PLAYBACK.defaultValue, + ), + turnScreenOff = json.optBooleanOrDefault( + "turnScreenOff", + ScrcpyOptions.TURN_SCREEN_OFF.defaultValue, + ), + keyInjectMode = json.optStringOrDefault( + "keyInjectMode", + ScrcpyOptions.KEY_INJECT_MODE.defaultValue, + ), + forwardKeyRepeat = json.optBooleanOrDefault( + "forwardKeyRepeat", + ScrcpyOptions.FORWARD_KEY_REPEAT.defaultValue, + ), + stayAwake = json.optBooleanOrDefault( + "stayAwake", + ScrcpyOptions.STAY_AWAKE.defaultValue, + ), + disableScreensaver = json.optBooleanOrDefault( + "disableScreensaver", + ScrcpyOptions.DISABLE_SCREENSAVER.defaultValue, + ), + powerOffOnClose = json.optBooleanOrDefault( + "powerOffOnClose", + ScrcpyOptions.POWER_OFF_ON_CLOSE.defaultValue, + ), + legacyPaste = json.optBooleanOrDefault( + "legacyPaste", + ScrcpyOptions.LEGACY_PASTE.defaultValue, + ), + clipboardAutosync = json.optBooleanOrDefault( + "clipboardAutosync", + ScrcpyOptions.CLIPBOARD_AUTOSYNC.defaultValue, + ), + downsizeOnError = json.optBooleanOrDefault( + "downsizeOnError", + ScrcpyOptions.DOWNSIZE_ON_ERROR.defaultValue, + ), + mouseHover = json.optBooleanOrDefault( + "mouseHover", + ScrcpyOptions.MOUSE_HOVER.defaultValue, + ), + cleanup = json.optBooleanOrDefault( + "cleanup", + ScrcpyOptions.CLEANUP.defaultValue, + ), + powerOn = json.optBooleanOrDefault( + "powerOn", + ScrcpyOptions.POWER_ON.defaultValue, + ), + video = json.optBooleanOrDefault( + "video", + ScrcpyOptions.VIDEO.defaultValue, + ), + audio = json.optBooleanOrDefault( + "audio", + ScrcpyOptions.AUDIO.defaultValue, + ), + requireAudio = json.optBooleanOrDefault( + "requireAudio", + ScrcpyOptions.REQUIRE_AUDIO.defaultValue, + ), + killAdbOnClose = json.optBooleanOrDefault( + "killAdbOnClose", + ScrcpyOptions.KILL_ADB_ON_CLOSE.defaultValue, + ), + cameraHighSpeed = json.optBooleanOrDefault( + "cameraHighSpeed", + ScrcpyOptions.CAMERA_HIGH_SPEED.defaultValue, + ), + list = json.optStringOrDefault( + "list", + ScrcpyOptions.LIST.defaultValue, + ), + audioDup = json.optBooleanOrDefault( + "audioDup", + ScrcpyOptions.AUDIO_DUP.defaultValue, + ), + newDisplay = json.optStringOrDefault( + "newDisplay", + ScrcpyOptions.NEW_DISPLAY.defaultValue, + ), + startApp = json.optStringOrDefault( + "startApp", + ScrcpyOptions.START_APP.defaultValue, + ), + startAppCustom = json.optStringOrDefault( + "startAppCustom", + ScrcpyOptions.START_APP_CUSTOM.defaultValue, + ), + startAppUseCustom = json.optBooleanOrDefault( + "startAppUseCustom", + ScrcpyOptions.START_APP_USE_CUSTOM.defaultValue, + ), + vdDestroyContent = json.optBooleanOrDefault( + "vdDestroyContent", + ScrcpyOptions.VD_DESTROY_CONTENT.defaultValue, + ), + vdSystemDecorations = json.optBooleanOrDefault( + "vdSystemDecorations", + ScrcpyOptions.VD_SYSTEM_DECORATIONS.defaultValue, + ), + cameraTorch = json.optBooleanOrDefault( + "cameraTorch", + ScrcpyOptions.CAMERA_TORCH.defaultValue, + ), + keepActive = json.optBooleanOrDefault( + "keepActive", + ScrcpyOptions.KEEP_ACTIVE.defaultValue, + ), + flexDisplay = json.optBooleanOrDefault( + "flexDisplay", + ScrcpyOptions.FLEX_DISPLAY.defaultValue, + ), + ) +} + +private fun JSONObject.optStringOrDefault(key: String, defaultValue: String): String = + if (has(key) && !isNull(key)) optString(key, defaultValue) else defaultValue + +private fun JSONObject.optBooleanOrDefault(key: String, defaultValue: Boolean): Boolean = + if (has(key) && !isNull(key)) optBoolean(key, defaultValue) else defaultValue + +private fun JSONObject.optIntOrDefault(key: String, defaultValue: Int): Int = + if (has(key) && !isNull(key)) optInt(key, defaultValue) else defaultValue + +private fun JSONObject.optLongOrDefault(key: String, defaultValue: Long): Long = + if (has(key) && !isNull(key)) optLong(key, defaultValue) else defaultValue diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyProfiles.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyProfiles.kt index 445aedc..50dcbe3 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyProfiles.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyProfiles.kt @@ -213,7 +213,7 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") { JSONObject() .put("id", profile.id) .put("name", profile.name) - .put("bundle", encodeBundle(profile.bundle)) + .put("bundle", encodeBundleToJson(profile.bundle)) ) } return array.toString() @@ -234,7 +234,7 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") { Profile( id = id, name = name.ifBlank { textNewProfile }, - bundle = decodeBundle(item.optJSONObject("bundle")), + bundle = decodeBundleFromJson(item.optJSONObject("bundle")), isBuiltinGlobal = id == ScrcpyOptions.GLOBAL_PROFILE_ID, ) ) @@ -242,331 +242,4 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") { } return State(profiles) } - - private fun encodeBundle(bundle: ScrcpyOptions.Bundle): JSONObject = - JSONObject() - .put("crop", bundle.crop) - .put("recordFilename", bundle.recordFilename) - .put("videoCodecOptions", bundle.videoCodecOptions) - .put("audioCodecOptions", bundle.audioCodecOptions) - .put("videoEncoder", bundle.videoEncoder) - .put("audioEncoder", bundle.audioEncoder) - .put("cameraId", bundle.cameraId) - .put("cameraSize", bundle.cameraSize) - .put("cameraSizeCustom", bundle.cameraSizeCustom) - .put("cameraSizeUseCustom", bundle.cameraSizeUseCustom) - .put("cameraAr", bundle.cameraAr) - .put("cameraFps", bundle.cameraFps) - .put("logLevel", bundle.logLevel) - .put("videoCodec", bundle.videoCodec) - .put("audioCodec", bundle.audioCodec) - .put("videoSource", bundle.videoSource) - .put("audioSource", bundle.audioSource) - .put("recordFormat", bundle.recordFormat) - .put("cameraFacing", bundle.cameraFacing) - .put("maxSize", bundle.maxSize) - .put("videoBitRate", bundle.videoBitRate) - .put("audioBitRate", bundle.audioBitRate) - .put("maxFps", bundle.maxFps) - .put("angle", bundle.angle) - .put("captureOrientation", bundle.captureOrientation) - .put("captureOrientationLock", bundle.captureOrientationLock) - .put("displayOrientation", bundle.displayOrientation) - .put("recordOrientation", bundle.recordOrientation) - .put("displayImePolicy", bundle.displayImePolicy) - .put("displayId", bundle.displayId) - .put("screenOffTimeout", bundle.screenOffTimeout) - .put("showTouches", bundle.showTouches) - .put("fullscreen", bundle.fullscreen) - .put("control", bundle.control) - .put("videoPlayback", bundle.videoPlayback) - .put("audioPlayback", bundle.audioPlayback) - .put("turnScreenOff", bundle.turnScreenOff) - .put("keyInjectMode", bundle.keyInjectMode) - .put("forwardKeyRepeat", bundle.forwardKeyRepeat) - .put("stayAwake", bundle.stayAwake) - .put("disableScreensaver", bundle.disableScreensaver) - .put("powerOffOnClose", bundle.powerOffOnClose) - .put("legacyPaste", bundle.legacyPaste) - .put("clipboardAutosync", bundle.clipboardAutosync) - .put("downsizeOnError", bundle.downsizeOnError) - .put("mouseHover", bundle.mouseHover) - .put("cleanup", bundle.cleanup) - .put("powerOn", bundle.powerOn) - .put("video", bundle.video) - .put("audio", bundle.audio) - .put("requireAudio", bundle.requireAudio) - .put("killAdbOnClose", bundle.killAdbOnClose) - .put("cameraHighSpeed", bundle.cameraHighSpeed) - .put("list", bundle.list) - .put("audioDup", bundle.audioDup) - .put("newDisplay", bundle.newDisplay) - .put("startApp", bundle.startApp) - .put("startAppCustom", bundle.startAppCustom) - .put("startAppUseCustom", bundle.startAppUseCustom) - .put("vdDestroyContent", bundle.vdDestroyContent) - .put("vdSystemDecorations", bundle.vdSystemDecorations) - - private fun decodeBundle(bundleJson: JSONObject?): ScrcpyOptions.Bundle { - val defaults = ScrcpyOptions.defaultBundle() - val json = bundleJson ?: return defaults - return defaults.copy( - crop = json.optStringOrDefault( - "crop", - defaults.crop, - ), - recordFilename = json.optStringOrDefault( - "recordFilename", - defaults.recordFilename, - ), - videoCodecOptions = json.optStringOrDefault( - "videoCodecOptions", - defaults.videoCodecOptions, - ), - audioCodecOptions = json.optStringOrDefault( - "audioCodecOptions", - defaults.audioCodecOptions, - ), - videoEncoder = json.optStringOrDefault( - "videoEncoder", - defaults.videoEncoder, - ), - audioEncoder = json.optStringOrDefault( - "audioEncoder", - defaults.audioEncoder, - ), - cameraId = json.optStringOrDefault( - "cameraId", - defaults.cameraId, - ), - cameraSize = json.optStringOrDefault( - "cameraSize", - defaults.cameraSize, - ), - cameraSizeCustom = json.optStringOrDefault( - "cameraSizeCustom", - defaults.cameraSizeCustom, - ), - cameraSizeUseCustom = json.optBooleanOrDefault( - "cameraSizeUseCustom", - defaults.cameraSizeUseCustom, - ), - cameraAr = json.optStringOrDefault( - "cameraAr", - defaults.cameraAr, - ), - cameraFps = json.optIntOrDefault( - "cameraFps", - defaults.cameraFps, - ), - logLevel = json.optStringOrDefault( - "logLevel", - defaults.logLevel, - ), - videoCodec = json.optStringOrDefault( - "videoCodec", - defaults.videoCodec, - ), - audioCodec = json.optStringOrDefault( - "audioCodec", - defaults.audioCodec, - ), - videoSource = json.optStringOrDefault( - "videoSource", - defaults.videoSource, - ), - audioSource = json.optStringOrDefault( - "audioSource", - defaults.audioSource, - ), - recordFormat = json.optStringOrDefault( - "recordFormat", - defaults.recordFormat, - ), - cameraFacing = json.optStringOrDefault( - "cameraFacing", - defaults.cameraFacing, - ), - maxSize = json.optIntOrDefault( - "maxSize", - defaults.maxSize, - ), - videoBitRate = json.optIntOrDefault( - "videoBitRate", - defaults.videoBitRate, - ), - audioBitRate = json.optIntOrDefault( - "audioBitRate", - defaults.audioBitRate, - ), - maxFps = json.optStringOrDefault( - "maxFps", - defaults.maxFps, - ), - angle = json.optStringOrDefault( - "angle", - defaults.angle, - ), - captureOrientation = json.optIntOrDefault( - "captureOrientation", - defaults.captureOrientation, - ), - captureOrientationLock = json.optStringOrDefault( - "captureOrientationLock", - defaults.captureOrientationLock, - ), - displayOrientation = json.optIntOrDefault( - "displayOrientation", - defaults.displayOrientation, - ), - recordOrientation = json.optIntOrDefault( - "recordOrientation", - defaults.recordOrientation, - ), - displayImePolicy = json.optStringOrDefault( - "displayImePolicy", - defaults.displayImePolicy, - ), - displayId = json.optIntOrDefault( - "displayId", - defaults.displayId, - ), - screenOffTimeout = json.optLongOrDefault( - "screenOffTimeout", - defaults.screenOffTimeout, - ), - showTouches = json.optBooleanOrDefault( - "showTouches", - defaults.showTouches, - ), - fullscreen = json.optBooleanOrDefault( - "fullscreen", - defaults.fullscreen, - ), - control = json.optBooleanOrDefault( - "control", - defaults.control, - ), - videoPlayback = json.optBooleanOrDefault( - "videoPlayback", - defaults.videoPlayback, - ), - audioPlayback = json.optBooleanOrDefault( - "audioPlayback", - defaults.audioPlayback, - ), - turnScreenOff = json.optBooleanOrDefault( - "turnScreenOff", - defaults.turnScreenOff, - ), - keyInjectMode = json.optStringOrDefault( - "keyInjectMode", - defaults.keyInjectMode, - ), - forwardKeyRepeat = json.optBooleanOrDefault( - "forwardKeyRepeat", - defaults.forwardKeyRepeat, - ), - stayAwake = json.optBooleanOrDefault( - "stayAwake", - defaults.stayAwake, - ), - disableScreensaver = json.optBooleanOrDefault( - "disableScreensaver", - defaults.disableScreensaver, - ), - powerOffOnClose = json.optBooleanOrDefault( - "powerOffOnClose", - defaults.powerOffOnClose, - ), - legacyPaste = json.optBooleanOrDefault( - "legacyPaste", - defaults.legacyPaste, - ), - clipboardAutosync = json.optBooleanOrDefault( - "clipboardAutosync", - defaults.clipboardAutosync, - ), - downsizeOnError = json.optBooleanOrDefault( - "downsizeOnError", - defaults.downsizeOnError, - ), - mouseHover = json.optBooleanOrDefault( - "mouseHover", - defaults.mouseHover, - ), - cleanup = json.optBooleanOrDefault( - "cleanup", - defaults.cleanup, - ), - powerOn = json.optBooleanOrDefault( - "powerOn", - defaults.powerOn, - ), - video = json.optBooleanOrDefault( - "video", - defaults.video, - ), - audio = json.optBooleanOrDefault( - "audio", - defaults.audio, - ), - requireAudio = json.optBooleanOrDefault( - "requireAudio", - defaults.requireAudio, - ), - killAdbOnClose = json.optBooleanOrDefault( - "killAdbOnClose", - defaults.killAdbOnClose, - ), - cameraHighSpeed = json.optBooleanOrDefault( - "cameraHighSpeed", - defaults.cameraHighSpeed, - ), - list = json.optStringOrDefault( - "list", - defaults.list, - ), - audioDup = json.optBooleanOrDefault( - "audioDup", - defaults.audioDup, - ), - newDisplay = json.optStringOrDefault( - "newDisplay", - defaults.newDisplay, - ), - startApp = json.optStringOrDefault( - "startApp", - defaults.startApp, - ), - startAppCustom = json.optStringOrDefault( - "startAppCustom", - defaults.startAppCustom, - ), - startAppUseCustom = json.optBooleanOrDefault( - "startAppUseCustom", - defaults.startAppUseCustom, - ), - vdDestroyContent = json.optBooleanOrDefault( - "vdDestroyContent", - defaults.vdDestroyContent, - ), - vdSystemDecorations = json.optBooleanOrDefault( - "vdSystemDecorations", - defaults.vdSystemDecorations, - ), - ) - } - - private fun JSONObject.optStringOrDefault(key: String, defaultValue: String): String = - if (has(key) && !isNull(key)) optString(key, defaultValue) else defaultValue - - private fun JSONObject.optBooleanOrDefault(key: String, defaultValue: Boolean): Boolean = - if (has(key) && !isNull(key)) optBoolean(key, defaultValue) else defaultValue - - private fun JSONObject.optIntOrDefault(key: String, defaultValue: Int): Int = - if (has(key) && !isNull(key)) optInt(key, defaultValue) else defaultValue - - private fun JSONObject.optLongOrDefault(key: String, defaultValue: Long): Long = - if (has(key) && !isNull(key)) optLong(key, defaultValue) else defaultValue -} +} \ No newline at end of file diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/util/Debouncer.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/util/Debouncer.kt new file mode 100644 index 0000000..bb3122b --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/util/Debouncer.kt @@ -0,0 +1,35 @@ +package io.github.miuzarte.scrcpyforandroid.util + +import android.os.Handler +import android.os.Looper +import kotlin.math.roundToInt + +/** + * Simple debouncer that delays execution of [block] until [delayMs] + * milliseconds have elapsed since the last call to [invoke]. + * + * All calls happen on the main thread via [Handler]. + */ +class Debouncer( + private val delayMs: Long, + private val block: (Int, Int) -> Unit, +) { + private val handler = Handler(Looper.getMainLooper()) + private var pending: Runnable? = null + + fun invoke(width: Float, height: Float) { + invoke(width.roundToInt(), height.roundToInt()) + } + + fun invoke(width: Int, height: Int) { + pending?.let { handler.removeCallbacks(it) } + val task = Runnable { block(width, height) } + pending = task + handler.postDelayed(task, delayMs) + } + + fun cancel() { + pending?.let { handler.removeCallbacks(it) } + pending = null + } +} diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 95b991f..7c19ba3 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -474,6 +474,7 @@ scrcpy 启动时不唤醒屏幕 scrcpy 结束后息屏 scrcpy 启动后保持设备唤醒状态(插入电源) + 模拟用户活动防止息屏 显示物理触控 启动后进入全屏 scrcpy 启动后保持本机屏幕常亮 @@ -485,6 +486,8 @@ 视频码率 视频来源 监视器 ID + 最小尺寸对齐 + 对齐值必须是 2 的次方 (1, 2, 4, 8, 16) 最大分辨率 最大帧率 摄像头 ID @@ -492,6 +495,7 @@ 摄像头分辨率 摄像头帧率 高帧率模式 + 相机手电筒 音频来源 音频双路输出 音频转发失败时终止 @@ -509,6 +513,7 @@ 禁用鼠标悬停转发 禁用结束后清理 默认情况下,scrcpy 会从设备中移除服务器二进制文件,并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)\n此选项将禁用此清理操作 + 自动调整尺寸以匹配界面 scrcpy 启动后打开应用 本机 日志等级 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ed3431..a3c2076 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -474,6 +474,7 @@ Do not power on the device on start Turn the device screen off when closing scrcpy Keep the device on while scrcpy is running, when the device is plugged in + Simulate user activity to keep the screen on Enable \"show touches\" on start, restore the initial value on exit Start in fullscreen Disable screensaver while scrcpy is running @@ -485,6 +486,8 @@ Video bitrate Video source Display ID + Min size alignment + Alignment must be a power of 2 (1, 2, 4, 8, or 16) Max resolution Max framerate Camera ID @@ -492,6 +495,7 @@ Camera resolution Camera framerate High framerate mode + Camera torch Audio source Duplicate audio (capture and keep playing on the device) Terminate when audio forwarding fails @@ -509,6 +513,7 @@ Do not forward mouse hover events Disable cleanup on exit By default, scrcpy removes the server binary from the device and restores device state on exit (show touches, stay awake, and power mode).\nThis option disables this cleanup operation. + Automatically resize to match the screen Open app after scrcpy starts Native Log level