improve: implement more scrcpy options

This commit is contained in:
Miuzarte
2026-04-11 23:38:27 +08:00
parent 2639da4b2c
commit 30e4c0679b
10 changed files with 721 additions and 179 deletions

2
.gitignore vendored
View File

@@ -34,3 +34,5 @@ google-services.json
*.hprof *.hprof
.kiro/ .kiro/
scrcpy.help*

View File

@@ -59,6 +59,7 @@ fun Preset<Int>.indexOfOrNearest(raw: String): Int {
object ScrcpyPresets { object ScrcpyPresets {
val MaxSize = Preset(listOf(0, 720, 1080, 1280, 1600, 1920, 2160, 2560, 3200, 3840)) // px 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 MaxFPS = Preset(listOf(0, 24, 30, 45, 60, 90, 120)) // fps
val ScreenOffTimeout = Preset(listOf(0, 15, 30, 60, 120, 300, 600)) // sec
val AudioBitRate = Preset(listOf(0, 32, 64, 96, 128, 160, 192, 256, 320, 384, 512)) // Kbps val AudioBitRate = Preset(listOf(0, 32, 64, 96, 128, 160, 192, 256, 320, 384, 512)) // Kbps
val CameraFps = Preset(listOf(0, 10, 15, 24, 30, 60, 120, 240, 480, 960)) // fps val CameraFps = Preset(listOf(0, 10, 15, 24, 30, 60, 120, 240, 480, 960)) // fps
} }

View File

@@ -133,6 +133,41 @@ object NativeAdbService {
response response
} }
suspend fun startApp(
packageName: String,
displayId: Int? = null,
forceStop: Boolean = false,
): String = mutex.withLock {
val normalizedPackageName = packageName.trim()
require(normalizedPackageName.isNotBlank()) { "package name is blank" }
if (forceStop) {
requireConnection().shell(
"am force-stop ${quoteShellArg(normalizedPackageName)}"
)
}
val resolveOutput = requireConnection().shell(
"cmd package resolve-activity --brief ${quoteShellArg(normalizedPackageName)}"
)
val componentName = resolveOutput
.lineSequence()
.map(String::trim)
.lastOrNull { '/' in it }
?: throw IllegalStateException(
"Cannot resolve launch activity for $normalizedPackageName"
)
val displayArg = displayId
?.takeIf { it >= 0 }
?.let { " --display $it" }
.orEmpty()
val command = "am start-activity$displayArg -n ${quoteShellArg(componentName)}"
val response = requireConnection().shell(command)
Log.d(TAG, "startApp(): package=$normalizedPackageName component=$componentName")
response
}
suspend fun openShellStream(command: String): AdbSocketStream = mutex.withLock { suspend fun openShellStream(command: String): AdbSocketStream = mutex.withLock {
requireConnection().openStream("shell:$command") requireConnection().openStream("shell:$command")
} }
@@ -161,5 +196,9 @@ object NativeAdbService {
?: throw IllegalStateException("ADB not connected") ?: throw IllegalStateException("ADB not connected")
} }
private fun quoteShellArg(value: String): String {
return "'" + value.replace("'", "'\\''") + "'"
}
private const val TAG = "NativeAdbService" private const val TAG = "NativeAdbService"
} }

View File

@@ -36,6 +36,7 @@ import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks
import io.github.miuzarte.scrcpyforandroid.services.EventLogger import io.github.miuzarte.scrcpyforandroid.services.EventLogger
import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent import io.github.miuzarte.scrcpyforandroid.services.EventLogger.logEvent
@@ -89,6 +90,13 @@ private const val ADB_TCP_PROBE_TIMEOUT_MS = 500
private const val PREVIEW_CARD_ITEM_KEY = "preview_card" private const val PREVIEW_CARD_ITEM_KEY = "preview_card"
private const val PREVIEW_CARD_ITEM_INDEX = 3 private const val PREVIEW_CARD_ITEM_INDEX = 3
private data class StartAppRequest(
val packageName: String,
val displayId: Int?,
val forceStop: Boolean,
val matchedAppLabel: String? = null,
)
@Composable @Composable
fun DeviceTabScreen( fun DeviceTabScreen(
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
@@ -517,11 +525,102 @@ fun DeviceTabPage(
} }
} }
suspend fun resolveStartAppRequest(
scrcpy: Scrcpy,
options: io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions,
): StartAppRequest? {
val raw = options.startApp.trim()
if (raw.isBlank()) {
return null
}
var query = raw
val forceStop = query.startsWith("+")
if (forceStop) {
query = query.drop(1).trimStart()
}
require(query.isNotBlank()) { "应用名或包名不能为空" }
val displayId = when {
options.videoSource != VideoSource.DISPLAY -> null
options.displayId >= 0 -> options.displayId
else -> null
}
if (!query.startsWith("?")) {
return StartAppRequest(
packageName = query,
displayId = displayId,
forceStop = forceStop,
)
}
val searchName = query.drop(1).trim()
require(searchName.isNotBlank()) { "应用名称不能为空" }
val apps = scrcpy.listings.getApps(forceRefresh = false)
val matches = apps.filter { it.label.startsWith(searchName, ignoreCase = true) }
require(matches.isNotEmpty()) { "未找到应用名以 \"$searchName\" 开头的应用" }
require(matches.size == 1) {
"按名称匹配到多个应用: " +
matches.take(5).joinToString { "${it.label} (${it.packageName})" }
}
return StartAppRequest(
packageName = matches[0].packageName,
displayId = displayId,
forceStop = forceStop,
matchedAppLabel = matches[0].label,
)
}
suspend fun startScrcpySession(openFullscreen: Boolean = false) { suspend fun startScrcpySession(openFullscreen: Boolean = false) {
val options = scrcpyOptions.toClientOptions(soBundleShared).fix() val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
val session = scrcpy.start(options) val session = scrcpy.start(options)
pendingScrollToPreview = true pendingScrollToPreview = true
if (openFullscreen) withContext(Dispatchers.Main) { val startAppRequest = runCatching {
resolveStartAppRequest(scrcpy, options)
}.getOrElse { error ->
logEvent(
"启动应用请求无效: ${
error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName
}",
Log.WARN,
error,
)
null
}
startAppRequest?.let { request ->
if (options.newDisplay.isNotBlank() && request.displayId == null) {
logEvent(
"当前实现无法获取 new display 的真实 displayId应用会启动到默认显示",
Log.WARN,
)
}
runCatching {
NativeAdbService.startApp(
packageName = request.packageName,
displayId = request.displayId,
forceStop = request.forceStop,
)
}.onSuccess {
val appLabelPart = request.matchedAppLabel?.let { " ($it)" }.orEmpty()
logEvent(
"已启动应用: ${request.packageName}$appLabelPart" +
request.displayId?.let { " @display=$it" }.orEmpty()
)
}.onFailure { error ->
logEvent(
"启动应用失败: ${request.packageName} (${
error.message?.takeIf { it.isNotBlank() } ?: error.javaClass.simpleName
})",
Log.WARN,
error,
)
}
}
if (options.fullscreen || openFullscreen) withContext(Dispatchers.Main) {
context.startActivity(StreamActivity.createIntent(context)) context.startActivity(StreamActivity.createIntent(context))
} }
if (options.disableScreensaver) if (options.disableScreensaver)
@@ -549,6 +648,15 @@ fun DeviceTabPage(
snackbar.show("scrcpy 已启动") snackbar.show("scrcpy 已启动")
} }
suspend fun stopScrcpySession() {
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
scrcpy.stop()
if (options.killAdbOnClose) {
// TODO
disconnectAdbConnection()
}
}
LaunchedEffect(pendingScrollToPreview, isPreviewCardVisible) { LaunchedEffect(pendingScrollToPreview, isPreviewCardVisible) {
if (!pendingScrollToPreview) return@LaunchedEffect if (!pendingScrollToPreview) return@LaunchedEffect
if (isPreviewCardVisible) return@LaunchedEffect if (isPreviewCardVisible) return@LaunchedEffect

View File

@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -40,8 +41,13 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
@@ -61,6 +67,9 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Store
import top.yukonga.miuix.kmp.icon.extended.Tune
import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference
@@ -153,26 +162,50 @@ internal fun ScrcpyAllOptionsPage(
.coerceAtLeast(0) .coerceAtLeast(0)
} }
val videoSourceItems = rememberSaveable { Shared.VideoSource.entries.map { it.string } } val videoSourceItems = rememberSaveable { VideoSource.entries.map { it.string } }
val videoSourceIndex = rememberSaveable(soBundle.videoSource) { val videoSourceIndex = rememberSaveable(soBundle.videoSource) {
Shared.VideoSource.entries VideoSource.entries
.indexOfFirst { it.string == soBundle.videoSource } .indexOfFirst { it.string == soBundle.videoSource }
.coerceAtLeast(0) .coerceAtLeast(0)
} }
var displayIdInput by rememberSaveable(soBundle.displayId) { val displays = scrcpy.listings.displays
mutableStateOf( val displayDropdownItems = rememberSaveable(displays, listRefreshVersion) {
if (soBundle.displayId == -1) "" listOf("默认") + displays.map { "${it.id} (${it.width}x${it.height})" }
else soBundle.displayId.toString() }
) val displayDropdownIndex = rememberSaveable(
soBundle.displayId,
displays,
listRefreshVersion,
) {
(displays.indexOfFirst { it.id == soBundle.displayId } + 1).coerceAtLeast(0)
} }
var cameraIdInput by rememberSaveable(soBundle.cameraId) { val cameras = scrcpy.listings.cameras
mutableStateOf(soBundle.cameraId) val cameraDropdownItems = rememberSaveable(cameras, listRefreshVersion) {
listOf("默认") + cameras.map { info ->
buildString {
append(info.id)
append(" (")
append(info.facing.string)
if (info.activeSize.isNotBlank()) {
append(", ")
append(info.activeSize)
}
append(')')
}
}
}
val cameraDropdownIndex = rememberSaveable(
soBundle.cameraId,
cameras,
listRefreshVersion,
) {
(cameras.indexOfFirst { it.id == soBundle.cameraId } + 1).coerceAtLeast(0)
} }
val cameraFacingItems = rememberSaveable { val cameraFacingItems = rememberSaveable {
listOf("默认") + Shared.CameraFacing.entries listOf("默认") + CameraFacing.entries
.drop(1) .drop(1)
.map { it.string } .map { it.string }
} }
@@ -180,7 +213,7 @@ internal fun ScrcpyAllOptionsPage(
if (soBundle.cameraFacing.isEmpty()) { if (soBundle.cameraFacing.isEmpty()) {
0 0
} else { } else {
val idx = Shared.CameraFacing.entries val idx = CameraFacing.entries
.indexOfFirst { it.string == soBundle.cameraFacing } .indexOfFirst { it.string == soBundle.cameraFacing }
if (idx > 0) idx else 0 if (idx > 0) idx else 0
} }
@@ -192,24 +225,23 @@ internal fun ScrcpyAllOptionsPage(
val cameraSizes = scrcpy.listings.cameraSizes val cameraSizes = scrcpy.listings.cameraSizes
val cameraSizeDropdownItems = rememberSaveable(cameraSizes, listRefreshVersion) { val cameraSizeDropdownItems = rememberSaveable(cameraSizes, listRefreshVersion) {
listOf("自动", "自定义") + cameraSizes listOf("默认", "自定义") + cameraSizes
} }
var cameraSizeDropdownIndex by rememberSaveable( val cameraSizeDropdownIndex = rememberSaveable(
soBundle.cameraSize, soBundle.cameraSize,
soBundle.cameraSizeCustom,
soBundle.cameraSizeUseCustom, soBundle.cameraSizeUseCustom,
cameraSizes, cameraSizes,
listRefreshVersion, listRefreshVersion,
) { ) {
mutableIntStateOf( when {
when { soBundle.cameraSizeUseCustom -> 1
soBundle.cameraSizeUseCustom -> 1 // "自定义" soBundle.cameraSize.isEmpty() -> 0
soBundle.cameraSize.isEmpty() -> 0 // "自动" soBundle.cameraSize in cameraSizes ->
soBundle.cameraSize in cameraSizes -> cameraSizes.indexOf(soBundle.cameraSize) + 2
cameraSizes.indexOf(soBundle.cameraSize) + 2
else -> 0 // 默认自动 else -> 0
} }
)
} }
var cameraArInput by rememberSaveable(soBundle.cameraAr) { var cameraArInput by rememberSaveable(soBundle.cameraAr) {
@@ -220,28 +252,19 @@ internal fun ScrcpyAllOptionsPage(
ScrcpyPresets.CameraFps.indexOfOrNearest(soBundle.cameraFps) ScrcpyPresets.CameraFps.indexOfOrNearest(soBundle.cameraFps)
} }
val audioSourceItems = rememberSaveable { val screenOffTimeoutPresetIndex = rememberSaveable(soBundle.screenOffTimeout) {
Shared.AudioSource.entries.map { it.string } ScrcpyPresets.ScreenOffTimeout.indexOfOrNearest(
} Tick(soBundle.screenOffTimeout).sec.toInt().coerceAtLeast(0)
val audioSourceIndex = rememberSaveable(soBundle.audioSource) { )
Shared.AudioSource.entries
.indexOfFirst { it.string == soBundle.audioSource }
.coerceAtLeast(0)
} }
val videoEncoderInfos = scrcpy.listings.videoEncoders val audioSourceItems = rememberSaveable {
val audioEncoderInfos = scrcpy.listings.audioEncoders AudioSource.entries.map { it.string }
val videoEncoders = remember(videoEncoderInfos) {
videoEncoderInfos.map { it.id }
} }
val audioEncoders = remember(audioEncoderInfos) { val audioSourceIndex = rememberSaveable(soBundle.audioSource) {
audioEncoderInfos.map { it.id } AudioSource.entries
} .indexOfFirst { it.string == soBundle.audioSource }
val videoEncoderTypes = remember(videoEncoderInfos) { .coerceAtLeast(0)
videoEncoderInfos.associate { it.id to it.type.s }
}
val audioEncoderTypes = remember(audioEncoderInfos) {
audioEncoderInfos.associate { it.id to it.type.s }
} }
val maxSizePresetIndex = rememberSaveable(soBundle.maxSize) { val maxSizePresetIndex = rememberSaveable(soBundle.maxSize) {
@@ -260,46 +283,118 @@ internal fun ScrcpyAllOptionsPage(
mutableStateOf(soBundle.audioCodecOptions) mutableStateOf(soBundle.audioCodecOptions)
} }
val videoEncoderDropdownItems = rememberSaveable(videoEncoders, listRefreshVersion) { val videoEncoders = scrcpy.listings.videoEncoders
listOf("") + videoEncoders val videoEncoderItems by remember(videoEncoders, listRefreshVersion) {
} derivedStateOf {
val videoEncoderIndex = buildList {
rememberSaveable(soBundle.videoEncoder, videoEncoders, listRefreshVersion) { add(SpinnerEntry(title = "自动"))
(videoEncoders.indexOf(soBundle.videoEncoder) + 1).coerceAtLeast(0) videoEncoders.forEach { info ->
add(
SpinnerEntry(
title = info.id,
summary = info.type.s,
)
)
}
}
} }
}
val videoEncoderIndex = rememberSaveable(
soBundle.videoEncoder,
videoEncoders,
listRefreshVersion,
) {
(videoEncoders.indexOfFirst { it.id == soBundle.videoEncoder } + 1)
.coerceAtLeast(0)
}
val audioEncoderDropdownItems = rememberSaveable(audioEncoders, listRefreshVersion) { val audioEncoders = scrcpy.listings.audioEncoders
listOf("") + audioEncoders val audioEncoderItems by remember(audioEncoders, listRefreshVersion) {
derivedStateOf {
buildList {
add(SpinnerEntry(title = "自动"))
audioEncoders.forEach { info ->
add(
SpinnerEntry(
title = info.id,
summary = info.type.s,
)
)
}
}
}
} }
val audioEncoderIndex = rememberSaveable( val audioEncoderIndex = rememberSaveable(
soBundle.audioEncoder, soBundle.audioEncoder,
audioEncoders, audioEncoders,
listRefreshVersion listRefreshVersion,
) { ) {
(audioEncoders.indexOf(soBundle.audioEncoder) + 1).coerceAtLeast(0) (audioEncoders.indexOfFirst { it.id == soBundle.audioEncoder } + 1)
.coerceAtLeast(0)
} }
val videoEncoderEntries = videoEncoderDropdownItems.map { encoderName -> val displayImePolicyItems = rememberSaveable {
if (encoderName == "") { listOf("默认") + DisplayImePolicy.entries
SpinnerEntry(title = "自动") .drop(1)
.map { it.string }
}
val displayImePolicyIndex = rememberSaveable(soBundle.displayImePolicy) {
if (soBundle.displayImePolicy.isEmpty()) {
0
} else { } else {
SpinnerEntry( val idx = DisplayImePolicy.entries
title = encoderName, .indexOfFirst { it.string == soBundle.displayImePolicy }
summary = videoEncoderTypes[encoderName], if (idx > 0) idx else 0
)
} }
} }
val audioEncoderEntries = audioEncoderDropdownItems.map { encoderName -> val apps = remember(scrcpy.listings.apps, listRefreshVersion) {
if (encoderName == "") { scrcpy.listings.apps.sortedBy { it.packageName }
SpinnerEntry(title = "自动") }
} else { val appDropdownItems by remember(apps, listRefreshVersion) {
SpinnerEntry( derivedStateOf {
title = encoderName, buildList {
summary = audioEncoderTypes[encoderName], add(SpinnerEntry(title = ""))
) add(SpinnerEntry(title = "自定义"))
apps.forEach { app ->
add(
SpinnerEntry(
icon = {
Icon(
imageVector =
if (app.system) MiuixIcons.Tune
else MiuixIcons.Store,
contentDescription = app.label,
modifier = Modifier.padding(end = UiSpacing.ContentVertical),
)
},
title = app.label,
summary = app.packageName,
)
)
}
}
} }
} }
val appDropdownIndex = rememberSaveable(
soBundle.startApp,
soBundle.startAppCustom,
soBundle.startAppUseCustom,
apps,
listRefreshVersion,
) {
when {
soBundle.startAppUseCustom -> 1
soBundle.startApp.isEmpty() -> 0
apps.any { it.packageName == soBundle.startApp } ->
apps.indexOfFirst { it.packageName == soBundle.startApp } + 2
else -> 0
}
}
var startAppCustomInput by rememberSaveable(soBundle.startAppCustom) {
mutableStateOf(soBundle.startAppCustom)
}
// [<width>x<height>][/<dpi>] // [<width>x<height>][/<dpi>]
val (ndWidth, ndHeight, ndDpi) = remember(soBundle.newDisplay) { val (ndWidth, ndHeight, ndDpi) = remember(soBundle.newDisplay) {
@@ -332,6 +427,13 @@ internal fun ScrcpyAllOptionsPage(
mutableStateOf(cY?.toString() ?: "") mutableStateOf(cY?.toString() ?: "")
} }
val logLevelItems = rememberSaveable { LogLevel.entries.map { it.string } }
val logLevelIndex = rememberSaveable(soBundle.logLevel) {
LogLevel.entries
.indexOfFirst { it.string == soBundle.logLevel }
.coerceAtLeast(0)
}
var serverParamsPreview by rememberSaveable { mutableStateOf("") } var serverParamsPreview by rememberSaveable { mutableStateOf("") }
// 监听选项变化, 自动更新预览 // 监听选项变化, 自动更新预览
LaunchedEffect(soBundle) { LaunchedEffect(soBundle) {
@@ -351,7 +453,7 @@ internal fun ScrcpyAllOptionsPage(
serverParamsPreview = clientOptions serverParamsPreview = clientOptions
.toServerParams(0u) .toServerParams(0u)
.toList(simplify = true) .toList(preview = true)
// improve readability using hard line breaks // improve readability using hard line breaks
.joinToString("\n") .joinToString("\n")
} }
@@ -374,19 +476,19 @@ internal fun ScrcpyAllOptionsPage(
item { item {
Card { Card {
SwitchPreference( SwitchPreference(
title = "启动后关闭屏幕", title = "scrcpy 启动后熄灭设备屏幕",
summary = "--turn-screen-off", summary = "--turn-screen-off",
checked = soBundle.turnScreenOff, checked = soBundle.turnScreenOff,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
turnScreenOff = it turnScreenOff = it
) )
if (it) { if (it) snackbar.show(
// github.com/Genymobile/scrcpy/issues/3376 // github.com/Genymobile/scrcpy/issues/3376
// github.com/Genymobile/scrcpy/issues/4587 // github.com/Genymobile/scrcpy/issues/4587
// github.com/Genymobile/scrcpy/issues/5676 // github.com/Genymobile/scrcpy/issues/5676
snackbar.show("注意:大部分设备在关闭屏幕后刷新率会降低/减半") "注意:大部分设备在关闭屏幕后刷新率会降低/减半"
} )
}, },
) )
SwitchPreference( SwitchPreference(
@@ -424,38 +526,45 @@ internal fun ScrcpyAllOptionsPage(
// enabled = control || video, // enabled = control || video,
) )
SuperSlider( SuperSlider(
title = "投屏过程中受控机的息屏时间", title = "scrcpy 启动后受控机的息屏时间",
summary = "--screen-off-timeout", summary = "--screen-off-timeout",
value = soBundle.screenOffTimeout.coerceAtLeast(0).toFloat(), value = screenOffTimeoutPresetIndex.toFloat(),
onValueChange = { onValueChange = { value ->
val idx = value.roundToInt()
.coerceIn(0, ScrcpyPresets.ScreenOffTimeout.lastIndex)
soBundle = soBundle.copy( soBundle = soBundle.copy(
screenOffTimeout = it.roundToInt() screenOffTimeout = ScrcpyPresets.ScreenOffTimeout[idx]
.takeIf { value -> value > 0 } .takeIf { it > 0 }
?.toLong() ?: -1 ?.toLong()
?.let(Tick::fromSec)
?.value
?: -1
) )
}, },
valueRange = 0f..600f, valueRange = 0f..ScrcpyPresets.ScreenOffTimeout.lastIndex.toFloat(),
steps = 600 - 1, steps = (ScrcpyPresets.ScreenOffTimeout.size - 2).coerceAtLeast(0),
unit = "s", unit = "s",
zeroStateText = "默认", zeroStateText = "不修改",
displayText = showKeyPoints = true,
if (soBundle.screenOffTimeout <= 0) "默认" keyPoints = ScrcpyPresets.ScreenOffTimeout.indices.map { it.toFloat() },
else soBundle.screenOffTimeout.toString(), displayText = Tick(soBundle.screenOffTimeout).sec.toString(),
inputInitialValue = inputInitialValue =
if (soBundle.screenOffTimeout <= 0) "" if (soBundle.screenOffTimeout <= 0) ""
else soBundle.screenOffTimeout.toString(), else Tick(soBundle.screenOffTimeout).sec.toString(),
inputFilter = { it.filter(Char::isDigit) }, inputFilter = { it.filter(Char::isDigit) },
inputValueRange = 0f..86400f, inputValueRange = 0f..86400f,
onInputConfirm = { onInputConfirm = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
screenOffTimeout = it.toLongOrNull() screenOffTimeout = it.toLongOrNull()
?.takeIf { value -> value > 0 } ?.takeIf { value -> value > 0 }
?.let(Tick::fromSec)
?.value
?: -1 ?: -1
) )
}, },
) )
SwitchPreference( SwitchPreference(
title = "开始投屏时不唤醒屏幕", title = "scrcpy 启动时不唤醒屏幕",
summary = "--no-power-on", summary = "--no-power-on",
checked = !soBundle.powerOn, checked = !soBundle.powerOn,
onCheckedChange = { onCheckedChange = {
@@ -465,7 +574,7 @@ internal fun ScrcpyAllOptionsPage(
}, },
) )
SwitchPreference( SwitchPreference(
title = "结束投屏时息屏", title = "scrcpy 结束后息屏",
summary = "--power-off-on-close", summary = "--power-off-on-close",
checked = soBundle.powerOffOnClose, checked = soBundle.powerOffOnClose,
onCheckedChange = { onCheckedChange = {
@@ -474,6 +583,16 @@ internal fun ScrcpyAllOptionsPage(
) )
}, },
) )
SwitchPreference(
title = "scrcpy 启动后保持设备唤醒状态(插入电源)",
summary = "--stay-awake",
checked = soBundle.stayAwake,
onCheckedChange = {
soBundle = soBundle.copy(
stayAwake = it
)
},
)
SwitchPreference( SwitchPreference(
title = "显示物理触控", title = "显示物理触控",
summary = "--show-touches", summary = "--show-touches",
@@ -485,15 +604,42 @@ internal fun ScrcpyAllOptionsPage(
}, },
) )
SwitchPreference( SwitchPreference(
title = "投屏时保持本机屏幕唤醒", title = "启动后进入全屏",
summary = "--fullscreen",
checked = soBundle.fullscreen,
onCheckedChange = {
soBundle = soBundle.copy(
fullscreen = it
)
},
)
SwitchPreference(
title = "scrcpy 启动后保持本机屏幕唤醒",
summary = "--disable-screensaver", summary = "--disable-screensaver",
checked = soBundle.disableScreensaver, checked = soBundle.disableScreensaver,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
disableScreensaver = it disableScreensaver = it
) )
if (it) snackbar.show(
"不保证可用"
)
}, },
) )
SwitchPreference(
title = "scrcpy 结束时断开 adb",
summary = "--kill-adb-on-close",
checked = soBundle.killAdbOnClose,
onCheckedChange = {
soBundle = soBundle.copy(
killAdbOnClose = it
)
if (it) snackbar.show(
"未实现"
)
},
enabled = false,
)
} }
} }
@@ -610,26 +756,46 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = videoSourceIndex, selectedIndex = videoSourceIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoSource = Shared.VideoSource.entries[it].string videoSource = VideoSource.entries[it].string
) )
}, },
) )
AnimatedVisibility(soBundle.videoSource == "display") { AnimatedVisibility(soBundle.videoSource == "display") {
Column { Column {
SuperTextField( ArrowPreference(
value = displayIdInput, title = "获取 Displays",
onValueChange = { displayIdInput = it }, summary = "--list-displays",
onFocusLost = { onClick = {
if (refreshBusy) return@ArrowPreference
scope.launch {
refreshBusy = true
snackbar.show("获取中")
try {
withContext(Dispatchers.IO) {
scrcpy.listings.getDisplays(forceRefresh = true)
}
listRefreshVersion += 1
snackbar.show("获取成功")
} catch (e: Exception) {
snackbar.show("刷新失败: ${e.message}")
} finally {
refreshBusy = false
}
}
},
)
OverlayDropdownPreference(
title = "监视器 ID",
summary = "--display-id",
items = displayDropdownItems,
selectedIndex = displayDropdownIndex,
onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
displayId = displayIdInput.toIntOrNull() ?: -1 displayId =
if (it == 0) -1
else displays[it - 1].id
) )
}, },
label = "--display-id",
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier
.fillMaxWidth()
.padding(all = UiSpacing.Large),
) )
SuperSlider( SuperSlider(
title = "最大分辨率", title = "最大分辨率",
@@ -694,19 +860,40 @@ internal fun ScrcpyAllOptionsPage(
} }
AnimatedVisibility(soBundle.videoSource == "camera") { AnimatedVisibility(soBundle.videoSource == "camera") {
Column { Column {
SuperTextField( ArrowPreference(
value = cameraIdInput, title = "获取 Cameras",
onValueChange = { cameraIdInput = it }, summary = "--list-cameras",
onFocusLost = { onClick = {
if (refreshBusy) return@ArrowPreference
scope.launch {
refreshBusy = true
snackbar.show("获取中")
try {
withContext(Dispatchers.IO) {
scrcpy.listings.getCameras(forceRefresh = true)
}
listRefreshVersion += 1
snackbar.show("获取成功")
} catch (e: Exception) {
snackbar.show("刷新失败: ${e.message}")
} finally {
refreshBusy = false
}
}
},
)
OverlayDropdownPreference(
title = "摄像头 ID",
summary = "--camera-id",
items = cameraDropdownItems,
selectedIndex = cameraDropdownIndex,
onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraId = cameraIdInput cameraId =
if (it == 0) ""
else cameras[it - 1].id
) )
}, },
label = "--camera-id",
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(all = UiSpacing.Large),
) )
OverlayDropdownPreference( OverlayDropdownPreference(
title = "摄像头朝向", title = "摄像头朝向",
@@ -717,7 +904,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraFacing = cameraFacing =
if (it == 0) "" if (it == 0) ""
else Shared.CameraFacing.entries[it].string else CameraFacing.entries[it].string
) )
}, },
) )
@@ -749,7 +936,6 @@ internal fun ScrcpyAllOptionsPage(
items = cameraSizeDropdownItems, items = cameraSizeDropdownItems,
selectedIndex = cameraSizeDropdownIndex, selectedIndex = cameraSizeDropdownIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
cameraSizeDropdownIndex = it
when (it) { when (it) {
0 -> { 0 -> {
// "自动" // "自动"
@@ -765,7 +951,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraSizeUseCustom = true cameraSizeUseCustom = true
) )
cameraSizeCustomInput = soBundle.cameraSize cameraSizeCustomInput = ""
} }
else -> { else -> {
@@ -785,15 +971,14 @@ internal fun ScrcpyAllOptionsPage(
value = cameraSizeCustomInput, value = cameraSizeCustomInput,
onValueChange = { cameraSizeCustomInput = it }, onValueChange = { cameraSizeCustomInput = it },
onFocusLost = { onFocusLost = {
if (cameraSizeCustomInput in cameraSizes) { soBundle = if (cameraSizeCustomInput in cameraSizes) {
// 输入的值存在于列表中, 取消自定义输入 // 输入的值存在于列表中, 取消自定义输入
cameraSizeDropdownIndex = cameraSizes soBundle.copy(
.indexOf(cameraSizeCustomInput) + 2 cameraSize = cameraSizeCustomInput,
soBundle = soBundle.copy(
cameraSizeUseCustom = false cameraSizeUseCustom = false
) )
} else { } else {
soBundle = soBundle.copy( soBundle.copy(
cameraSizeCustom = cameraSizeCustomInput cameraSizeCustom = cameraSizeCustomInput
) )
} }
@@ -859,26 +1044,6 @@ internal fun ScrcpyAllOptionsPage(
) )
}, },
) )
SwitchPreference(
title = "关闭虚拟显示器时保留内容",
summary = "--no-vd-destroy-content",
checked = !soBundle.vdDestroyContent,
onCheckedChange = {
soBundle = soBundle.copy(
vdDestroyContent = !it
)
},
)
SwitchPreference(
title = "禁用虚拟显示器系统装饰",
summary = "--no-vd-system-decorations",
checked = !soBundle.vdSystemDecorations,
onCheckedChange = {
soBundle = soBundle.copy(
vdSystemDecorations = !it
)
},
)
} }
} }
@@ -894,7 +1059,7 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = audioSourceIndex, selectedIndex = audioSourceIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioSource = Shared.AudioSource.entries[it].string audioSource = AudioSource.entries[it].string
) )
}, },
) )
@@ -943,7 +1108,7 @@ internal fun ScrcpyAllOptionsPage(
snackbar.show("获取中") snackbar.show("获取中")
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
scrcpy.listings.getVideoEncoders(forceRefresh = true) scrcpy.listings.getEncoders(forceRefresh = true)
} }
listRefreshVersion += 1 listRefreshVersion += 1
snackbar.show("获取成功") snackbar.show("获取成功")
@@ -959,11 +1124,13 @@ internal fun ScrcpyAllOptionsPage(
OverlaySpinnerPreference( OverlaySpinnerPreference(
title = "视频编码器", title = "视频编码器",
summary = "--video-encoder", summary = "--video-encoder",
items = videoEncoderEntries, items = videoEncoderItems,
selectedIndex = videoEncoderIndex, selectedIndex = videoEncoderIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoEncoder = videoEncoderEntries[it].title ?: "" videoEncoder =
if (it == 0) ""
else videoEncoders[it - 1].id
) )
}, },
) )
@@ -984,11 +1151,13 @@ internal fun ScrcpyAllOptionsPage(
OverlaySpinnerPreference( OverlaySpinnerPreference(
title = "音频编码器", title = "音频编码器",
summary = "--audio-encoder", summary = "--audio-encoder",
items = audioEncoderEntries, items = audioEncoderItems,
selectedIndex = audioEncoderIndex, selectedIndex = audioEncoderIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioEncoder = audioEncoderEntries[it].title ?: "" audioEncoder =
if (it == 0) ""
else audioEncoders[it - 1].id
) )
}, },
) )
@@ -1009,6 +1178,160 @@ internal fun ScrcpyAllOptionsPage(
} }
} }
if (soBundle.videoSource == "display") item {
Card {
OverlayDropdownPreference(
title = "输入法显示策略",
summary = "--display-ime-policy",
items = displayImePolicyItems,
selectedIndex = displayImePolicyIndex,
onSelectedIndexChange = {
soBundle = soBundle.copy(
displayImePolicy =
if (it == 0) ""
else DisplayImePolicy.entries[it].string
)
},
)
SwitchPreference(
title = "关闭虚拟显示器时保留内容",
summary = "--no-vd-destroy-content",
checked = !soBundle.vdDestroyContent,
onCheckedChange = {
soBundle = soBundle.copy(
vdDestroyContent = !it
)
},
)
SwitchPreference(
title = "禁用虚拟显示器系统装饰",
summary = "--no-vd-system-decorations",
checked = !soBundle.vdSystemDecorations,
onCheckedChange = {
soBundle = soBundle.copy(
vdSystemDecorations = !it
)
},
)
SwitchPreference(
title = "禁用结束后清理",
summary = "--no-downsize-on-error",
checked = !soBundle.downsizeOnError,
onCheckedChange = {
soBundle = soBundle.copy(
downsizeOnError = !it
)
if (it) snackbar.show(
"默认情况下,在 MediaCodec 出错时," +
"scrcpy 会自动尝试使用更低的分辨率重新开始。" +
"\n此选项将禁用此行为。"
)
},
)
SwitchPreference(
title = "禁用结束后清理",
summary = "--no-cleanup",
checked = !soBundle.cleanup,
onCheckedChange = {
soBundle = soBundle.copy(
cleanup = !it
)
if (it) snackbar.show(
"默认情况下scrcpy 会从设备中移除服务器二进制文件," +
"并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)" +
"\n此选项将禁用此清理操作"
)
},
)
}
}
if (soBundle.videoSource == "display") item {
Card {
ArrowPreference(
title = "获取应用列表",
summary = "--list-apps",
onClick = {
if (refreshBusy) return@ArrowPreference
scope.launch {
refreshBusy = true
snackbar.show("获取中")
try {
withContext(Dispatchers.IO) {
scrcpy.listings.getApps(forceRefresh = true)
}
listRefreshVersion += 1
snackbar.show("获取成功")
} catch (e: Exception) {
snackbar.show("刷新失败: ${e.message}")
} finally {
refreshBusy = false
}
}
},
)
OverlaySpinnerPreference(
title = "scrcpy 启动后打开应用",
summary = "--start-app\nTODO: 未实现虚拟屏配合",
items = appDropdownItems,
selectedIndex = appDropdownIndex,
onSelectedIndexChange = {
when (it) {
0 -> {
soBundle = soBundle.copy(
startApp = "",
startAppUseCustom = false,
)
startAppCustomInput = ""
}
1 -> {
soBundle = soBundle.copy(
startAppUseCustom = true
)
startAppCustomInput = ""
}
else -> {
val selectedApp = apps[it - 2]
soBundle = soBundle.copy(
startApp = selectedApp.packageName,
startAppUseCustom = false,
)
startAppCustomInput = ""
}
}
},
)
AnimatedVisibility(soBundle.startAppUseCustom) {
SuperTextField(
value = startAppCustomInput,
onValueChange = { startAppCustomInput = it },
onFocusLost = {
val matchedAppIndex = apps
.indexOfFirst { it.packageName == startAppCustomInput }
soBundle = if (matchedAppIndex >= 0) {
soBundle.copy(
startApp = apps[matchedAppIndex].packageName,
startAppUseCustom = false,
)
} else {
soBundle.copy(
startAppCustom = startAppCustomInput
)
}
},
label = "--start-app",
useLabelAsPlaceholder = true,
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(all = UiSpacing.Large),
)
}
}
}
if (soBundle.videoSource == "display") item { if (soBundle.videoSource == "display") item {
Card { Card {
Column( Column(
@@ -1243,6 +1566,22 @@ internal fun ScrcpyAllOptionsPage(
} }
} }
item {
Card {
OverlayDropdownPreference(
title = "日志等级",
summary = "--verbosity",
items = logLevelItems,
selectedIndex = logLevelIndex,
onSelectedIndexChange = {
soBundle = soBundle.copy(
logLevel = LogLevel.entries[it].string
)
},
)
}
}
item { Spacer(Modifier.height(UiSpacing.PageBottom)) } item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
} }
} }

View File

@@ -164,7 +164,7 @@ data class ClientOptions(
// var clipboardAutosync: Boolean = true, // to server // var clipboardAutosync: Boolean = true, // to server
// --no-downsize-on-error // --no-downsize-on-error
// var downsizeOnError: Boolean = true, // to server var downsizeOnError: Boolean = true, // to server
// var tcpip: Boolean, // to server // var tcpip: Boolean, // to server
// var tcpipDst: String = "", // to server // var tcpipDst: String = "", // to server
@@ -172,7 +172,7 @@ data class ClientOptions(
// var selectTcpip: Boolean, // to server // var selectTcpip: Boolean, // to server
// --no-cleanup // --no-cleanup
var cleanUp: Boolean = true, // to server var cleanup: Boolean = true, // to server
// var startFpsCounter: Boolean, // var startFpsCounter: Boolean,
@@ -226,6 +226,12 @@ data class ClientOptions(
M4A, MKA, OPUS, AAC, FLAC, WAV -> true M4A, MKA, OPUS, AAC, FLAC, WAV -> true
else -> false else -> false
} }
companion object {
fun fromString(value: String) =
entries.find { it.string.equals(value, ignoreCase = true) }
?: AUTO
}
} }
fun fix(): ClientOptions { fun fix(): ClientOptions {
@@ -552,7 +558,9 @@ data class ClientOptions(
powerOffOnClose = powerOffOnClose, powerOffOnClose = powerOffOnClose,
cleanUp = cleanUp, downsizeOnError = downsizeOnError,
cleanUp = cleanup,
powerOn = powerOn, powerOn = powerOn,
// killAdbOnClose == killAdbOnClose, // client side // killAdbOnClose == killAdbOnClose, // client side

View File

@@ -344,7 +344,7 @@ class Scrcpy(
return getEncoders(forceRefresh).second return getEncoders(forceRefresh).second
} }
private suspend fun getEncoders(forceRefresh: Boolean = false) suspend fun getEncoders(forceRefresh: Boolean = false)
: Pair<List<EncoderInfo>, List<EncoderInfo>> { : Pair<List<EncoderInfo>, List<EncoderInfo>> {
if (!forceRefresh && cachedVideoEncoders != null && cachedAudioEncoders != null) if (!forceRefresh && cachedVideoEncoders != null && cachedAudioEncoders != null)
return cachedVideoEncoders.orEmpty() to cachedAudioEncoders.orEmpty() return cachedVideoEncoders.orEmpty() to cachedAudioEncoders.orEmpty()
@@ -455,7 +455,7 @@ class Scrcpy(
video = false, video = false,
audio = false, audio = false,
control = false, control = false,
cleanUp = false, cleanup = false,
list = list, list = list,
) )
val serverParams = options.toServerParams(scid) val serverParams = options.toServerParams(scid)
@@ -1065,6 +1065,7 @@ class Scrcpy(
} }
serverLogBuffer.addLast(line) serverLogBuffer.addLast(line)
} }
logEvent(line)
Log.i(TAG, "[server:$socketName] $line") Log.i(TAG, "[server:$socketName] $line")
} }
} }

View File

@@ -73,7 +73,7 @@ data class ServerParams(
var powerOffOnClose: Boolean, var powerOffOnClose: Boolean,
// var downsizeOnError: Boolean, var downsizeOnError: Boolean,
// var tcpip: Boolean, // var tcpip: Boolean,
// var tcpipDst: String, // var tcpipDst: String,
// var selectUsb: Boolean, // var selectUsb: Boolean,
@@ -107,13 +107,12 @@ data class ServerParams(
return (extraArgs.toList() + this.toList()).joinToString(SEPARATOR) return (extraArgs.toList() + this.toList()).joinToString(SEPARATOR)
} }
fun toList(simplify: Boolean = false): MutableList<String> { fun toList(preview: Boolean = false): MutableList<String> {
val cmd = mutableListOf<String>() val cmd = mutableListOf<String>()
if (!simplify) { if (!preview) cmd.add("scid=${scid.toString(16)}")
cmd.add("scid=${scid.toString(16)}")
cmd.add("log_level=${logLevel.string}") cmd.add("log_level=${logLevel.string}")
}
if (!video) { if (!video) {
cmd.add("video=false") cmd.add("video=false")
@@ -216,7 +215,7 @@ data class ServerParams(
require(screenOffTimeout >= 0) { require(screenOffTimeout >= 0) {
"screen_off_timeout must be >= 0" "screen_off_timeout must be >= 0"
} }
cmd.add("screen_off_timeout=${screenOffTimeout.toMs()}") cmd.add("screen_off_timeout=${screenOffTimeout.ms}")
} }
if (videoCodecOptions.isNotBlank()) { if (videoCodecOptions.isNotBlank()) {
validate(videoCodecOptions) validate(videoCodecOptions)
@@ -242,8 +241,6 @@ data class ServerParams(
if (!clipBoardAutosync) { if (!clipBoardAutosync) {
cmd.add("clipboard_autosync=false") cmd.add("clipboard_autosync=false")
} }
// not implemented
val downsizeOnError = false
if (!downsizeOnError) { if (!downsizeOnError) {
cmd.add("downsize_on_error=false") cmd.add("downsize_on_error=false")
} }
@@ -285,4 +282,4 @@ data class ServerParams(
} }
return cmd return cmd
} }
} }

View File

@@ -15,6 +15,11 @@ class Shared {
fun toMs(): Long = value / 1000 fun toMs(): Long = value / 1000
fun toSec(): Long = value / 1_000_000 fun toSec(): Long = value / 1_000_000
fun toSecDouble(): Double = value / 1_000_000.0 fun toSecDouble(): Double = value / 1_000_000.0
val ns: Long get() = toNs()
val us: Long get() = toUs()
val ms: Long get() = toMs()
val sec: Long get() = toSec()
val secDouble: Double get() = toSecDouble()
operator fun plus(other: Tick): Tick = Tick(value + other.value) operator fun plus(other: Tick): Tick = Tick(value + other.value)
operator fun minus(other: Tick): Tick = Tick(value - other.value) operator fun minus(other: Tick): Tick = Tick(value - other.value)
@@ -38,17 +43,23 @@ class Shared {
fun Duration.toTick(): Tick = Tick(this.inWholeMicroseconds) fun Duration.toTick(): Tick = Tick(this.inWholeMicroseconds)
} }
enum class ListOptions(val value: Int) { enum class ListOptions(val string: String, val value: Int) {
NULL(0x0), NULL("null", 0x0),
ENCODERS(0x1), // --list-encoders ENCODERS("encoders", 0x1), // --list-encoders
DISPLAYS(0x2), // --list-displays DISPLAYS("displays", 0x2), // --list-displays
CAMERAS(0x4), // --list-cameras CAMERAS("cameras", 0x4), // --list-cameras
CAMERA_SIZES(0x8), // --list-camera-sizes CAMERA_SIZES("camera_sizes", 0x8), // --list-camera-sizes
APPS(0x10); // --list-apps APPS("apps", 0x10); // --list-apps
infix fun or(other: ListOptions) = value or other.value infix fun or(other: ListOptions) = value or other.value
infix fun and(other: ListOptions) = value and other.value infix fun and(other: ListOptions) = value and other.value
infix fun has(other: ListOptions) = this and other != 0 infix fun has(other: ListOptions) = this and other != 0
companion object {
fun fromString(value: String) =
entries.find { it.string.equals(value, ignoreCase = true) }
?: NULL
}
} }
enum class EncoderType(val s: String) { enum class EncoderType(val s: String) {
@@ -62,6 +73,12 @@ class Shared {
INFO("info"), INFO("info"),
WARN("warn"), WARN("warn"),
ERROR("error"); ERROR("error");
companion object {
fun fromString(value: String) =
entries.find { it.string.equals(value, ignoreCase = true) }
?: VERBOSE
}
} }
enum class Codec( enum class Codec(
@@ -177,6 +194,12 @@ class Shared {
UNLOCKED("unlocked"), // ignore UNLOCKED("unlocked"), // ignore
LOCKED_VALUE("locked_value"), // "@${orientation.string}" LOCKED_VALUE("locked_value"), // "@${orientation.string}"
LOCKED_INITIAL("locked_initial"); // "@" LOCKED_INITIAL("locked_initial"); // "@"
companion object {
fun fromString(value: String) =
entries.find { it.string.equals(value, ignoreCase = true) }
?: UNLOCKED
}
} }
enum class DisplayImePolicy(val string: String) { enum class DisplayImePolicy(val string: String) {
@@ -184,5 +207,11 @@ class Shared {
LOCAL("local"), LOCAL("local"),
FALLBACK("fallback"), FALLBACK("fallback"),
HIDE("hide"); HIDE("hide");
companion object {
fun fromString(value: String) =
entries.find { it.string.equals(value, ignoreCase = true) }
?: UNDEFINED
}
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.RecordFormat
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
@@ -74,11 +75,11 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
) )
val LOG_LEVEL = Pair( val LOG_LEVEL = Pair(
stringPreferencesKey("log_level"), stringPreferencesKey("log_level"),
"info", LogLevel.INFO.string,
) )
val VIDEO_CODEC = Pair( val VIDEO_CODEC = Pair(
stringPreferencesKey("video_codec"), stringPreferencesKey("video_codec"),
"h264", Codec.H264.string,
) )
val AUDIO_CODEC = Pair( val AUDIO_CODEC = Pair(
stringPreferencesKey("audio_codec"), stringPreferencesKey("audio_codec"),
@@ -138,7 +139,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
) )
val DISPLAY_IME_POLICY = Pair( val DISPLAY_IME_POLICY = Pair(
stringPreferencesKey("display_ime_policy"), stringPreferencesKey("display_ime_policy"),
"undefined", DisplayImePolicy.UNDEFINED.string,
) )
val DISPLAY_ID = Pair( val DISPLAY_ID = Pair(
intPreferencesKey("display_id"), intPreferencesKey("display_id"),
@@ -184,6 +185,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
booleanPreferencesKey("power_off_on_close"), booleanPreferencesKey("power_off_on_close"),
false, false,
) )
val DOWNSIZE_ON_ERROR = Pair(
booleanPreferencesKey("downsize_on_error"),
true,
)
val CLEANUP = Pair( val CLEANUP = Pair(
booleanPreferencesKey("cleanup"), booleanPreferencesKey("cleanup"),
true, true,
@@ -228,6 +233,14 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
stringPreferencesKey("start_app"), stringPreferencesKey("start_app"),
"", "",
) )
val START_APP_CUSTOM = Pair(
stringPreferencesKey("start_app_custom"),
"",
)
val START_APP_USE_CUSTOM = Pair(
booleanPreferencesKey("start_app_use_custom"),
false,
)
val VD_DESTROY_CONTENT = Pair( val VD_DESTROY_CONTENT = Pair(
booleanPreferencesKey("vd_destroy_content"), booleanPreferencesKey("vd_destroy_content"),
true, true,
@@ -334,6 +347,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
val stayAwake: Boolean, val stayAwake: Boolean,
val disableScreensaver: Boolean, val disableScreensaver: Boolean,
val powerOffOnClose: Boolean, val powerOffOnClose: Boolean,
val downsizeOnError: Boolean,
val cleanup: Boolean, val cleanup: Boolean,
val powerOn: Boolean, val powerOn: Boolean,
val video: Boolean, val video: Boolean,
@@ -345,6 +359,8 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
val audioDup: Boolean, val audioDup: Boolean,
val newDisplay: String, val newDisplay: String,
val startApp: String, val startApp: String,
val startAppCustom: String,
val startAppUseCustom: Boolean,
val vdDestroyContent: Boolean, val vdDestroyContent: Boolean,
val vdSystemDecorations: Boolean val vdSystemDecorations: Boolean
) : Parcelable { ) : Parcelable {
@@ -449,6 +465,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
stayAwake = preferences.read(STAY_AWAKE), stayAwake = preferences.read(STAY_AWAKE),
disableScreensaver = preferences.read(DISABLE_SCREENSAVER), disableScreensaver = preferences.read(DISABLE_SCREENSAVER),
powerOffOnClose = preferences.read(POWER_OFF_ON_CLOSE), powerOffOnClose = preferences.read(POWER_OFF_ON_CLOSE),
downsizeOnError = preferences.read(DOWNSIZE_ON_ERROR),
cleanup = preferences.read(CLEANUP), cleanup = preferences.read(CLEANUP),
powerOn = preferences.read(POWER_ON), powerOn = preferences.read(POWER_ON),
video = preferences.read(VIDEO), video = preferences.read(VIDEO),
@@ -460,6 +477,8 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
audioDup = preferences.read(AUDIO_DUP), audioDup = preferences.read(AUDIO_DUP),
newDisplay = preferences.read(NEW_DISPLAY), newDisplay = preferences.read(NEW_DISPLAY),
startApp = preferences.read(START_APP), startApp = preferences.read(START_APP),
startAppCustom = preferences.read(START_APP_CUSTOM),
startAppUseCustom = preferences.read(START_APP_USE_CUSTOM),
vdDestroyContent = preferences.read(VD_DESTROY_CONTENT), vdDestroyContent = preferences.read(VD_DESTROY_CONTENT),
vdSystemDecorations = preferences.read(VD_SYSTEM_DECORATIONS), vdSystemDecorations = preferences.read(VD_SYSTEM_DECORATIONS),
) )
@@ -493,12 +512,12 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom, cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom,
cameraAr = bundle.cameraAr, cameraAr = bundle.cameraAr,
cameraFps = bundle.cameraFps.toUShort(), cameraFps = bundle.cameraFps.toUShort(),
logLevel = LogLevel.valueOf(bundle.logLevel.uppercase()), logLevel = LogLevel.fromString(bundle.logLevel),
videoCodec = Codec.fromString(bundle.videoCodec), videoCodec = Codec.fromString(bundle.videoCodec),
audioCodec = Codec.fromString(bundle.audioCodec), audioCodec = Codec.fromString(bundle.audioCodec),
videoSource = VideoSource.fromString(bundle.videoSource), videoSource = VideoSource.fromString(bundle.videoSource),
audioSource = AudioSource.fromString(bundle.audioSource), audioSource = AudioSource.fromString(bundle.audioSource),
recordFormat = ClientOptions.RecordFormat.valueOf(bundle.recordFormat.uppercase()), recordFormat = RecordFormat.fromString(bundle.recordFormat),
cameraFacing = CameraFacing.fromString(bundle.cameraFacing), cameraFacing = CameraFacing.fromString(bundle.cameraFacing),
maxSize = bundle.maxSize.toUShort(), maxSize = bundle.maxSize.toUShort(),
videoBitRate = bundle.videoBitRate, videoBitRate = bundle.videoBitRate,
@@ -506,12 +525,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
maxFps = bundle.maxFps, maxFps = bundle.maxFps,
angle = bundle.angle, angle = bundle.angle,
captureOrientation = Orientation.fromInt(bundle.captureOrientation), captureOrientation = Orientation.fromInt(bundle.captureOrientation),
captureOrientationLock = OrientationLock.valueOf( captureOrientationLock = OrientationLock.fromString(bundle.captureOrientationLock),
bundle.captureOrientationLock.uppercase()
),
displayOrientation = Orientation.fromInt(bundle.displayOrientation), displayOrientation = Orientation.fromInt(bundle.displayOrientation),
recordOrientation = Orientation.fromInt(bundle.recordOrientation), recordOrientation = Orientation.fromInt(bundle.recordOrientation),
displayImePolicy = DisplayImePolicy.valueOf(bundle.displayImePolicy.uppercase()), displayImePolicy = DisplayImePolicy.fromString(bundle.displayImePolicy),
displayId = bundle.displayId, displayId = bundle.displayId,
screenOffTimeout = Tick(bundle.screenOffTimeout), screenOffTimeout = Tick(bundle.screenOffTimeout),
showTouches = bundle.showTouches, showTouches = bundle.showTouches,
@@ -523,17 +540,18 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
stayAwake = bundle.stayAwake, stayAwake = bundle.stayAwake,
disableScreensaver = bundle.disableScreensaver, disableScreensaver = bundle.disableScreensaver,
powerOffOnClose = bundle.powerOffOnClose, powerOffOnClose = bundle.powerOffOnClose,
cleanUp = bundle.cleanup, downsizeOnError = bundle.downsizeOnError,
cleanup = bundle.cleanup,
powerOn = bundle.powerOn, powerOn = bundle.powerOn,
video = bundle.video, video = bundle.video,
audio = bundle.audio, audio = bundle.audio,
requireAudio = bundle.requireAudio, requireAudio = bundle.requireAudio,
killAdbOnClose = bundle.killAdbOnClose, killAdbOnClose = bundle.killAdbOnClose,
cameraHighSpeed = bundle.cameraHighSpeed, cameraHighSpeed = bundle.cameraHighSpeed,
list = ListOptions.valueOf(bundle.list.uppercase()), list = ListOptions.fromString(bundle.list),
audioDup = bundle.audioDup, audioDup = bundle.audioDup,
newDisplay = bundle.newDisplay, newDisplay = bundle.newDisplay,
startApp = bundle.startApp, startApp = if (!bundle.startAppUseCustom) bundle.startApp else bundle.startAppCustom,
vdDestroyContent = bundle.vdDestroyContent, vdDestroyContent = bundle.vdDestroyContent,
vdSystemDecorations = bundle.vdSystemDecorations vdSystemDecorations = bundle.vdSystemDecorations
) )