upstream: scrcpy v4.1 (#59)

编译期下载 URL/版本/SHA256 更新至 v4.1
支持 VP8 与 VP9 解码
支持 ignoreVideoEncoderConstraints 参数
AnnexBDecoder.kt 重命名至 MediaCodecVideoDecoder.kt,避免在使用 vp8/vp9 时的语义问题
This commit is contained in:
謬紗特
2026-07-13 13:53:46 +08:00
committed by GitHub
parent d1e20c2a6b
commit d1e9b8cfdc
13 changed files with 99 additions and 45 deletions

View File

@@ -1,5 +1,10 @@
# Change Log # Change Log
## 0.5.0
- 依赖: scrcpy 升级至 v4.1
- 新增: vp8/vp9 解码
## 0.4.5 ## 0.4.5
- 重构: 改了好多不知道怎么写,总之修复了一些已知问题 - 重构: 改了好多不知道怎么写,总之修复了一些已知问题

View File

@@ -59,8 +59,8 @@ android {
applicationId = "io.github.miuzarte.scrcpyforandroid" applicationId = "io.github.miuzarte.scrcpyforandroid"
minSdk = 26 minSdk = 26
targetSdk = 37 targetSdk = 37
versionCode = 35 versionCode = 36
versionName = "0.4.5_pre1" versionName = "0.5.0"
externalNativeBuild { externalNativeBuild {
cmake { cmake {
@@ -168,9 +168,9 @@ dependencies {
} }
val scrcpyServerAssetDir = "${project.projectDir}/src/main/assets/bin" val scrcpyServerAssetDir = "${project.projectDir}/src/main/assets/bin"
val scrcpyServerAssetFile = "$scrcpyServerAssetDir/scrcpy-server-v4.0" val scrcpyServerAssetFile = "$scrcpyServerAssetDir/scrcpy-server-v4.1"
val scrcpyServerDownloadUrl = "https://github.com/Genymobile/scrcpy/releases/download/v4.0/scrcpy-server-v4.0" val scrcpyServerDownloadUrl = "https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-server-v4.1"
val scrcpyServerSha256 = "84924bd564a1eb6089c872c7521f968058977f91f5ff02514a8c74aff3210f3a" val scrcpyServerSha256 = "deacb991ed2509715160ffdc7907e47b4160eb30d1566217e9047fd5b8850cae"
val downloadScrcpyServer by tasks.registering { val downloadScrcpyServer by tasks.registering {
description = "Download scrcpy-server binary from GitHub releases if absent or SHA256 mismatch" description = "Download scrcpy-server binary from GitHub releases if absent or SHA256 mismatch"
@@ -203,7 +203,7 @@ val downloadScrcpyServer by tasks.registering {
val needsDownload = !file.exists() || computeSha256(file) != expectedSha val needsDownload = !file.exists() || computeSha256(file) != expectedSha
if (needsDownload) { if (needsDownload) {
logger.lifecycle("Downloading scrcpy-server-v4.0 from GitHub releases...") logger.lifecycle("Downloading scrcpy-server-v4.1 from GitHub releases...")
try { try {
URI(url).toURL().openStream().use { input -> URI(url).toURL().openStream().use { input ->
file.outputStream().use { output -> file.outputStream().use { output ->
@@ -212,7 +212,7 @@ val downloadScrcpyServer by tasks.registering {
} }
} catch (e: Exception) { } catch (e: Exception) {
throw GradleException( throw GradleException(
"Failed to download scrcpy-server-v4.0 from GitHub releases.\n" + "Failed to download scrcpy-server-v4.1 from GitHub releases.\n" +
" URL: $url\n" + " URL: $url\n" +
" You may download it manually and place it at: ${file.absolutePath}\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" + " If you are behind a proxy, check your Gradle proxy settings\n" +
@@ -223,14 +223,14 @@ val downloadScrcpyServer by tasks.registering {
val actualSha = computeSha256(file) val actualSha = computeSha256(file)
require(actualSha == expectedSha) { require(actualSha == expectedSha) {
"SHA256 mismatch for scrcpy-server-v4.0!\n" + "SHA256 mismatch for scrcpy-server-v4.1!\n" +
" Expected: $expectedSha\n" + " Expected: $expectedSha\n" +
" Got: $actualSha\n" + " Got: $actualSha\n" +
" Delete ${file.absolutePath} to retry download." " Delete ${file.absolutePath} to retry download."
} }
logger.lifecycle("scrcpy-server-v4.0 downloaded and verified.") logger.lifecycle("scrcpy-server-v4.1 downloaded and verified.")
} else { } else {
logger.lifecycle("scrcpy-server-v4.0 exists with correct SHA256, skip download.") logger.lifecycle("scrcpy-server-v4.1 exists with correct SHA256, skip download.")
} }
} }
} }

View File

@@ -210,7 +210,7 @@ object NativeCoreFacade {
val info = scrcpy.currentSessionState.value ?: return@withLock val info = scrcpy.currentSessionState.value ?: return@withLock
if (info.width <= 0 || info.height <= 0) return@withLock if (info.width <= 0 || info.height <= 0) return@withLock
val mime = mimeForCodec(info.codec) val mime = info.codec?.mime ?: "video/avc"
if (!DecoderCapabilities.isSizeSupported(mime, info.width, info.height)) { if (!DecoderCapabilities.isSizeSupported(mime, info.width, info.height)) {
Log.w( Log.w(
TAG, TAG,
@@ -323,12 +323,5 @@ object NativeCoreFacade {
runCatching { scrcpy.start(options) } runCatching { scrcpy.start(options) }
} }
private fun mimeForCodec(codec: Codec?): String = when (codec) {
Codec.H264 -> "video/avc"
Codec.H265 -> "video/hevc"
Codec.AV1 -> "video/av01"
else -> "video/avc"
}
private const val TAG = "NativeCoreFacade" private const val TAG = "NativeCoreFacade"
} }

View File

@@ -6,19 +6,21 @@ import android.util.Log
import android.view.Surface import android.view.Surface
/** /**
* AnnexBDecoder * MediaCodecVideoDecoder
* *
* Purpose: * Purpose:
* - Wraps Android MediaCodec for Annex-B framed codecs (H.264/H.265/AV1). * - Wraps Android MediaCodec for all video codecs (H.264/H.265/AV1/VP8/VP9).
* - Handles critical startup packets (config/keyframes) and provides callbacks * - Handles critical startup packets (config/keyframes) and provides callbacks
* for output size changes and FPS updates. * for output size changes and FPS updates.
* - Codecs without Annex-B config packets (VP8/VP9) work natively: no CSD is
* required for MediaCodec initialization.
* *
* Threading / safety: * Threading / safety:
* - Public methods are synchronized to allow calls from multiple threads * - Public methods are synchronized to allow calls from multiple threads
* (packet producer vs. teardown). Internally, MediaCodec callbacks and * (packet producer vs. teardown). Internally, MediaCodec callbacks and
* buffer queues are used on the calling thread. * buffer queues are used on the calling thread.
*/ */
class AnnexBDecoder( class MediaCodecVideoDecoder(
width: Int, width: Int,
height: Int, height: Int,
outputSurface: Surface, outputSurface: Surface,
@@ -232,7 +234,7 @@ class AnnexBDecoder(
} }
companion object { companion object {
private const val TAG = "AnnexBDecoder" private const val TAG = "MediaCodecVideoDecoder"
private const val MIME_AVC = "video/avc" private const val MIME_AVC = "video/avc"
private const val INPUT_TIMEOUT_US = 10_000L private const val INPUT_TIMEOUT_US = 10_000L
private const val CRITICAL_INPUT_TIMEOUT_US = 50_000L private const val CRITICAL_INPUT_TIMEOUT_US = 50_000L

View File

@@ -13,7 +13,7 @@ import java.util.ArrayDeque
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
/** /**
* Owns the video [AnnexBDecoder] lifecycle and all state surrounding it: * Owns the video [MediaCodecVideoDecoder] lifecycle and all state surrounding it:
* *
* - Creating / rebuilding / releasing the decoder bound to the persistent renderer surface. * - Creating / rebuilding / releasing the decoder bound to the persistent renderer surface.
* - Caching and replaying bootstrap packets (config + keyframe + following frames) so a * - Caching and replaying bootstrap packets (config + keyframe + following frames) so a
@@ -27,7 +27,7 @@ import java.util.concurrent.CopyOnWriteArraySet
* - All public methods are safe to call from any thread; mutations are guarded by * - All public methods are safe to call from any thread; mutations are guarded by
* [controllerLock] for synchronous methods and by the facade's session mutex for * [controllerLock] for synchronous methods and by the facade's session mutex for
* suspend methods. * suspend methods.
* - The decoder itself is single-threaded (AnnexBDecoder is @Synchronized). * - The decoder itself is single-threaded (MediaCodecVideoDecoder is @Synchronized).
* *
* This class does **not** decide session restart policy; it reports errors and lets * This class does **not** decide session restart policy; it reports errors and lets
* [NativeCoreFacade] decide whether to restart the scrcpy session. * [NativeCoreFacade] decide whether to restart the scrcpy session.
@@ -38,7 +38,7 @@ internal class VideoDecoderController(
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private val controllerLock = Any() private val controllerLock = Any()
private var decoder: AnnexBDecoder? = null private var decoder: MediaCodecVideoDecoder? = null
@Volatile @Volatile
private var currentSessionInfo: Scrcpy.Session.SessionInfo? = null private var currentSessionInfo: Scrcpy.Session.SessionInfo? = null
@@ -239,7 +239,7 @@ internal class VideoDecoderController(
} }
/** /**
* Core method: construct a new [AnnexBDecoder] and replay bootstrap packets into it. * Core method: construct a new [MediaCodecVideoDecoder] and replay bootstrap packets into it.
* *
* Caller must have already released the old decoder (if any) and, when needed, recreated * Caller must have already released the old decoder (if any) and, when needed, recreated
* the persistent surface. Must be called while holding [controllerLock]. * the persistent surface. Must be called while holding [controllerLock].
@@ -255,12 +255,7 @@ internal class VideoDecoderController(
} }
val surface = renderer.getDecoderSurface() val surface = renderer.getDecoderSurface()
val mime = when (session.codec) { val mime = session.codec?.mime ?: "video/avc"
Codec.H264 -> "video/avc"
Codec.H265 -> "video/hevc"
Codec.AV1 -> "video/av01"
else -> "video/avc"
}
Log.i( Log.i(
TAG, TAG,
"createOrReplaceDecoder(): codec=${session.codec?.string ?: "null"}, " + "createOrReplaceDecoder(): codec=${session.codec?.string ?: "null"}, " +
@@ -269,7 +264,7 @@ internal class VideoDecoderController(
"persistent=true", "persistent=true",
) )
val newDecoder = try { val newDecoder = try {
AnnexBDecoder( MediaCodecVideoDecoder(
width = session.width, width = session.width,
height = session.height, height = session.height,
outputSurface = surface, outputSurface = surface,
@@ -277,7 +272,7 @@ internal class VideoDecoderController(
onOutputSizeChanged = { width, height -> onOutputSizeChanged = { width, height ->
val current = currentSessionInfo val current = currentSessionInfo
if (current == null || (current.width == width && current.height == height)) { if (current == null || (current.width == width && current.height == height)) {
return@AnnexBDecoder return@MediaCodecVideoDecoder
} }
Log.i( Log.i(
TAG, TAG,
@@ -340,7 +335,7 @@ internal class VideoDecoderController(
// ---------- Bootstrap packet cache ---------- // ---------- Bootstrap packet cache ----------
private fun replayBootstrapPackets(decoder: AnnexBDecoder) { private fun replayBootstrapPackets(decoder: MediaCodecVideoDecoder) {
val snapshot = synchronized(bootstrapLock) { bootstrapPackets.toList() } val snapshot = synchronized(bootstrapLock) { bootstrapPackets.toList() }
if (snapshot.isEmpty()) { if (snapshot.isEmpty()) {
return return

View File

@@ -1779,6 +1779,16 @@ internal fun ScrcpyAllOptionsPage(
) )
}, },
) )
SwitchPreference(
title = stringResource(R.string.scrcpyopt_ignore_video_encoder_constraints),
summary = "--ignore-video-encoder-constraints",
checked = soBundle.ignoreVideoEncoderConstraints,
onCheckedChange = {
soBundle = soBundle.copy(
ignoreVideoEncoderConstraints = it,
)
},
)
} }
} }

View File

@@ -212,6 +212,12 @@ data class ClientOptions(
// --flex-display // --flex-display
var flexDisplay: Boolean = false, // to server var flexDisplay: Boolean = false, // to server
// --ignore-video-encoder-constraints
var ignoreVideoEncoderConstraints: Boolean = false, // to server
// --no-terminal-title
// var updateTerminalTitle: Boolean = true,
) { ) {
enum class KeyInjectMode(val string: String) { enum class KeyInjectMode(val string: String) {
MIXED("mixed"), MIXED("mixed"),
@@ -633,6 +639,7 @@ data class ClientOptions(
vdSystemDecorations = vdSystemDecorations, vdSystemDecorations = vdSystemDecorations,
keepActive = keepActive, keepActive = keepActive,
flexDisplay = flexDisplay, flexDisplay = flexDisplay,
ignoreVideoEncoderConstraints = ignoreVideoEncoderConstraints,
list = list, list = list,
) )
} }

View File

@@ -116,9 +116,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-v4.0" const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v4.1"
const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v4.0" const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v4.1"
const val DEFAULT_SERVER_VERSION = "4.0" const val DEFAULT_SERVER_VERSION = "4.1"
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
@@ -389,6 +389,10 @@ class Scrcpy(
session.setClipboard(text, paste) session.setClipboard(text, paste)
} }
suspend fun scanFile(path: String) = withContext(Dispatchers.IO) {
session.scanFile(path)
}
suspend fun injectTouch( suspend fun injectTouch(
action: Int, action: Int,
pointerId: Long, pointerId: Long,
@@ -1317,6 +1321,10 @@ class Scrcpy(
withControlWriter("setClipboard") { setClipboard(text, paste) } withControlWriter("setClipboard") { setClipboard(text, paste) }
} }
suspend fun scanFile(path: String) = mutex.withLock {
withControlWriter("scanFile") { scanFile(path) }
}
suspend fun injectTouch( suspend fun injectTouch(
action: Int, action: Int,
pointerId: Long, pointerId: Long,
@@ -1775,6 +1783,16 @@ class Scrcpy(
output.flush() output.flush()
} }
@Synchronized
fun scanFile(path: String) {
val bytes = path.toByteArray(Charsets.UTF_8)
require(bytes.size <= 256) { "scan file path is too long (max 256 bytes)" }
output.writeByte(TYPE_SCAN_FILE)
output.writeInt(bytes.size)
output.write(bytes)
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)
@@ -1836,6 +1854,7 @@ class Scrcpy(
private const val TYPE_CAMERA_ZOOM_IN = 19 private const val TYPE_CAMERA_ZOOM_IN = 19
private const val TYPE_CAMERA_ZOOM_OUT = 20 private const val TYPE_CAMERA_ZOOM_OUT = 20
private const val TYPE_RESIZE_DISPLAY = 21 private const val TYPE_RESIZE_DISPLAY = 21
private const val TYPE_SCAN_FILE = 22
private const val SEQUENCE_INVALID = 0L private const val SEQUENCE_INVALID = 0L

View File

@@ -86,6 +86,7 @@ data class ServerParams(
val vdSystemDecorations: Boolean, val vdSystemDecorations: Boolean,
val keepActive: Boolean, val keepActive: Boolean,
val flexDisplay: Boolean, val flexDisplay: Boolean,
val ignoreVideoEncoderConstraints: Boolean,
val list: ListOptions, val list: ListOptions,
) { ) {
@@ -268,6 +269,9 @@ data class ServerParams(
if (flexDisplay) { if (flexDisplay) {
cmd.add("flex_display=true") cmd.add("flex_display=true")
} }
if (ignoreVideoEncoderConstraints) {
cmd.add("ignore_video_encoder_constraints=true")
}
if (displayImePolicy != DisplayImePolicy.UNDEFINED) { if (displayImePolicy != DisplayImePolicy.UNDEFINED) {
cmd.add("display_ime_policy=${displayImePolicy.string}") cmd.add("display_ime_policy=${displayImePolicy.string}")
} }

View File

@@ -82,18 +82,21 @@ class Shared {
enum class Codec( enum class Codec(
val string: String, val string: String,
val displayName: String, val displayName: String,
val mime: String,
val type: Type, val type: Type,
val id: Int, val id: Int,
val isLossless: Boolean, val isLossless: Boolean,
) { ) {
H264("h264", "H.264", Type.VIDEO, 0x68323634, false), // default, ignore when passing H264("h264", "H.264", "video/avc", Type.VIDEO, 0x68323634, false), // default, ignore when passing
H265("h265", "H.265", Type.VIDEO, 0x68323635, false), H265("h265", "H.265", "video/hevc", Type.VIDEO, 0x68323635, false),
AV1("av1", "AV1", Type.VIDEO, 0x00617631, false), AV1("av1", "AV1", "video/av01", Type.VIDEO, 0x00617631, false),
VP8("vp8", "VP8", "video/x-vnd.on2.vp8", Type.VIDEO, 0x00767038, false),
VP9("vp9", "VP9", "video/x-vnd.on2.vp9", Type.VIDEO, 0x00767039, false),
OPUS("opus", "Opus", Type.AUDIO, 0x6f707573, false), // default, ignore when passing OPUS("opus", "Opus", "audio/opus", Type.AUDIO, 0x6f707573, false), // default, ignore when passing
AAC("aac", "AAC", Type.AUDIO, 0x00616163, false), AAC("aac", "AAC", "audio/mp4a-latm", Type.AUDIO, 0x00616163, false),
FLAC("flac", "FLAC", Type.AUDIO, 0x666c6163, true), FLAC("flac", "FLAC", "audio/flac", Type.AUDIO, 0x666c6163, true),
RAW("raw", "RAW", Type.AUDIO, 0x00726177, true); // wav raw RAW("raw", "RAW", "audio/raw", Type.AUDIO, 0x00726177, true); // wav raw
enum class Type { VIDEO, AUDIO } enum class Type { VIDEO, AUDIO }

View File

@@ -283,6 +283,10 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
booleanPreferencesKey("flex_display"), booleanPreferencesKey("flex_display"),
false, false,
) )
val IGNORE_VIDEO_ENCODER_CONSTRAINTS = Pair(
booleanPreferencesKey("ignore_video_encoder_constraints"),
false,
)
fun defaultBundle() = Bundle( fun defaultBundle() = Bundle(
crop = CROP.defaultValue, crop = CROP.defaultValue,
@@ -351,6 +355,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
cameraTorch = CAMERA_TORCH.defaultValue, cameraTorch = CAMERA_TORCH.defaultValue,
keepActive = KEEP_ACTIVE.defaultValue, keepActive = KEEP_ACTIVE.defaultValue,
flexDisplay = FLEX_DISPLAY.defaultValue, flexDisplay = FLEX_DISPLAY.defaultValue,
ignoreVideoEncoderConstraints = IGNORE_VIDEO_ENCODER_CONSTRAINTS.defaultValue,
) )
} }
@@ -422,6 +427,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
val cameraTorch: Boolean, val cameraTorch: Boolean,
val keepActive: Boolean, val keepActive: Boolean,
val flexDisplay: Boolean, val flexDisplay: Boolean,
val ignoreVideoEncoderConstraints: Boolean,
): Parcelable { ): Parcelable {
} }
@@ -491,6 +497,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
bundleField(CAMERA_TORCH) { it.cameraTorch }, bundleField(CAMERA_TORCH) { it.cameraTorch },
bundleField(KEEP_ACTIVE) { it.keepActive }, bundleField(KEEP_ACTIVE) { it.keepActive },
bundleField(FLEX_DISPLAY) { it.flexDisplay }, bundleField(FLEX_DISPLAY) { it.flexDisplay },
bundleField(IGNORE_VIDEO_ENCODER_CONSTRAINTS) { it.ignoreVideoEncoderConstraints },
) )
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences) val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
@@ -562,6 +569,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
cameraTorch = preferences.read(CAMERA_TORCH), cameraTorch = preferences.read(CAMERA_TORCH),
keepActive = preferences.read(KEEP_ACTIVE), keepActive = preferences.read(KEEP_ACTIVE),
flexDisplay = preferences.read(FLEX_DISPLAY), flexDisplay = preferences.read(FLEX_DISPLAY),
ignoreVideoEncoderConstraints = preferences.read(IGNORE_VIDEO_ENCODER_CONSTRAINTS),
) )
suspend fun loadBundle() = loadBundle(::bundleFromPreferences) suspend fun loadBundle() = loadBundle(::bundleFromPreferences)
@@ -645,6 +653,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
cameraTorch = bundle.cameraTorch, cameraTorch = bundle.cameraTorch,
keepActive = bundle.keepActive, keepActive = bundle.keepActive,
flexDisplay = bundle.flexDisplay, flexDisplay = bundle.flexDisplay,
ignoreVideoEncoderConstraints = bundle.ignoreVideoEncoderConstraints,
) )
} }
@@ -716,6 +725,7 @@ internal fun encodeBundleToJson(bundle: ScrcpyOptions.Bundle): JSONObject =
.put("cameraTorch", bundle.cameraTorch) .put("cameraTorch", bundle.cameraTorch)
.put("keepActive", bundle.keepActive) .put("keepActive", bundle.keepActive)
.put("flexDisplay", bundle.flexDisplay) .put("flexDisplay", bundle.flexDisplay)
.put("ignoreVideoEncoderConstraints", bundle.ignoreVideoEncoderConstraints)
internal fun decodeBundleFromJson(bundleJson: JSONObject?): ScrcpyOptions.Bundle { internal fun decodeBundleFromJson(bundleJson: JSONObject?): ScrcpyOptions.Bundle {
val json = bundleJson ?: return ScrcpyOptions.defaultBundle() val json = bundleJson ?: return ScrcpyOptions.defaultBundle()
@@ -984,6 +994,10 @@ internal fun decodeBundleFromJson(bundleJson: JSONObject?): ScrcpyOptions.Bundle
"flexDisplay", "flexDisplay",
ScrcpyOptions.FLEX_DISPLAY.defaultValue, ScrcpyOptions.FLEX_DISPLAY.defaultValue,
), ),
ignoreVideoEncoderConstraints = json.optBooleanOrDefault(
"ignoreVideoEncoderConstraints",
ScrcpyOptions.IGNORE_VIDEO_ENCODER_CONSTRAINTS.defaultValue,
),
) )
} }

View File

@@ -536,6 +536,7 @@
<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_flex_display">自动调整尺寸以匹配界面</string>
<string name="scrcpyopt_ignore_video_encoder_constraints">忽略视频编码器约束</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>

View File

@@ -537,6 +537,7 @@
<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_flex_display">Automatically resize to match the screen</string>
<string name="scrcpyopt_ignore_video_encoder_constraints">Ignore video encoder constraints</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>