aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/AudioSink.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
commit852a8a00273c9128efeed0217a8e5d3fcd8bf780 (patch)
treec72f959731fa3c7c809cf1a0222363b6c9c9b03b /app/src/main/java/invalid/lena/scrcpy/AudioSink.java
parentf379b93d52bf0dbd3816ec3f64d165314771d2a6 (diff)
downloadscrcpy-android-852a8a00273c9128efeed0217a8e5d3fcd8bf780.tar.gz
app: harden mirroring lifecycle and state
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/AudioSink.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/AudioSink.java55
1 files changed, 42 insertions, 13 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/AudioSink.java b/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
index 7f09620..5507c5c 100644
--- a/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
+++ b/app/src/main/java/invalid/lena/scrcpy/AudioSink.java
@@ -14,6 +14,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.Deque;
+import java.util.concurrent.atomic.AtomicBoolean;
// Audio output: AudioTrack writing 48 kHz stereo 16-bit PCM. The
// upstream feed is either raw PCM (passthrough) or Opus packets
@@ -24,6 +25,7 @@ public final class AudioSink implements AudioFrames {
private static final int CHANNEL_OUT = AudioFormat.CHANNEL_OUT_STEREO;
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private static final int MAX_PENDING_OPUS = 16;
+ private static final int MAX_PACKET_BYTES = 256 * 1024;
// Defaults documented at <https://developer.android.com/reference/android/media/MediaCodec#CSD>.
private static final long DEFAULT_PRE_ROLL_NS = 80_000_000L;
@@ -32,16 +34,17 @@ public final class AudioSink implements AudioFrames {
private volatile MediaCodec opusCodec;
private HandlerThread opusThread;
private Handler opusHandler;
- private boolean opusConfigured;
+ private volatile boolean opusConfigured;
private int fourcc;
private final Object lock = new Object();
private final Runnable onFatalError;
+ private final AtomicBoolean fatalReported = new AtomicBoolean();
+ private final AtomicBoolean playbackReported = new AtomicBoolean();
private final Deque<Integer> freeOpusInputs = new ArrayDeque<>();
private final Deque<byte[]> pendingOpus = new ArrayDeque<>(MAX_PENDING_OPUS);
private volatile boolean released;
- public volatile long frames; // public read for the status overlay
private long droppedBytes;
private long lastDropLogMs;
@@ -69,7 +72,17 @@ public final class AudioSink implements AudioFrames {
track = new AudioTrack(
attrs, fmt, bufSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
+ if (track.getState() != AudioTrack.STATE_INITIALIZED) {
+ track.release();
+ track = null;
+ throw new IllegalStateException("audio sink: AudioTrack failed to initialize");
+ }
track.play();
+ if (track.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
+ track.release();
+ track = null;
+ throw new IllegalStateException("audio sink: AudioTrack failed to start");
+ }
Log.i("audio sink: AudioTrack started sr=%d ch=2 buf=%d", SAMPLE_RATE, bufSize);
if (fourcc == Wire.CODEC_OPUS) {
@@ -103,14 +116,15 @@ public final class AudioSink implements AudioFrames {
out.get(pcm);
writePcm(pcm, 0, pcm.length);
}
- } catch (IllegalStateException ignored) {
+ } catch (IllegalStateException e) {
+ reportFatal(e, "audio sink: opus output");
} finally {
try { mc.releaseOutputBuffer(idx, false); }
catch (IllegalStateException ignored) {}
}
}
@Override public void onError(MediaCodec mc, MediaCodec.CodecException e) {
- Log.e(e, "audio sink: opus codec error");
+ reportFatal(e, "audio sink: opus codec error");
}
@Override public void onOutputFormatChanged(MediaCodec mc, MediaFormat f) {
Log.i("audio sink: opus output format %s", f);
@@ -121,12 +135,18 @@ public final class AudioSink implements AudioFrames {
@Override
public void feed(byte[] data, int off, int len, boolean isConfig) {
- if (released || len <= 0) return;
+ if (data == null || off < 0 || len < 0 || off > data.length - len) {
+ throw new IndexOutOfBoundsException("invalid audio frame range");
+ }
+ if (len > MAX_PACKET_BYTES) {
+ reportFatal(null, "audio sink: packet too large (" + len + " bytes)");
+ return;
+ }
+ if (released || len == 0) return;
if (fourcc == Wire.CODEC_OPUS) {
feedOpus(data, off, len, isConfig);
} else {
// Raw PCM: passthrough.
- frames++;
writePcm(data, off, len);
}
}
@@ -137,7 +157,7 @@ public final class AudioSink implements AudioFrames {
// OpusHead is 19 bytes; pre_skip lives at bytes [10..11]. Reject
// anything shorter before indexing into it.
if (len < 19) {
- Log.e("audio sink: opus head too short (%d bytes)", len);
+ reportFatal(null, "audio sink: opus head too short (" + len + " bytes)");
return;
}
try {
@@ -156,7 +176,7 @@ public final class AudioSink implements AudioFrames {
opusConfigured = true;
Log.i("audio sink: opus configured, pre_skip=%d ns", preSkipNs);
} catch (Exception e) {
- Log.e(e, "audio sink: opus configure");
+ reportFatal(e, "audio sink: opus configure");
}
return;
}
@@ -181,16 +201,15 @@ public final class AudioSink implements AudioFrames {
try {
ByteBuffer in = codec.getInputBuffer(idx);
if (in == null || packet.length > in.capacity()) {
- Log.e("audio sink: opus packet exceeds codec input (%d bytes)", packet.length);
- if (onFatalError != null) onFatalError.run();
+ reportFatal(null,
+ "audio sink: opus packet exceeds codec input (" + packet.length + " bytes)");
return;
}
in.clear();
in.put(packet);
codec.queueInputBuffer(idx, 0, packet.length, 0, 0);
- frames++;
} catch (IllegalStateException e) {
- Log.w("audio sink: opus queueInputBuffer: %s", e);
+ reportFatal(e, "audio sink: opus queueInputBuffer");
}
}
@@ -206,9 +225,12 @@ public final class AudioSink implements AudioFrames {
if (released || t == null || len <= 0) return;
int written = t.write(data, off, len, AudioTrack.WRITE_NON_BLOCKING);
if (written < 0) {
- Log.w("audio sink: write rc=%d", written);
+ reportFatal(null, "audio sink: AudioTrack write rc=" + written);
return;
}
+ if (written > 0 && playbackReported.compareAndSet(false, true)) {
+ Log.i("audio sink: playback started");
+ }
if (written < len) {
droppedBytes += (len - written);
long now = System.currentTimeMillis();
@@ -219,6 +241,13 @@ public final class AudioSink implements AudioFrames {
}
}
+ private void reportFatal(Exception error, String message) {
+ if (!fatalReported.compareAndSet(false, true)) return;
+ if (error == null) Log.e("%s", message);
+ else Log.e(error, "%s", message);
+ if (onFatalError != null) onFatalError.run();
+ }
+
@Override
public void release() {
MediaCodec c;