diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b7c4f..12f4eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 0.5.0 + +- 依赖: scrcpy 升级至 v4.1 +- 新增: vp8/vp9 解码 + ## 0.4.5 - 重构: 改了好多不知道怎么写,总之修复了一些已知问题 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 880bbe2..f346153 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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.") } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt index 594a469..e901ef7 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/NativeCoreFacade.kt @@ -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" } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/MediaCodecVideoDecoder.kt similarity index 96% rename from app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt rename to app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/MediaCodecVideoDecoder.kt index 8f0a3ce..edf85c5 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AnnexBDecoder.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/MediaCodecVideoDecoder.kt @@ -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 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/VideoDecoderController.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/VideoDecoderController.kt index ea026ff..460f704 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/VideoDecoderController.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/VideoDecoderController.kt @@ -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 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt index 4a24f04..c2eb0f3 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt @@ -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, + ) + }, + ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt index e26627c..52a920e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt @@ -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, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt index 9d9ae77..a965544 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Scrcpy.kt @@ -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 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt index 93154d1..46f94e4 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ServerParams.kt @@ -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}") } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt index 72d6190..45d54a2 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/Shared.kt @@ -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 } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt index 8c8d8d1..316f0e5 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/ScrcpyOptions.kt @@ -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 = 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, + ), ) } diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 3287fc4..2221031 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -536,6 +536,7 @@ 禁用结束后清理 默认情况下,scrcpy 会从设备中移除服务器二进制文件,并在退出时恢复设备状态(显示触摸、保持唤醒和电源模式)\n此选项将禁用此清理操作 自动调整尺寸以匹配界面 + 忽略视频编码器约束 scrcpy 启动后打开应用 本机 日志等级 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be74265..c55d524 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -537,6 +537,7 @@ Disable cleanup on exit 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 Automatically resize to match the screen + Ignore video encoder constraints Open app after scrcpy starts Native Log level