aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/AudioStream.java
diff options
context:
space:
mode:
authorLena <lena@omega>2026-08-01 00:00:00 +0000
committerLena <lena@omega>2026-08-01 00:00:00 +0000
commit75b1a5569bf9048acefa49f2921b2653a828d545 (patch)
tree1b0342936b71ac5e5f59ad3b4e1e37cea2691ff5 /app/src/main/java/invalid/lena/scrcpy/AudioStream.java
parent4ea99f1b77ba547014fb7b7becaa8de2fd671a0b (diff)
downloadscrcpy-android-75b1a5569bf9048acefa49f2921b2653a828d545.tar.gz
audio: harden Opus playback
Preserve decoder timing and keep blocking speaker writes off codec callbacks. Playback failures no longer take down video or control.
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/AudioStream.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/AudioStream.java59
1 files changed, 21 insertions, 38 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/AudioStream.java b/app/src/main/java/invalid/lena/scrcpy/AudioStream.java
index 13b39f5..45ea69c 100644
--- a/app/src/main/java/invalid/lena/scrcpy/AudioStream.java
+++ b/app/src/main/java/invalid/lena/scrcpy/AudioStream.java
@@ -2,7 +2,6 @@ package invalid.lena.scrcpy;
import java.io.IOException;
import java.io.InputStream;
-import java.util.concurrent.TimeUnit;
// Reads the scrcpy audio socket and drives an AudioFrames sink.
//
@@ -19,7 +18,10 @@ import java.util.concurrent.TimeUnit;
// Takes a plain InputStream; caller owns stream lifecycle.
public final class AudioStream {
- private static final long FLAG_CONFIG = 1L << 62;
+ private static final long FLAG_SESSION = 1L << 63;
+ private static final long FLAG_CONFIG = 1L << 62;
+ private static final long FLAG_KEYFRAME = 1L << 61;
+ private static final long PTS_MASK = ~(FLAG_SESSION | FLAG_CONFIG | FLAG_KEYFRAME);
// Generous upper bound for one audio packet (a raw PCM block or an
// opus packet is a few KB). A corrupt or hostile length field must
@@ -28,30 +30,12 @@ public final class AudioStream {
private final InputStream source;
private final AudioFrames sink;
- private final Runnable onFatalError;
private Thread thread;
private volatile boolean stop;
- // Liveness signal for Session's stall watchdog. The audio socket is
- // the only one that is reliably continuous: the server captures PCM
- // at 48 kHz whether or not anything on the target's screen moves, so
- // silence here means the wire is gone. Video is useless for this - a
- // static screen queues nothing to the encoder and legitimately
- // produces no frames for minutes.
- private volatile boolean started;
- private volatile long lastPacketAtMs;
-
- public boolean isStarted() { return started; }
- public long lastPacketAtMs() { return lastPacketAtMs; }
-
public AudioStream(InputStream source, AudioFrames sink) {
- this(source, sink, null);
- }
-
- public AudioStream(InputStream source, AudioFrames sink, Runnable onFatalError) {
this.source = source;
this.sink = sink;
- this.onFatalError = onFatalError;
}
public void start() {
@@ -85,9 +69,13 @@ public final class AudioStream {
throw new IOException("audio: unexpected codec " + Wire.fourccName(fourcc));
}
Log.i("audio meta codec=%s", Wire.fourccName(fourcc));
- sink.start(fourcc);
- lastPacketAtMs = monotonicMs();
- started = true;
+ boolean playback = true;
+ try {
+ sink.start(fourcc);
+ } catch (RuntimeException e) {
+ Log.e(e, "audio output disabled");
+ playback = false;
+ }
byte[] hdr = new byte[12];
byte[] payload = new byte[16 * 1024];
@@ -97,33 +85,28 @@ public final class AudioStream {
long ptsAndFlags = Wire.readBe64(hdr, 0);
int size = Wire.readBe32(hdr, 8);
boolean cfg = (ptsAndFlags & FLAG_CONFIG) != 0;
+ long ptsUs = ptsAndFlags & PTS_MASK;
if (size <= 0 || size > MAX_FRAME_SIZE) {
throw new IOException("audio frame size out of range: " + size);
}
if (size > payload.length) payload = new byte[size];
Wire.readFully(source, payload, 0, size);
- lastPacketAtMs = monotonicMs();
- sink.feed(payload, 0, size, cfg);
+ if (playback) {
+ try {
+ sink.feed(payload, 0, size, ptsUs, cfg);
+ } catch (RuntimeException e) {
+ Log.e(e, "audio output disabled");
+ playback = false;
+ }
+ }
if (++frames == 1) Log.i("audio frame n=1 size=%d cfg=%s", size, cfg);
}
} catch (IOException e) {
- if (!stop) {
- Log.e(e, "audio reader");
- reportFatal();
- }
+ if (!stop) Log.e(e, "audio reader");
} catch (Exception e) {
Log.e(e, "audio reader unexpected");
- if (!stop) reportFatal();
} finally {
Log.i("audio reader: end");
}
}
-
- private void reportFatal() {
- if (onFatalError != null) onFatalError.run();
- }
-
- private static long monotonicMs() {
- return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
- }
}