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
|
*.mp4
|
||||||
|
|
||||||
abandoned_assets/
|
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 的图标派生
|
||||||
|
|
||||||
## 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
|
## PARAMS
|
||||||
|
|
||||||
- ~~orientation locking~~
|
- ~~orientation locking~~
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import java.net.URI
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -56,8 +58,8 @@ android {
|
|||||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 30
|
versionCode = 31
|
||||||
versionName = "0.3.4"
|
versionName = "0.4.0"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
@@ -162,3 +164,75 @@ dependencies {
|
|||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
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.nativecore.PersistentVideoRenderer
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
|
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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
@@ -22,6 +27,7 @@ import java.util.concurrent.CopyOnWriteArraySet
|
|||||||
*/
|
*/
|
||||||
object NativeCoreFacade {
|
object NativeCoreFacade {
|
||||||
private val sessionLifecycleMutex = Mutex()
|
private val sessionLifecycleMutex = Mutex()
|
||||||
|
private val lifecycleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
private val renderer = PersistentVideoRenderer()
|
private val renderer = PersistentVideoRenderer()
|
||||||
private var activeSurfaceId: Int? = null
|
private var activeSurfaceId: Int? = null
|
||||||
private var decoder: AnnexBDecoder? = null
|
private var decoder: AnnexBDecoder? = null
|
||||||
@@ -41,6 +47,10 @@ object NativeCoreFacade {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var currentSessionInfo: Scrcpy.Session.SessionInfo? = null
|
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() {
|
suspend fun close() {
|
||||||
sessionLifecycleMutex.withLock {
|
sessionLifecycleMutex.withLock {
|
||||||
releaseAllDecoders()
|
releaseAllDecoders()
|
||||||
@@ -78,6 +88,10 @@ object NativeCoreFacade {
|
|||||||
return
|
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)
|
createOrReplaceDecoder(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,8 +171,10 @@ object NativeCoreFacade {
|
|||||||
*/
|
*/
|
||||||
suspend fun onScrcpySessionStarted(
|
suspend fun onScrcpySessionStarted(
|
||||||
session: Scrcpy.Session.SessionInfo,
|
session: Scrcpy.Session.SessionInfo,
|
||||||
sessionMgr: Scrcpy.Session
|
sessionMgr: Scrcpy.Session,
|
||||||
|
scrcpy: Scrcpy,
|
||||||
) = sessionLifecycleMutex.withLock {
|
) = sessionLifecycleMutex.withLock {
|
||||||
|
scrcpyRef = scrcpy
|
||||||
currentSessionInfo = session
|
currentSessionInfo = session
|
||||||
releaseAllDecoders()
|
releaseAllDecoders()
|
||||||
synchronized(bootstrapLock) {
|
synchronized(bootstrapLock) {
|
||||||
@@ -166,8 +182,13 @@ object NativeCoreFacade {
|
|||||||
latestConfigPacket = null
|
latestConfigPacket = null
|
||||||
}
|
}
|
||||||
if (activeSurfaceId != null || recordingSurfaceAttached) {
|
if (activeSurfaceId != null || recordingSurfaceAttached) {
|
||||||
Log.i(TAG, "onScrcpySessionStarted(): bind decoder to persistent surface")
|
// v4.0: width/height come from first video session packet, not from initial metadata
|
||||||
createOrReplaceDecoder(session)
|
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
|
packetCount = 0
|
||||||
sessionMgr.attachVideoConsumer { packet ->
|
sessionMgr.attachVideoConsumer { packet ->
|
||||||
@@ -179,9 +200,10 @@ object NativeCoreFacade {
|
|||||||
"videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoder=${decoder != null}"
|
"videoFeed(): packets=$packetCount key=${packet.isKeyFrame} cfg=${packet.isConfig} decoder=${decoder != null}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val currentDecoder = decoder ?: return@attachVideoConsumer
|
|
||||||
|
val dec = decoder ?: return@attachVideoConsumer
|
||||||
runCatching {
|
runCatching {
|
||||||
currentDecoder.feedAnnexB(
|
dec.feedAnnexB(
|
||||||
packet.data,
|
packet.data,
|
||||||
packet.ptsUs,
|
packet.ptsUs,
|
||||||
packet.isKeyFrame,
|
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.
|
* Called by Scrcpy.kt when a session stops.
|
||||||
* Cleans up decoders and resets state.
|
* Cleans up decoders and resets state.
|
||||||
@@ -201,6 +251,7 @@ object NativeCoreFacade {
|
|||||||
bootstrapPackets.clear()
|
bootstrapPackets.clear()
|
||||||
latestConfigPacket = null
|
latestConfigPacket = null
|
||||||
}
|
}
|
||||||
|
scrcpyRef = null
|
||||||
currentSessionInfo = null
|
currentSessionInfo = null
|
||||||
recordingSurfaceAttached = false
|
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 {
|
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 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 ScreenOffTimeout = Preset(listOf(0, 15, 30, 60, 120, 300, 600)) // sec
|
||||||
|
|||||||
@@ -605,9 +605,13 @@ internal class DeviceTabViewModel(
|
|||||||
|
|
||||||
val videoDetail =
|
val videoDetail =
|
||||||
if (!resolvedOptions.video) "off"
|
if (!resolvedOptions.video) "off"
|
||||||
else if (activeBundle.videoBitRate <= 0) "${session.codec?.string ?: "null"} ${session.width}x${session.height} @default"
|
else {
|
||||||
else "${session.codec?.string ?: "null"} ${session.width}x${session.height} " +
|
val codec = session.codec?.string ?: "null"
|
||||||
"@%.1fMbps".format(activeBundle.videoBitRate / 1_000_000f)
|
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 =
|
val audioDetail =
|
||||||
if (!activeBundle.audio) "off"
|
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.Scrcpy
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler
|
import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
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.LocalInputService
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
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.AppListBottomSheet
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.AppListEntry
|
import io.github.miuzarte.scrcpyforandroid.widgets.AppListEntry
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.ScrcpyVideoSurface
|
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(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -694,7 +709,17 @@ fun FullscreenControlPage(
|
|||||||
}
|
}
|
||||||
else Modifier
|
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 =
|
val sessionAspect =
|
||||||
if (session.height == 0) 16f / 9f
|
if (session.height == 0) 16f / 9f
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ internal fun ScrcpyAllOptionsScreen(
|
|||||||
bundle = copySourceBundle,
|
bundle = copySourceBundle,
|
||||||
)
|
)
|
||||||
selectedProfileId = created.id
|
selectedProfileId = created.id
|
||||||
|
bindCurrentConnectedDevice(created.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileDialogMode.Rename -> {
|
ProfileDialogMode.Rename -> {
|
||||||
@@ -597,6 +598,9 @@ internal fun ScrcpyAllOptionsPage(
|
|||||||
var cameraArInput by rememberSaveable(soBundle.cameraAr) {
|
var cameraArInput by rememberSaveable(soBundle.cameraAr) {
|
||||||
mutableStateOf(soBundle.cameraAr)
|
mutableStateOf(soBundle.cameraAr)
|
||||||
}
|
}
|
||||||
|
var cameraZoomInput by rememberSaveable(soBundle.cameraZoom) {
|
||||||
|
mutableStateOf(soBundle.cameraZoom)
|
||||||
|
}
|
||||||
|
|
||||||
val cameraFpsPresetIndex = rememberSaveable(soBundle.cameraFps) {
|
val cameraFpsPresetIndex = rememberSaveable(soBundle.cameraFps) {
|
||||||
ScrcpyPresets.CameraFps.indexOfOrNearest(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) {
|
val maxSizePresetIndex = rememberSaveable(soBundle.maxSize) {
|
||||||
ScrcpyPresets.MaxSize.indexOfOrNearest(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(
|
SwitchPreference(
|
||||||
title = stringResource(R.string.scrcpyopt_show_touches),
|
title = stringResource(R.string.scrcpyopt_show_touches),
|
||||||
summary = "--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(
|
SuperSlider(
|
||||||
title = stringResource(R.string.scrcpyopt_max_size),
|
title = stringResource(R.string.scrcpyopt_max_size),
|
||||||
summary = "--max-size",
|
summary = "--max-size",
|
||||||
@@ -1433,6 +1484,20 @@ internal fun ScrcpyAllOptionsPage(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(all = UiSpacing.Large),
|
.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(
|
SuperSlider(
|
||||||
title = stringResource(R.string.scrcpyopt_camera_fps),
|
title = stringResource(R.string.scrcpyopt_camera_fps),
|
||||||
summary = "--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
|
var cameraSize: String = "", // to server
|
||||||
// --camera-ar
|
// --camera-ar
|
||||||
var cameraAr: String = "", // to server
|
var cameraAr: String = "", // to server
|
||||||
|
// --camera-zoom
|
||||||
|
var cameraZoom: String = "", // to server
|
||||||
// --camera-fps
|
// --camera-fps
|
||||||
var cameraFps: UShort = 0u, // to server
|
var cameraFps: UShort = 0u, // to server
|
||||||
|
|
||||||
@@ -74,6 +76,9 @@ data class ClientOptions(
|
|||||||
// OR of enum sc_shortcut_mod values
|
// OR of enum sc_shortcut_mod values
|
||||||
// var shortcutMods: ShortcutMod,
|
// var shortcutMods: ShortcutMod,
|
||||||
|
|
||||||
|
// --min-size-alignment
|
||||||
|
var minSizeAlignment: UByte = 1u, // to server
|
||||||
|
|
||||||
// --max-size
|
// --max-size
|
||||||
var maxSize: UShort = 0u, // to server
|
var maxSize: UShort = 0u, // to server
|
||||||
|
|
||||||
@@ -207,6 +212,15 @@ data class ClientOptions(
|
|||||||
var vdDestroyContent: Boolean = true, // to server
|
var vdDestroyContent: Boolean = true, // to server
|
||||||
// --no-vd-system-decorations
|
// --no-vd-system-decorations
|
||||||
var vdSystemDecorations: Boolean = true, // to server
|
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) {
|
enum class KeyInjectMode(val string: String) {
|
||||||
MIXED("mixed"),
|
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
|
if (displayImePolicy != DisplayImePolicy.UNDEFINED
|
||||||
&& displayId == 0 && newDisplay.isBlank()
|
&& displayId == 0 && newDisplay.isBlank()
|
||||||
) {
|
) {
|
||||||
@@ -542,6 +577,11 @@ data class ClientOptions(
|
|||||||
"Cannot start an Android app if control is disabled"
|
"Cannot start an Android app if control is disabled"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (keepActive) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Cannot keep device active if control is disabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
@@ -560,6 +600,7 @@ data class ClientOptions(
|
|||||||
crop = crop,
|
crop = crop,
|
||||||
|
|
||||||
maxSize = maxSize,
|
maxSize = maxSize,
|
||||||
|
minSizeAlignment = minSizeAlignment,
|
||||||
videoBitRate = videoBitRate,
|
videoBitRate = videoBitRate,
|
||||||
audioBitRate = audioBitRate,
|
audioBitRate = audioBitRate,
|
||||||
maxFps = maxFps,
|
maxFps = maxFps,
|
||||||
@@ -583,6 +624,7 @@ data class ClientOptions(
|
|||||||
cameraId = cameraId,
|
cameraId = cameraId,
|
||||||
cameraSize = cameraSize,
|
cameraSize = cameraSize,
|
||||||
cameraAr = cameraAr,
|
cameraAr = cameraAr,
|
||||||
|
cameraZoom = cameraZoom,
|
||||||
cameraFps = cameraFps,
|
cameraFps = cameraFps,
|
||||||
|
|
||||||
powerOffOnClose = powerOffOnClose,
|
powerOffOnClose = powerOffOnClose,
|
||||||
@@ -595,8 +637,11 @@ data class ClientOptions(
|
|||||||
powerOn = powerOn,
|
powerOn = powerOn,
|
||||||
|
|
||||||
cameraHighSpeed = cameraHighSpeed,
|
cameraHighSpeed = cameraHighSpeed,
|
||||||
|
cameraTorch = cameraTorch,
|
||||||
vdDestroyContent = vdDestroyContent,
|
vdDestroyContent = vdDestroyContent,
|
||||||
vdSystemDecorations = vdSystemDecorations,
|
vdSystemDecorations = vdSystemDecorations,
|
||||||
|
keepActive = keepActive,
|
||||||
|
flexDisplay = flexDisplay,
|
||||||
list = list,
|
list = list,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import java.io.DataOutputStream
|
|||||||
import java.io.EOFException
|
import java.io.EOFException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
@@ -74,7 +76,10 @@ class Scrcpy(
|
|||||||
private val lowLatency: Boolean = false,
|
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 backgroundScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val clipboardSyncLock = Any()
|
private val clipboardSyncLock = Any()
|
||||||
private val clipboardManager by lazy {
|
private val clipboardManager by lazy {
|
||||||
@@ -99,6 +104,10 @@ class Scrcpy(
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var isRunning: Boolean = false
|
private var isRunning: Boolean = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
@JvmField
|
||||||
|
var flexDisplay: Boolean = false
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var audioPlayer: ScrcpyAudioPlayer? = null
|
private var audioPlayer: ScrcpyAudioPlayer? = null
|
||||||
|
|
||||||
@@ -116,9 +125,9 @@ class Scrcpy(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "Scrcpy"
|
private const val TAG = "Scrcpy"
|
||||||
|
|
||||||
const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v3.3.4"
|
const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v4.0"
|
||||||
const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v3.3.4"
|
const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v4.0"
|
||||||
const val DEFAULT_SERVER_VERSION = "3.3.4"
|
const val DEFAULT_SERVER_VERSION = "4.0"
|
||||||
const val DEFAULT_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar"
|
const val DEFAULT_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar"
|
||||||
|
|
||||||
// Regex patterns for parsing server output
|
// Regex patterns for parsing server output
|
||||||
@@ -197,11 +206,12 @@ class Scrcpy(
|
|||||||
forwardKeyRepeat = options.forwardKeyRepeat,
|
forwardKeyRepeat = options.forwardKeyRepeat,
|
||||||
)
|
)
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
flexDisplay = options.flexDisplay
|
||||||
startClipboardSync()
|
startClipboardSync()
|
||||||
|
|
||||||
// Setup video consumer (notify NativeCoreFacade to setup decoders)
|
// Setup video consumer (notify NativeCoreFacade to setup decoders)
|
||||||
if (options.video) {
|
if (options.video) {
|
||||||
NativeCoreFacade.onScrcpySessionStarted(info, session)
|
NativeCoreFacade.onScrcpySessionStarted(info, session, this@Scrcpy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup audio player
|
// Setup audio player
|
||||||
@@ -287,7 +297,10 @@ class Scrcpy(
|
|||||||
|
|
||||||
Log.i(
|
Log.i(
|
||||||
TAG, "start(): Session started successfully - device=${info.deviceName}, " +
|
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"}, " +
|
"audio=${if (options.audio) options.audioCodec.string else "off"}, " +
|
||||||
"control=${options.control}"
|
"control=${options.control}"
|
||||||
)
|
)
|
||||||
@@ -305,6 +318,7 @@ class Scrcpy(
|
|||||||
runCatching { aacRecorder?.release() }
|
runCatching { aacRecorder?.release() }
|
||||||
aacRecorder = null
|
aacRecorder = null
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
flexDisplay = false
|
||||||
_currentSessionState.value = null
|
_currentSessionState.value = null
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@@ -332,6 +346,7 @@ class Scrcpy(
|
|||||||
audioPlayer?.release()
|
audioPlayer?.release()
|
||||||
audioPlayer = null
|
audioPlayer = null
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
flexDisplay = false
|
||||||
_currentSessionState.value = null
|
_currentSessionState.value = null
|
||||||
stopClipboardSync()
|
stopClipboardSync()
|
||||||
Log.i(TAG, "stop(): Session stopped successfully")
|
Log.i(TAG, "stop(): Session stopped successfully")
|
||||||
@@ -348,6 +363,10 @@ class Scrcpy(
|
|||||||
session.startApp(name)
|
session.startApp(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun resizeDisplay(width: Int, height: Int) = withContext(Dispatchers.IO) {
|
||||||
|
session.resizeDisplay(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun injectKeycode(
|
suspend fun injectKeycode(
|
||||||
action: Int,
|
action: Int,
|
||||||
keycode: Int,
|
keycode: Int,
|
||||||
@@ -423,6 +442,7 @@ class Scrcpy(
|
|||||||
val current = _currentSessionState.value ?: return
|
val current = _currentSessionState.value ?: return
|
||||||
if (current.width == width && current.height == height) return
|
if (current.width == width && current.height == height) return
|
||||||
_currentSessionState.value = current.copy(width = width, height = height)
|
_currentSessionState.value = current.copy(width = width, height = height)
|
||||||
|
NativeCoreFacade.onVideoSizeChanged(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startClipboardSync() {
|
private fun startClipboardSync() {
|
||||||
@@ -970,6 +990,7 @@ class Scrcpy(
|
|||||||
*/
|
*/
|
||||||
class Session(
|
class Session(
|
||||||
private val onRemoteClipboardText: (String) -> Unit,
|
private val onRemoteClipboardText: (String) -> Unit,
|
||||||
|
private val onVideoSessionSize: (Int, Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
@@ -1085,20 +1106,17 @@ class Scrcpy(
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
val videoCodecId: Int
|
val videoCodecId = if (options.video) {
|
||||||
val width: Int
|
checkNotNull(videoInput).readInt()
|
||||||
val height: Int
|
|
||||||
if (options.video) {
|
|
||||||
val vInput = checkNotNull(videoInput)
|
|
||||||
videoCodecId = vInput.readInt()
|
|
||||||
width = vInput.readInt()
|
|
||||||
height = vInput.readInt()
|
|
||||||
} else {
|
} else {
|
||||||
videoCodecId = 0
|
0
|
||||||
width = 0
|
|
||||||
height = 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(
|
val sessionInfo = SessionInfo(
|
||||||
deviceName = deviceName,
|
deviceName = deviceName,
|
||||||
codecId = videoCodecId,
|
codecId = videoCodecId,
|
||||||
@@ -1154,11 +1172,37 @@ class Scrcpy(
|
|||||||
|
|
||||||
videoReaderThread = thread(start = true, name = "scrcpy-video-reader") {
|
videoReaderThread = thread(start = true, name = "scrcpy-video-reader") {
|
||||||
try {
|
try {
|
||||||
|
val sessionFlagByte = (PACKET_FLAG_SESSION ushr 56).toInt()
|
||||||
|
val headerBuf = ByteArray(12)
|
||||||
while (activeSession === session && !vStream.closed) {
|
while (activeSession === session && !vStream.closed) {
|
||||||
try {
|
try {
|
||||||
val ptsAndFlags = vInput.readLong()
|
vInput.readFully(headerBuf)
|
||||||
val packetSize = vInput.readInt()
|
|
||||||
if (packetSize <= 0) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1244,7 +1288,14 @@ class Scrcpy(
|
|||||||
requireControlWriter().startApp(name)
|
requireControlWriter().startApp(name)
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
Log.w(TAG, "startApp(): control channel not available", e)
|
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) {
|
fun setClipboard(text: String, paste: Boolean) {
|
||||||
val bytes = text.toByteArray(Charsets.UTF_8)
|
val bytes = text.toByteArray(Charsets.UTF_8)
|
||||||
output.writeByte(TYPE_SET_CLIPBOARD)
|
output.writeByte(TYPE_SET_CLIPBOARD)
|
||||||
output.writeLong(CLIPBOARD_SEQUENCE_INVALID)
|
output.writeLong(SEQUENCE_INVALID)
|
||||||
output.writeByte(if (paste) 1 else 0)
|
output.writeByte(if (paste) 1 else 0)
|
||||||
output.writeInt(bytes.size)
|
output.writeInt(bytes.size)
|
||||||
output.write(bytes)
|
output.write(bytes)
|
||||||
@@ -1698,6 +1749,36 @@ class Scrcpy(
|
|||||||
output.flush()
|
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) {
|
private fun writePosition(x: Int, y: Int, screenWidth: Int, screenHeight: Int) {
|
||||||
output.writeInt(x)
|
output.writeInt(x)
|
||||||
output.writeInt(y)
|
output.writeInt(y)
|
||||||
@@ -1725,9 +1806,10 @@ class Scrcpy(
|
|||||||
private const val CONNECT_RETRY_COUNT = 100
|
private const val CONNECT_RETRY_COUNT = 100
|
||||||
private const val CONNECT_RETRY_DELAY_MS = 100L
|
private const val CONNECT_RETRY_DELAY_MS = 100L
|
||||||
private const val DEVICE_NAME_FIELD_LENGTH = 64
|
private const val DEVICE_NAME_FIELD_LENGTH = 64
|
||||||
private const val PACKET_FLAG_CONFIG = 1L shl 63
|
private const val PACKET_FLAG_SESSION = 1L shl 63
|
||||||
private const val PACKET_FLAG_KEY_FRAME = 1L shl 62
|
private const val PACKET_FLAG_CONFIG = 1L shl 62
|
||||||
private const val PACKET_PTS_MASK = (1L shl 62) - 1
|
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_DISABLED = 0
|
||||||
private const val AUDIO_ERROR = 1
|
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_TOUCH_EVENT = 2
|
||||||
private const val TYPE_INJECT_SCROLL_EVENT = 3
|
private const val TYPE_INJECT_SCROLL_EVENT = 3
|
||||||
private const val TYPE_BACK_OR_SCREEN_ON = 4
|
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_CLIPBOARD = 9
|
||||||
private const val TYPE_SET_DISPLAY_POWER = 10
|
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 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 {
|
private fun socketNameFor(scid: Int): String {
|
||||||
return "scrcpy_%08x".format(scid)
|
return "scrcpy_%08x".format(scid)
|
||||||
|
|||||||
@@ -14,84 +14,89 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
|
|||||||
// 启动 scrcpy 前直接继承自 [ClientOptions],
|
// 启动 scrcpy 前直接继承自 [ClientOptions],
|
||||||
// 不需要默认值
|
// 不需要默认值
|
||||||
data class ServerParams(
|
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,
|
val videoCodec: Codec,
|
||||||
var audioCodec: Codec,
|
val audioCodec: Codec,
|
||||||
var videoSource: VideoSource,
|
val videoSource: VideoSource,
|
||||||
var audioSource: AudioSource,
|
val audioSource: AudioSource,
|
||||||
|
|
||||||
var cameraFacing: CameraFacing,
|
val cameraFacing: CameraFacing,
|
||||||
|
|
||||||
var crop: String,
|
val crop: String,
|
||||||
|
|
||||||
var videoCodecOptions: String,
|
val videoCodecOptions: String,
|
||||||
var audioCodecOptions: String,
|
val audioCodecOptions: String,
|
||||||
var videoEncoder: String,
|
val videoEncoder: String,
|
||||||
var audioEncoder: String,
|
val audioEncoder: String,
|
||||||
|
|
||||||
var cameraId: String,
|
val cameraId: String,
|
||||||
var cameraSize: String,
|
val cameraSize: String,
|
||||||
var cameraAr: String, // aspect ratio
|
val cameraAr: String, // aspect ratio
|
||||||
var cameraFps: UShort,
|
val cameraZoom: String,
|
||||||
|
val cameraFps: UShort,
|
||||||
|
|
||||||
// var portRange: PortRange, // sc_port_range(first, last)
|
// val portRange: PortRange, // sc_port_range(first, last)
|
||||||
// var tunnelHost: UInt,
|
// val tunnelHost: UInt,
|
||||||
// var tunnelPort: UShort,
|
// val tunnelPort: UShort,
|
||||||
|
|
||||||
var maxSize: UShort,
|
val maxSize: UShort,
|
||||||
|
val minSizeAlignment: UByte,
|
||||||
|
|
||||||
var videoBitRate: Int,
|
val videoBitRate: Int,
|
||||||
var audioBitRate: Int,
|
val audioBitRate: Int,
|
||||||
|
|
||||||
var maxFps: String, // float to be parsed by the server
|
val maxFps: String, // float to be parsed by the server
|
||||||
var angle: 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,
|
val captureOrientation: Orientation,
|
||||||
var captureOrientationLock: OrientationLock,
|
val captureOrientationLock: OrientationLock,
|
||||||
|
|
||||||
var control: Boolean,
|
val control: Boolean,
|
||||||
|
|
||||||
var displayId: Int,
|
val displayId: Int,
|
||||||
var newDisplay: String,
|
val newDisplay: String,
|
||||||
|
|
||||||
var displayImePolicy: DisplayImePolicy,
|
val displayImePolicy: DisplayImePolicy,
|
||||||
|
|
||||||
var video: Boolean,
|
val video: Boolean,
|
||||||
var audio: Boolean,
|
val audio: Boolean,
|
||||||
var audioDup: Boolean,
|
val audioDup: Boolean,
|
||||||
var showTouches: Boolean,
|
val showTouches: Boolean,
|
||||||
var stayAwake: Boolean,
|
val stayAwake: Boolean,
|
||||||
|
|
||||||
// var forceAdbForward: Boolean,
|
// val forceAdbForward: Boolean,
|
||||||
|
|
||||||
var powerOffOnClose: Boolean,
|
val powerOffOnClose: Boolean,
|
||||||
var legacyPaste: Boolean,
|
val legacyPaste: Boolean,
|
||||||
var clipboardAutosync: Boolean,
|
val clipboardAutosync: Boolean,
|
||||||
|
|
||||||
var downsizeOnError: Boolean,
|
val downsizeOnError: Boolean,
|
||||||
// var tcpip: Boolean,
|
// val tcpip: Boolean,
|
||||||
// var tcpipDst: String,
|
// val tcpipDst: String,
|
||||||
// var selectUsb: Boolean,
|
// val selectUsb: Boolean,
|
||||||
// var selectTcpip: Boolean,
|
// val selectTcpip: Boolean,
|
||||||
|
|
||||||
var cleanUp: Boolean,
|
val cleanUp: Boolean,
|
||||||
var powerOn: Boolean,
|
val powerOn: Boolean,
|
||||||
|
|
||||||
// var killAdbOnClose: Boolean,
|
// val killAdbOnClose: Boolean,
|
||||||
|
|
||||||
var cameraHighSpeed: Boolean,
|
val cameraHighSpeed: Boolean,
|
||||||
|
val cameraTorch: Boolean,
|
||||||
|
|
||||||
var vdDestroyContent: Boolean,
|
val vdDestroyContent: Boolean,
|
||||||
var vdSystemDecorations: Boolean,
|
val vdSystemDecorations: Boolean,
|
||||||
|
val keepActive: Boolean,
|
||||||
|
val flexDisplay: Boolean,
|
||||||
|
|
||||||
var list: ListOptions,
|
val list: ListOptions,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val SEPARATOR: String = " "
|
const val SEPARATOR: String = " "
|
||||||
@@ -155,6 +160,9 @@ data class ServerParams(
|
|||||||
validate(maxFps)
|
validate(maxFps)
|
||||||
cmd.add("max_fps=${maxFps.trim()}")
|
cmd.add("max_fps=${maxFps.trim()}")
|
||||||
}
|
}
|
||||||
|
if (minSizeAlignment != 1.toUByte()) {
|
||||||
|
cmd.add("min_size_alignment=${minSizeAlignment}")
|
||||||
|
}
|
||||||
if (angle.isNotBlank()) {
|
if (angle.isNotBlank()) {
|
||||||
validate(angle)
|
validate(angle)
|
||||||
cmd.add("angle=${angle.trim()}")
|
cmd.add("angle=${angle.trim()}")
|
||||||
@@ -207,6 +215,13 @@ data class ServerParams(
|
|||||||
if (cameraHighSpeed) {
|
if (cameraHighSpeed) {
|
||||||
cmd.add("camera_high_speed=true")
|
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) {
|
if (showTouches) {
|
||||||
cmd.add("show_touches=true")
|
cmd.add("show_touches=true")
|
||||||
}
|
}
|
||||||
@@ -259,6 +274,9 @@ data class ServerParams(
|
|||||||
validate(newDisplay)
|
validate(newDisplay)
|
||||||
cmd.add("new_display=${newDisplay.trim()}")
|
cmd.add("new_display=${newDisplay.trim()}")
|
||||||
}
|
}
|
||||||
|
if (flexDisplay) {
|
||||||
|
cmd.add("flex_display=true")
|
||||||
|
}
|
||||||
if (displayImePolicy != DisplayImePolicy.UNDEFINED) {
|
if (displayImePolicy != DisplayImePolicy.UNDEFINED) {
|
||||||
cmd.add("display_ime_policy=${displayImePolicy.string}")
|
cmd.add("display_ime_policy=${displayImePolicy.string}")
|
||||||
}
|
}
|
||||||
@@ -268,6 +286,9 @@ data class ServerParams(
|
|||||||
if (!vdSystemDecorations) {
|
if (!vdSystemDecorations) {
|
||||||
cmd.add("vd_system_decorations=false")
|
cmd.add("vd_system_decorations=false")
|
||||||
}
|
}
|
||||||
|
if (keepActive) {
|
||||||
|
cmd.add("keep_active=true")
|
||||||
|
}
|
||||||
if (list has ListOptions.ENCODERS) {
|
if (list has ListOptions.ENCODERS) {
|
||||||
cmd.add("list_encoders=true")
|
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.flow.StateFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -75,6 +76,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
stringPreferencesKey("camera_ar"),
|
stringPreferencesKey("camera_ar"),
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
val CAMERA_ZOOM = Pair(
|
||||||
|
stringPreferencesKey("camera_zoom"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
val CAMERA_FPS = Pair(
|
val CAMERA_FPS = Pair(
|
||||||
intPreferencesKey("camera_fps"),
|
intPreferencesKey("camera_fps"),
|
||||||
0,
|
0,
|
||||||
@@ -107,6 +112,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
stringPreferencesKey("camera_facing"),
|
stringPreferencesKey("camera_facing"),
|
||||||
"any",
|
"any",
|
||||||
)
|
)
|
||||||
|
val MIN_SIZE_ALIGNMENT = Pair(
|
||||||
|
intPreferencesKey("min_size_alignment"),
|
||||||
|
1,
|
||||||
|
)
|
||||||
val MAX_SIZE = Pair(
|
val MAX_SIZE = Pair(
|
||||||
intPreferencesKey("max_size"),
|
intPreferencesKey("max_size"),
|
||||||
0,
|
0,
|
||||||
@@ -275,6 +284,18 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
booleanPreferencesKey("vd_system_decorations"),
|
booleanPreferencesKey("vd_system_decorations"),
|
||||||
true,
|
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(
|
fun defaultBundle() = Bundle(
|
||||||
crop = CROP.defaultValue,
|
crop = CROP.defaultValue,
|
||||||
@@ -288,6 +309,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
cameraSizeCustom = CAMERA_SIZE_CUSTOM.defaultValue,
|
cameraSizeCustom = CAMERA_SIZE_CUSTOM.defaultValue,
|
||||||
cameraSizeUseCustom = CAMERA_SIZE_USE_CUSTOM.defaultValue,
|
cameraSizeUseCustom = CAMERA_SIZE_USE_CUSTOM.defaultValue,
|
||||||
cameraAr = CAMERA_AR.defaultValue,
|
cameraAr = CAMERA_AR.defaultValue,
|
||||||
|
cameraZoom = CAMERA_ZOOM.defaultValue,
|
||||||
cameraFps = CAMERA_FPS.defaultValue,
|
cameraFps = CAMERA_FPS.defaultValue,
|
||||||
logLevel = LOG_LEVEL.defaultValue,
|
logLevel = LOG_LEVEL.defaultValue,
|
||||||
videoCodec = VIDEO_CODEC.defaultValue,
|
videoCodec = VIDEO_CODEC.defaultValue,
|
||||||
@@ -296,6 +318,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
audioSource = AUDIO_SOURCE.defaultValue,
|
audioSource = AUDIO_SOURCE.defaultValue,
|
||||||
recordFormat = RECORD_FORMAT.defaultValue,
|
recordFormat = RECORD_FORMAT.defaultValue,
|
||||||
cameraFacing = CAMERA_FACING.defaultValue,
|
cameraFacing = CAMERA_FACING.defaultValue,
|
||||||
|
minSizeAlignment = MIN_SIZE_ALIGNMENT.defaultValue,
|
||||||
maxSize = MAX_SIZE.defaultValue,
|
maxSize = MAX_SIZE.defaultValue,
|
||||||
videoBitRate = VIDEO_BIT_RATE.defaultValue,
|
videoBitRate = VIDEO_BIT_RATE.defaultValue,
|
||||||
audioBitRate = AUDIO_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,
|
startAppUseCustom = START_APP_USE_CUSTOM.defaultValue,
|
||||||
vdDestroyContent = VD_DESTROY_CONTENT.defaultValue,
|
vdDestroyContent = VD_DESTROY_CONTENT.defaultValue,
|
||||||
vdSystemDecorations = VD_SYSTEM_DECORATIONS.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 cameraSizeCustom: String,
|
||||||
val cameraSizeUseCustom: Boolean,
|
val cameraSizeUseCustom: Boolean,
|
||||||
val cameraAr: String,
|
val cameraAr: String,
|
||||||
|
val cameraZoom: String,
|
||||||
val cameraFps: Int,
|
val cameraFps: Int,
|
||||||
val logLevel: String,
|
val logLevel: String,
|
||||||
val videoCodec: String,
|
val videoCodec: String,
|
||||||
@@ -362,6 +389,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
val audioSource: String,
|
val audioSource: String,
|
||||||
val recordFormat: String,
|
val recordFormat: String,
|
||||||
val cameraFacing: String,
|
val cameraFacing: String,
|
||||||
|
val minSizeAlignment: Int,
|
||||||
val maxSize: Int,
|
val maxSize: Int,
|
||||||
val videoBitRate: Int,
|
val videoBitRate: Int,
|
||||||
val audioBitRate: Int,
|
val audioBitRate: Int,
|
||||||
@@ -403,7 +431,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
val startAppCustom: String,
|
val startAppCustom: String,
|
||||||
val startAppUseCustom: Boolean,
|
val startAppUseCustom: Boolean,
|
||||||
val vdDestroyContent: Boolean,
|
val vdDestroyContent: Boolean,
|
||||||
val vdSystemDecorations: Boolean
|
val vdSystemDecorations: Boolean,
|
||||||
|
val cameraTorch: Boolean,
|
||||||
|
val keepActive: Boolean,
|
||||||
|
val flexDisplay: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +450,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
bundleField(CAMERA_SIZE_CUSTOM) { it.cameraSizeCustom },
|
bundleField(CAMERA_SIZE_CUSTOM) { it.cameraSizeCustom },
|
||||||
bundleField(CAMERA_SIZE_USE_CUSTOM) { it.cameraSizeUseCustom },
|
bundleField(CAMERA_SIZE_USE_CUSTOM) { it.cameraSizeUseCustom },
|
||||||
bundleField(CAMERA_AR) { it.cameraAr },
|
bundleField(CAMERA_AR) { it.cameraAr },
|
||||||
|
bundleField(CAMERA_ZOOM) { it.cameraZoom },
|
||||||
bundleField(CAMERA_FPS) { it.cameraFps },
|
bundleField(CAMERA_FPS) { it.cameraFps },
|
||||||
bundleField(LOG_LEVEL) { it.logLevel },
|
bundleField(LOG_LEVEL) { it.logLevel },
|
||||||
bundleField(VIDEO_CODEC) { it.videoCodec },
|
bundleField(VIDEO_CODEC) { it.videoCodec },
|
||||||
@@ -427,6 +459,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
bundleField(AUDIO_SOURCE) { it.audioSource },
|
bundleField(AUDIO_SOURCE) { it.audioSource },
|
||||||
bundleField(RECORD_FORMAT) { it.recordFormat },
|
bundleField(RECORD_FORMAT) { it.recordFormat },
|
||||||
bundleField(CAMERA_FACING) { it.cameraFacing },
|
bundleField(CAMERA_FACING) { it.cameraFacing },
|
||||||
|
bundleField(MIN_SIZE_ALIGNMENT) { it.minSizeAlignment },
|
||||||
bundleField(MAX_SIZE) { it.maxSize },
|
bundleField(MAX_SIZE) { it.maxSize },
|
||||||
bundleField(VIDEO_BIT_RATE) { it.videoBitRate },
|
bundleField(VIDEO_BIT_RATE) { it.videoBitRate },
|
||||||
bundleField(AUDIO_BIT_RATE) { it.audioBitRate },
|
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(START_APP_USE_CUSTOM) { it.startAppUseCustom },
|
||||||
bundleField(VD_DESTROY_CONTENT) { it.vdDestroyContent },
|
bundleField(VD_DESTROY_CONTENT) { it.vdDestroyContent },
|
||||||
bundleField(VD_SYSTEM_DECORATIONS) { it.vdSystemDecorations },
|
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)
|
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
|
||||||
@@ -484,6 +520,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
cameraSizeCustom = preferences.read(CAMERA_SIZE_CUSTOM),
|
cameraSizeCustom = preferences.read(CAMERA_SIZE_CUSTOM),
|
||||||
cameraSizeUseCustom = preferences.read(CAMERA_SIZE_USE_CUSTOM),
|
cameraSizeUseCustom = preferences.read(CAMERA_SIZE_USE_CUSTOM),
|
||||||
cameraAr = preferences.read(CAMERA_AR),
|
cameraAr = preferences.read(CAMERA_AR),
|
||||||
|
cameraZoom = preferences.read(CAMERA_ZOOM),
|
||||||
cameraFps = preferences.read(CAMERA_FPS),
|
cameraFps = preferences.read(CAMERA_FPS),
|
||||||
logLevel = preferences.read(LOG_LEVEL),
|
logLevel = preferences.read(LOG_LEVEL),
|
||||||
videoCodec = preferences.read(VIDEO_CODEC),
|
videoCodec = preferences.read(VIDEO_CODEC),
|
||||||
@@ -492,6 +529,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
audioSource = preferences.read(AUDIO_SOURCE),
|
audioSource = preferences.read(AUDIO_SOURCE),
|
||||||
recordFormat = preferences.read(RECORD_FORMAT),
|
recordFormat = preferences.read(RECORD_FORMAT),
|
||||||
cameraFacing = preferences.read(CAMERA_FACING),
|
cameraFacing = preferences.read(CAMERA_FACING),
|
||||||
|
minSizeAlignment = preferences.read(MIN_SIZE_ALIGNMENT),
|
||||||
maxSize = preferences.read(MAX_SIZE),
|
maxSize = preferences.read(MAX_SIZE),
|
||||||
videoBitRate = preferences.read(VIDEO_BIT_RATE),
|
videoBitRate = preferences.read(VIDEO_BIT_RATE),
|
||||||
audioBitRate = preferences.read(AUDIO_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),
|
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),
|
||||||
|
cameraTorch = preferences.read(CAMERA_TORCH),
|
||||||
|
keepActive = preferences.read(KEEP_ACTIVE),
|
||||||
|
flexDisplay = preferences.read(FLEX_DISPLAY),
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun loadBundle() = loadBundle(::bundleFromPreferences)
|
suspend fun loadBundle() = loadBundle(::bundleFromPreferences)
|
||||||
@@ -564,6 +605,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
cameraId = bundle.cameraId,
|
cameraId = bundle.cameraId,
|
||||||
cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom,
|
cameraSize = if (!bundle.cameraSizeUseCustom) bundle.cameraSize else bundle.cameraSizeCustom,
|
||||||
cameraAr = bundle.cameraAr,
|
cameraAr = bundle.cameraAr,
|
||||||
|
cameraZoom = bundle.cameraZoom,
|
||||||
cameraFps = bundle.cameraFps.toUShort(),
|
cameraFps = bundle.cameraFps.toUShort(),
|
||||||
logLevel = LogLevel.fromString(bundle.logLevel),
|
logLevel = LogLevel.fromString(bundle.logLevel),
|
||||||
videoCodec = Codec.fromString(bundle.videoCodec),
|
videoCodec = Codec.fromString(bundle.videoCodec),
|
||||||
@@ -572,6 +614,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
audioSource = AudioSource.fromString(bundle.audioSource),
|
audioSource = AudioSource.fromString(bundle.audioSource),
|
||||||
recordFormat = RecordFormat.fromString(bundle.recordFormat),
|
recordFormat = RecordFormat.fromString(bundle.recordFormat),
|
||||||
cameraFacing = CameraFacing.fromString(bundle.cameraFacing),
|
cameraFacing = CameraFacing.fromString(bundle.cameraFacing),
|
||||||
|
minSizeAlignment = bundle.minSizeAlignment.toUByte(),
|
||||||
maxSize = bundle.maxSize.toUShort(),
|
maxSize = bundle.maxSize.toUShort(),
|
||||||
videoBitRate = bundle.videoBitRate,
|
videoBitRate = bundle.videoBitRate,
|
||||||
audioBitRate = bundle.audioBitRate,
|
audioBitRate = bundle.audioBitRate,
|
||||||
@@ -611,6 +654,360 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
|||||||
newDisplay = bundle.newDisplay,
|
newDisplay = bundle.newDisplay,
|
||||||
startApp = if (!bundle.startAppUseCustom) bundle.startApp else bundle.startAppCustom,
|
startApp = if (!bundle.startAppUseCustom) bundle.startApp else bundle.startAppCustom,
|
||||||
vdDestroyContent = bundle.vdDestroyContent,
|
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()
|
JSONObject()
|
||||||
.put("id", profile.id)
|
.put("id", profile.id)
|
||||||
.put("name", profile.name)
|
.put("name", profile.name)
|
||||||
.put("bundle", encodeBundle(profile.bundle))
|
.put("bundle", encodeBundleToJson(profile.bundle))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return array.toString()
|
return array.toString()
|
||||||
@@ -234,7 +234,7 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
|
|||||||
Profile(
|
Profile(
|
||||||
id = id,
|
id = id,
|
||||||
name = name.ifBlank { textNewProfile },
|
name = name.ifBlank { textNewProfile },
|
||||||
bundle = decodeBundle(item.optJSONObject("bundle")),
|
bundle = decodeBundleFromJson(item.optJSONObject("bundle")),
|
||||||
isBuiltinGlobal = id == ScrcpyOptions.GLOBAL_PROFILE_ID,
|
isBuiltinGlobal = id == ScrcpyOptions.GLOBAL_PROFILE_ID,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -242,331 +242,4 @@ class ScrcpyProfiles(context: Context) : Settings(context, "ScrcpyProfiles") {
|
|||||||
}
|
}
|
||||||
return State(profiles)
|
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_no_power_on">scrcpy 启动时不唤醒屏幕</string>
|
||||||
<string name="scrcpyopt_power_off_on_close">scrcpy 结束后息屏</string>
|
<string name="scrcpyopt_power_off_on_close">scrcpy 结束后息屏</string>
|
||||||
<string name="scrcpyopt_stay_awake">scrcpy 启动后保持设备唤醒状态(插入电源)</string>
|
<string name="scrcpyopt_stay_awake">scrcpy 启动后保持设备唤醒状态(插入电源)</string>
|
||||||
|
<string name="scrcpyopt_keep_active">模拟用户活动防止息屏</string>
|
||||||
<string name="scrcpyopt_show_touches">显示物理触控</string>
|
<string name="scrcpyopt_show_touches">显示物理触控</string>
|
||||||
<string name="scrcpyopt_fullscreen">启动后进入全屏</string>
|
<string name="scrcpyopt_fullscreen">启动后进入全屏</string>
|
||||||
<string name="scrcpyopt_disable_screensaver">scrcpy 启动后保持本机屏幕常亮</string>
|
<string name="scrcpyopt_disable_screensaver">scrcpy 启动后保持本机屏幕常亮</string>
|
||||||
@@ -485,6 +486,8 @@
|
|||||||
<string name="scrcpyopt_video_bitrate">视频码率</string>
|
<string name="scrcpyopt_video_bitrate">视频码率</string>
|
||||||
<string name="scrcpyopt_video_source">视频来源</string>
|
<string name="scrcpyopt_video_source">视频来源</string>
|
||||||
<string name="scrcpyopt_display_id">监视器 ID</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_size">最大分辨率</string>
|
||||||
<string name="scrcpyopt_max_fps">最大帧率</string>
|
<string name="scrcpyopt_max_fps">最大帧率</string>
|
||||||
<string name="scrcpyopt_camera_id">摄像头 ID</string>
|
<string name="scrcpyopt_camera_id">摄像头 ID</string>
|
||||||
@@ -492,6 +495,7 @@
|
|||||||
<string name="scrcpyopt_camera_size">摄像头分辨率</string>
|
<string name="scrcpyopt_camera_size">摄像头分辨率</string>
|
||||||
<string name="scrcpyopt_camera_fps">摄像头帧率</string>
|
<string name="scrcpyopt_camera_fps">摄像头帧率</string>
|
||||||
<string name="scrcpyopt_camera_high_speed">高帧率模式</string>
|
<string name="scrcpyopt_camera_high_speed">高帧率模式</string>
|
||||||
|
<string name="scrcpyopt_camera_torch">相机手电筒</string>
|
||||||
<string name="scrcpyopt_audio_source">音频来源</string>
|
<string name="scrcpyopt_audio_source">音频来源</string>
|
||||||
<string name="scrcpyopt_audio_dup">音频双路输出</string>
|
<string name="scrcpyopt_audio_dup">音频双路输出</string>
|
||||||
<string name="scrcpyopt_require_audio">音频转发失败时终止</string>
|
<string name="scrcpyopt_require_audio">音频转发失败时终止</string>
|
||||||
@@ -509,6 +513,7 @@
|
|||||||
<string name="scrcpyopt_no_mouse_hover">禁用鼠标悬停转发</string>
|
<string name="scrcpyopt_no_mouse_hover">禁用鼠标悬停转发</string>
|
||||||
<string name="scrcpyopt_no_cleanup">禁用结束后清理</string>
|
<string name="scrcpyopt_no_cleanup">禁用结束后清理</string>
|
||||||
<string name="scrcpyopt_no_cleanup_desc">默认情况下,scrcpy 会从设备中移除服务器二进制文件,并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)\n此选项将禁用此清理操作</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_start_app">scrcpy 启动后打开应用</string>
|
||||||
<string name="scrcpyopt_native">本机</string>
|
<string name="scrcpyopt_native">本机</string>
|
||||||
<string name="scrcpyopt_log_level">日志等级</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_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_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_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_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_fullscreen">Start in fullscreen</string>
|
||||||
<string name="scrcpyopt_disable_screensaver">Disable screensaver while scrcpy is running</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_bitrate">Video bitrate</string>
|
||||||
<string name="scrcpyopt_video_source">Video source</string>
|
<string name="scrcpyopt_video_source">Video source</string>
|
||||||
<string name="scrcpyopt_display_id">Display ID</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_size">Max resolution</string>
|
||||||
<string name="scrcpyopt_max_fps">Max framerate</string>
|
<string name="scrcpyopt_max_fps">Max framerate</string>
|
||||||
<string name="scrcpyopt_camera_id">Camera ID</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_size">Camera resolution</string>
|
||||||
<string name="scrcpyopt_camera_fps">Camera framerate</string>
|
<string name="scrcpyopt_camera_fps">Camera framerate</string>
|
||||||
<string name="scrcpyopt_camera_high_speed">High framerate mode</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_source">Audio source</string>
|
||||||
<string name="scrcpyopt_audio_dup">Duplicate audio (capture and keep playing on the device)</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>
|
<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_mouse_hover">Do not forward mouse hover events</string>
|
||||||
<string name="scrcpyopt_no_cleanup">Disable cleanup on exit</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_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_start_app">Open app after scrcpy starts</string>
|
||||||
<string name="scrcpyopt_native">Native</string>
|
<string name="scrcpyopt_native">Native</string>
|
||||||
<string name="scrcpyopt_log_level">Log level</string>
|
<string name="scrcpyopt_log_level">Log level</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user