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`
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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*
|
||||
|
||||
39
TODO.md
39
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~~
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
0
app/src/main/assets/bin/.gitkeep
Normal file
0
app/src/main/assets/bin/.gitkeep
Normal file
Binary file not shown.
@@ -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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -35,28 +35,8 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function for Int presets to find index from Int value.
|
||||
*/
|
||||
fun Preset<Int>.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<Int>.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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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<Bundle> = 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -474,6 +474,7 @@
|
||||
<string name="scrcpyopt_no_power_on">scrcpy 启动时不唤醒屏幕</string>
|
||||
<string name="scrcpyopt_power_off_on_close">scrcpy 结束后息屏</string>
|
||||
<string name="scrcpyopt_stay_awake">scrcpy 启动后保持设备唤醒状态(插入电源)</string>
|
||||
<string name="scrcpyopt_keep_active">模拟用户活动防止息屏</string>
|
||||
<string name="scrcpyopt_show_touches">显示物理触控</string>
|
||||
<string name="scrcpyopt_fullscreen">启动后进入全屏</string>
|
||||
<string name="scrcpyopt_disable_screensaver">scrcpy 启动后保持本机屏幕常亮</string>
|
||||
@@ -485,6 +486,8 @@
|
||||
<string name="scrcpyopt_video_bitrate">视频码率</string>
|
||||
<string name="scrcpyopt_video_source">视频来源</string>
|
||||
<string name="scrcpyopt_display_id">监视器 ID</string>
|
||||
<string name="scrcpyopt_min_size_alignment">最小尺寸对齐</string>
|
||||
<string name="scrcpyopt_min_size_alignment_invalid">对齐值必须是 2 的次方 (1, 2, 4, 8, 16)</string>
|
||||
<string name="scrcpyopt_max_size">最大分辨率</string>
|
||||
<string name="scrcpyopt_max_fps">最大帧率</string>
|
||||
<string name="scrcpyopt_camera_id">摄像头 ID</string>
|
||||
@@ -492,6 +495,7 @@
|
||||
<string name="scrcpyopt_camera_size">摄像头分辨率</string>
|
||||
<string name="scrcpyopt_camera_fps">摄像头帧率</string>
|
||||
<string name="scrcpyopt_camera_high_speed">高帧率模式</string>
|
||||
<string name="scrcpyopt_camera_torch">相机手电筒</string>
|
||||
<string name="scrcpyopt_audio_source">音频来源</string>
|
||||
<string name="scrcpyopt_audio_dup">音频双路输出</string>
|
||||
<string name="scrcpyopt_require_audio">音频转发失败时终止</string>
|
||||
@@ -509,6 +513,7 @@
|
||||
<string name="scrcpyopt_no_mouse_hover">禁用鼠标悬停转发</string>
|
||||
<string name="scrcpyopt_no_cleanup">禁用结束后清理</string>
|
||||
<string name="scrcpyopt_no_cleanup_desc">默认情况下,scrcpy 会从设备中移除服务器二进制文件,并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)\n此选项将禁用此清理操作</string>
|
||||
<string name="scrcpyopt_flex_display">自动调整尺寸以匹配界面</string>
|
||||
<string name="scrcpyopt_start_app">scrcpy 启动后打开应用</string>
|
||||
<string name="scrcpyopt_native">本机</string>
|
||||
<string name="scrcpyopt_log_level">日志等级</string>
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
<string name="scrcpyopt_no_power_on">Do not power on the device on start</string>
|
||||
<string name="scrcpyopt_power_off_on_close">Turn the device screen off when closing scrcpy</string>
|
||||
<string name="scrcpyopt_stay_awake">Keep the device on while scrcpy is running, when the device is plugged in</string>
|
||||
<string name="scrcpyopt_keep_active">Simulate user activity to keep the screen on</string>
|
||||
<string name="scrcpyopt_show_touches">Enable \"show touches\" on start, restore the initial value on exit</string>
|
||||
<string name="scrcpyopt_fullscreen">Start in fullscreen</string>
|
||||
<string name="scrcpyopt_disable_screensaver">Disable screensaver while scrcpy is running</string>
|
||||
@@ -485,6 +486,8 @@
|
||||
<string name="scrcpyopt_video_bitrate">Video bitrate</string>
|
||||
<string name="scrcpyopt_video_source">Video source</string>
|
||||
<string name="scrcpyopt_display_id">Display ID</string>
|
||||
<string name="scrcpyopt_min_size_alignment">Min size alignment</string>
|
||||
<string name="scrcpyopt_min_size_alignment_invalid">Alignment must be a power of 2 (1, 2, 4, 8, or 16)</string>
|
||||
<string name="scrcpyopt_max_size">Max resolution</string>
|
||||
<string name="scrcpyopt_max_fps">Max framerate</string>
|
||||
<string name="scrcpyopt_camera_id">Camera ID</string>
|
||||
@@ -492,6 +495,7 @@
|
||||
<string name="scrcpyopt_camera_size">Camera resolution</string>
|
||||
<string name="scrcpyopt_camera_fps">Camera framerate</string>
|
||||
<string name="scrcpyopt_camera_high_speed">High framerate mode</string>
|
||||
<string name="scrcpyopt_camera_torch">Camera torch</string>
|
||||
<string name="scrcpyopt_audio_source">Audio source</string>
|
||||
<string name="scrcpyopt_audio_dup">Duplicate audio (capture and keep playing on the device)</string>
|
||||
<string name="scrcpyopt_require_audio">Terminate when audio forwarding fails</string>
|
||||
@@ -509,6 +513,7 @@
|
||||
<string name="scrcpyopt_no_mouse_hover">Do not forward mouse hover events</string>
|
||||
<string name="scrcpyopt_no_cleanup">Disable cleanup on exit</string>
|
||||
<string name="scrcpyopt_no_cleanup_desc">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.</string>
|
||||
<string name="scrcpyopt_flex_display">Automatically resize to match the screen</string>
|
||||
<string name="scrcpyopt_start_app">Open app after scrcpy starts</string>
|
||||
<string name="scrcpyopt_native">Native</string>
|
||||
<string name="scrcpyopt_log_level">Log level</string>
|
||||
|
||||
Reference in New Issue
Block a user