From 852a8a00273c9128efeed0217a8e5d3fcd8bf780 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: app: harden mirroring lifecycle and state --- .../main/java/invalid/lena/scrcpy/AudioSink.java | 55 +++++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) (limited to 'app/src/main/java/invalid/lena/scrcpy/AudioSink.java') 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 . 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 freeOpusInputs = new ArrayDeque<>(); private final Deque 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; -- cgit v1.2.3