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

@@ -59,8 +59,8 @@ android {
applicationId = "io.github.miuzarte.scrcpyforandroid"
minSdk = 26
targetSdk = 37
versionCode = 35
versionName = "0.4.5_pre1"
versionCode = 36
versionName = "0.5.0"
externalNativeBuild {
cmake {
@@ -168,9 +168,9 @@ dependencies {
}
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 scrcpyServerAssetFile = "$scrcpyServerAssetDir/scrcpy-server-v4.1"
val scrcpyServerDownloadUrl = "https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-server-v4.1"
val scrcpyServerSha256 = "deacb991ed2509715160ffdc7907e47b4160eb30d1566217e9047fd5b8850cae"
val downloadScrcpyServer by tasks.registering {
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
if (needsDownload) {
logger.lifecycle("Downloading scrcpy-server-v4.0 from GitHub releases...")
logger.lifecycle("Downloading scrcpy-server-v4.1 from GitHub releases...")
try {
URI(url).toURL().openStream().use { input ->
file.outputStream().use { output ->
@@ -212,7 +212,7 @@ val downloadScrcpyServer by tasks.registering {
}
} catch (e: Exception) {
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" +
" You may download it manually and place it at: ${file.absolutePath}\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)
require(actualSha == expectedSha) {
"SHA256 mismatch for scrcpy-server-v4.0!\n" +
"SHA256 mismatch for scrcpy-server-v4.1!\n" +
" Expected: $expectedSha\n" +
" Got: $actualSha\n" +
" 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 {
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
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)) {
Log.w(
TAG,
@@ -323,12 +323,5 @@ object NativeCoreFacade {
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"
}

View File

@@ -6,19 +6,21 @@ import android.util.Log
import android.view.Surface
/**
* AnnexBDecoder
* MediaCodecVideoDecoder
*
* 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
* 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:
* - Public methods are synchronized to allow calls from multiple threads
* (packet producer vs. teardown). Internally, MediaCodec callbacks and
* buffer queues are used on the calling thread.
*/
class AnnexBDecoder(
class MediaCodecVideoDecoder(
width: Int,
height: Int,
outputSurface: Surface,
@@ -232,7 +234,7 @@ class AnnexBDecoder(
}
companion object {
private const val TAG = "AnnexBDecoder"
private const val TAG = "MediaCodecVideoDecoder"
private const val MIME_AVC = "video/avc"
private const val INPUT_TIMEOUT_US = 10_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
/**
* 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.
* - 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
* [controllerLock] for synchronous methods and by the facade's session mutex for
* 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
* [NativeCoreFacade] decide whether to restart the scrcpy session.
@@ -38,7 +38,7 @@ internal class VideoDecoderController(
private val mainHandler = Handler(Looper.getMainLooper())
private val controllerLock = Any()
private var decoder: AnnexBDecoder? = null
private var decoder: MediaCodecVideoDecoder? = null
@Volatile
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
* the persistent surface. Must be called while holding [controllerLock].
@@ -255,12 +255,7 @@ internal class VideoDecoderController(
}
val surface = renderer.getDecoderSurface()
val mime = when (session.codec) {
Codec.H264 -> "video/avc"
Codec.H265 -> "video/hevc"
Codec.AV1 -> "video/av01"
else -> "video/avc"
}
val mime = session.codec?.mime ?: "video/avc"
Log.i(
TAG,
"createOrReplaceDecoder(): codec=${session.codec?.string ?: "null"}, " +
@@ -269,7 +264,7 @@ internal class VideoDecoderController(
"persistent=true",
)
val newDecoder = try {
AnnexBDecoder(
MediaCodecVideoDecoder(
width = session.width,
height = session.height,
outputSurface = surface,
@@ -277,7 +272,7 @@ internal class VideoDecoderController(
onOutputSizeChanged = { width, height ->
val current = currentSessionInfo
if (current == null || (current.width == width && current.height == height)) {
return@AnnexBDecoder
return@MediaCodecVideoDecoder
}
Log.i(
TAG,
@@ -340,7 +335,7 @@ internal class VideoDecoderController(
// ---------- Bootstrap packet cache ----------
private fun replayBootstrapPackets(decoder: AnnexBDecoder) {
private fun replayBootstrapPackets(decoder: MediaCodecVideoDecoder) {
val snapshot = synchronized(bootstrapLock) { bootstrapPackets.toList() }
if (snapshot.isEmpty()) {
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
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) {
MIXED("mixed"),
@@ -633,6 +639,7 @@ data class ClientOptions(
vdSystemDecorations = vdSystemDecorations,
keepActive = keepActive,
flexDisplay = flexDisplay,
ignoreVideoEncoderConstraints = ignoreVideoEncoderConstraints,
list = list,
)
}

View File

@@ -116,9 +116,9 @@ class Scrcpy(
companion object {
private const val TAG = "Scrcpy"
const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v4.0"
const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v4.0"
const val DEFAULT_SERVER_VERSION = "4.0"
const val DEFAULT_SERVER_ASSET = "bin/scrcpy-server-v4.1"
const val DEFAULT_SERVER_ASSET_NAME = "scrcpy-server-v4.1"
const val DEFAULT_SERVER_VERSION = "4.1"
const val DEFAULT_REMOTE_PATH = "/data/local/tmp/scrcpy-server.jar"
// Regex patterns for parsing server output
@@ -389,6 +389,10 @@ class Scrcpy(
session.setClipboard(text, paste)
}
suspend fun scanFile(path: String) = withContext(Dispatchers.IO) {
session.scanFile(path)
}
suspend fun injectTouch(
action: Int,
pointerId: Long,
@@ -1317,6 +1321,10 @@ class Scrcpy(
withControlWriter("setClipboard") { setClipboard(text, paste) }
}
suspend fun scanFile(path: String) = mutex.withLock {
withControlWriter("scanFile") { scanFile(path) }
}
suspend fun injectTouch(
action: Int,
pointerId: Long,
@@ -1775,6 +1783,16 @@ class Scrcpy(
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) {
output.writeInt(x)
output.writeInt(y)
@@ -1836,6 +1854,7 @@ class Scrcpy(
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 TYPE_SCAN_FILE = 22
private const val SEQUENCE_INVALID = 0L

View File

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

View File

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

View File

@@ -283,6 +283,10 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
booleanPreferencesKey("flex_display"),
false,
)
val IGNORE_VIDEO_ENCODER_CONSTRAINTS = Pair(
booleanPreferencesKey("ignore_video_encoder_constraints"),
false,
)
fun defaultBundle() = Bundle(
crop = CROP.defaultValue,
@@ -351,6 +355,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
cameraTorch = CAMERA_TORCH.defaultValue,
keepActive = KEEP_ACTIVE.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 keepActive: Boolean,
val flexDisplay: Boolean,
val ignoreVideoEncoderConstraints: Boolean,
): Parcelable {
}
@@ -491,6 +497,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
bundleField(CAMERA_TORCH) { it.cameraTorch },
bundleField(KEEP_ACTIVE) { it.keepActive },
bundleField(FLEX_DISPLAY) { it.flexDisplay },
bundleField(IGNORE_VIDEO_ENCODER_CONSTRAINTS) { it.ignoreVideoEncoderConstraints },
)
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
@@ -562,6 +569,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
cameraTorch = preferences.read(CAMERA_TORCH),
keepActive = preferences.read(KEEP_ACTIVE),
flexDisplay = preferences.read(FLEX_DISPLAY),
ignoreVideoEncoderConstraints = preferences.read(IGNORE_VIDEO_ENCODER_CONSTRAINTS),
)
suspend fun loadBundle() = loadBundle(::bundleFromPreferences)
@@ -645,6 +653,7 @@ class ScrcpyOptions(context: Context): Settings(context, "ScrcpyOptions") {
cameraTorch = bundle.cameraTorch,
keepActive = bundle.keepActive,
flexDisplay = bundle.flexDisplay,
ignoreVideoEncoderConstraints = bundle.ignoreVideoEncoderConstraints,
)
}
@@ -716,6 +725,7 @@ internal fun encodeBundleToJson(bundle: ScrcpyOptions.Bundle): JSONObject =
.put("cameraTorch", bundle.cameraTorch)
.put("keepActive", bundle.keepActive)
.put("flexDisplay", bundle.flexDisplay)
.put("ignoreVideoEncoderConstraints", bundle.ignoreVideoEncoderConstraints)
internal fun decodeBundleFromJson(bundleJson: JSONObject?): ScrcpyOptions.Bundle {
val json = bundleJson ?: return ScrcpyOptions.defaultBundle()
@@ -984,6 +994,10 @@ internal fun decodeBundleFromJson(bundleJson: JSONObject?): ScrcpyOptions.Bundle
"flexDisplay",
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_desc">默认情况下scrcpy 会从设备中移除服务器二进制文件,并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)\n此选项将禁用此清理操作</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_native">本机</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_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_ignore_video_encoder_constraints">Ignore video encoder constraints</string>
<string name="scrcpyopt_start_app">Open app after scrcpy starts</string>
<string name="scrcpyopt_native">Native</string>
<string name="scrcpyopt_log_level">Log level</string>