diff options
| author | Lena <lena@omega> | 2026-07-01 00:00:00 +0000 |
|---|---|---|
| committer | Lena <lena@omega> | 2026-07-01 00:00:00 +0000 |
| commit | ff7acf898b48359275a5b09b82ed926a945233f8 (patch) | |
| tree | 5e45b04be22cb1531f075ec35eb6f91518e09165 /app/src/main/java/invalid/lena/scrcpy/Server.java | |
| parent | bddc306de0cebde44f99b3a9c4ddba130946bef3 (diff) | |
| download | scrcpy-android-ff7acf898b48359275a5b09b82ed926a945233f8.tar.gz | |
app: fix session lifecycle, teardown, and opus playback
Opus playback never worked: feed() called dequeueInputBuffer() on a
codec in async-callback mode, which always throws, so every packet
was silently dropped. Feed input through the async callback with a
bounded pending queue instead.
Make bring-up transactional: roll back partially opened streams and
the server shell on failure, join reader threads on stop, and close
the owning ADB streams before releasing sinks. Replace the
openAbstract watchdog thread with a real timeout in the vendored
AdbConnection.open(), which also removes the half-open stream from
the lookup table on failure.
Harden the activity: release owned Surfaces, gate callbacks on a
destroyed flag and a session generation, serialize reconnect, and
handle target replacement via singleTask + onNewIntent. Propagate
device-list write failures instead of swallowing them, bound the
clipboard payload and the video pending queue against hostile
peers, and discard incomplete recordings instead of keeping corrupt
files.
Use the mediaPlayback foreground-service type; Android 15 stops
dataSync services after six hours. Drop the unused
ACCESS_NETWORK_STATE permission.
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/Server.java')
| -rw-r--r-- | app/src/main/java/invalid/lena/scrcpy/Server.java | 142 |
1 files changed, 54 insertions, 88 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/Server.java b/app/src/main/java/invalid/lena/scrcpy/Server.java index 6ae752f..284dd6c 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Server.java +++ b/app/src/main/java/invalid/lena/scrcpy/Server.java @@ -7,12 +7,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.net.ConnectException; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import io.github.muntashirakon.adb.AdbStream; @@ -38,21 +37,8 @@ public final class Server { private static final String ASSET_JAR = "scrcpy-server.jar"; private static final String ASSET_VERSION = "scrcpy-server.version"; private static final int FILE_MODE = 0100644; // regular file, 0644 - // Server forks a CleanUp helper before opening its abstract sockets; - // on slow emulators that takes several seconds, so the budget needs - // to be generous. The deadline is per stream: in practice only the - // first open waits (the server is still starting) and the other two - // dial instantly, so a healthy bring-up stays well below the e2e - // test deadline (test-rig/e2e.sh's E2E_DEADLINE, default 60 s). - private static final long OPEN_DEADLINE_MS = 20_000; - private static final int OPEN_BACKOFF_MS = 100; - // Per-attempt timeout for adb.openAbstract. libadb-android's - // AdbConnection.open() blocks on a naked stream.wait() with no - // loop or timeout - a missed notification (response arrives before - // we park) wedges the call forever. Wrap each attempt with a - // timeout + interrupt so the loop can move on. - private static final long OPEN_ATTEMPT_TIMEOUT_MS = 800; - + private static final long LISTENER_DEADLINE_MS = 20_000; + private static final long LISTENER_RETRY_MS = 100; public static final class Streams { public final AdbStream videoAds, audioAds, controlAds; public final InputStream videoIn, audioIn, controlIn; @@ -84,6 +70,7 @@ public final class Server { } public Streams bringUp() throws Exception { + serverEof = false; String version = readVersion(); long pushed = push(); Log.i("push %s bytes=%d", REMOTE_PATH, pushed); @@ -93,24 +80,40 @@ public final class Server { Log.i("spawn server ver=%s scid=%s", version, scid); Log.i("cmdline: %s", cmd); - shell = adb.openShell(cmd); - shellPump = new Thread(() -> pump(shell.openInputStream()), "server-stdout"); - shellPump.setDaemon(true); - shellPump.start(); + AdbStream va = null, aa = null, ca = null; + boolean committed = false; + try { + shell = adb.openShell(cmd); + AdbStream shellRef = shell; + shellPump = new Thread(() -> pump(shellRef.openInputStream()), "server-stdout"); + shellPump.setDaemon(true); + shellPump.start(); - AdbStream va = openAbstract(scid); - AdbStream aa = openAbstract(scid); - AdbStream ca = openAbstract(scid); + // These accepts are ordered. If one times out, the whole ADB + // connection is discarded by Session; retrying an individual + // open could shift video/audio/control onto the wrong sockets. + va = openAbstract(scid); + aa = openAbstract(scid); + ca = openAbstract(scid); - InputStream vi = va.openInputStream(); - InputStream ai = aa.openInputStream(); - InputStream ci = ca.openInputStream(); - OutputStream co = ca.openOutputStream(); + InputStream vi = va.openInputStream(); + InputStream ai = aa.openInputStream(); + InputStream ci = ca.openInputStream(); + OutputStream co = ca.openOutputStream(); - String name = readDeviceMeta(vi); - Log.i("device name=%s", name); - streams = new Streams(va, aa, ca, vi, ai, ci, co, name, scid, version); - return streams; + String name = readDeviceMeta(vi); + Log.i("device name=%s", name); + streams = new Streams(va, aa, ca, vi, ai, ci, co, name, scid, version); + committed = true; + return streams; + } finally { + if (!committed) { + closeQuietly(va); + closeQuietly(aa); + closeQuietly(ca); + closeShell(); + } + } } // Idempotent. Closes the three media/control streams first (lets @@ -127,6 +130,10 @@ public final class Server { closeQuietly(streams.controlAds); streams = null; } + closeShell(); + } + + private void closeShell() { Thread t = shellPump; AdbStream s = shell; shell = null; @@ -198,67 +205,26 @@ public final class Server { private AdbStream openAbstract(String scid) throws Exception { String name = "scrcpy_" + scid; - long deadline = System.currentTimeMillis() + OPEN_DEADLINE_MS; - Throwable last = null; - for (int attempt = 1; System.currentTimeMillis() < deadline; attempt++) { - // If the shell stream closed (server died early - usually with - // an error printed to stderr that our pump captured), bail - // immediately. Otherwise we'd spin out the deadline on a - // dead target. + long deadline = System.currentTimeMillis() + LISTENER_DEADLINE_MS; + ConnectException last = null; + while (System.currentTimeMillis() < deadline) { if (serverEof) { - throw new IOException("server exited before opening " - + name + " (see 'server:' logs)", - last instanceof Exception ? (Exception) last : null); + throw new IOException("server exited before opening " + name, last); } try { - AdbStream s = openAbstractOnce(name, OPEN_ATTEMPT_TIMEOUT_MS); - Log.i("openAbstract %s ok (attempt %d)", name, attempt); - return s; - } catch (Throwable t) { - last = t; - Thread.sleep(OPEN_BACKOFF_MS); - } - } - throw new IOException("openAbstract " + name + " failed after " - + (OPEN_DEADLINE_MS / 1000) + " s", - last instanceof Exception ? (Exception) last : null); - } - - // Wraps a single adb.openAbstract call with a hard timeout. The - // upstream call can wedge forever on a missed notify in its naked - // stream.wait(); we run it on a daemon thread and join with timeout, - // interrupt on overrun, and let the caller retry. If the orphaned - // call succeeds after we gave up, the stream must be closed: - // keeping it would silently consume one of the scrcpy server's - // three accepts and shift every later dial off by one. - private AdbStream openAbstractOnce(String name, long timeoutMs) throws Exception { - AtomicReference<AdbStream> result = new AtomicReference<>(); - AtomicReference<Throwable> err = new AtomicReference<>(); - AtomicBoolean abandoned = new AtomicBoolean(); - Thread t = new Thread(() -> { - try { - AdbStream s = adb.openAbstract(name); - result.set(s); - if (abandoned.get()) closeQuietly(s); - } catch (Throwable ex) { - err.set(ex); + AdbStream stream = adb.openAbstract(name); + Log.i("openAbstract %s ok", name); + return stream; + } catch (ConnectException e) { + // A rejected OPEN consumed no server accept. Retry while the + // server creates its listener; timeout failures remain fatal + // because their acceptance state is ambiguous. + last = e; + Thread.sleep(LISTENER_RETRY_MS); } - }, "openAbstract-" + name); - t.setDaemon(true); - t.start(); - t.join(timeoutMs); - if (t.isAlive()) { - abandoned.set(true); - t.interrupt(); - // One side of the publish/abandon race closes the stream; - // closeQuietly tolerates both doing it. - closeQuietly(result.get()); - throw new IOException("openAbstract " + name + " timed out"); } - Throwable ex = err.get(); - if (ex instanceof Exception) throw (Exception) ex; - if (ex != null) throw new RuntimeException(ex); - return result.get(); + throw new IOException("server did not open " + name + " within " + + LISTENER_DEADLINE_MS + " ms", last); } private static String readDeviceMeta(InputStream in) throws IOException { |