Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change Log

## 0.5.0

- 依赖: scrcpy 升级至 v4.1
- 新增: vp8/vp9 解码

## 0.4.5

- 重构: 改了好多不知道怎么写,总之修复了一些已知问题
Expand Down
20 changes: 10 additions & 10 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 ->
Expand All @@ -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" +
Expand All @@ -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.")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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].
Expand All @@ -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"}, " +
Expand All @@ -269,15 +264,15 @@ internal class VideoDecoderController(
"persistent=true",
)
val newDecoder = try {
AnnexBDecoder(
MediaCodecVideoDecoder(
width = session.width,
height = session.height,
outputSurface = surface,
mimeType = mime,
onOutputSizeChanged = { width, height ->
val current = currentSessionInfo
if (current == null || (current.width == width && current.height == height)) {
return@AnnexBDecoder
return@MediaCodecVideoDecoder
}
Log.i(
TAG,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
},
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -633,6 +639,7 @@ data class ClientOptions(
vdSystemDecorations = vdSystemDecorations,
keepActive = keepActive,
flexDisplay = flexDisplay,
ignoreVideoEncoderConstraints = ignoreVideoEncoderConstraints,
list = list,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ data class ServerParams(
val vdSystemDecorations: Boolean,
val keepActive: Boolean,
val flexDisplay: Boolean,
val ignoreVideoEncoderConstraints: Boolean,

val list: ListOptions,
) {
Expand Down Expand Up @@ -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}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),

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
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", "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 }

Expand Down
Loading