1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
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.
//
// Wire format (big-endian, scrcpy 3.x/4.x):
// 1) Stream meta: uint32 fourcc
// special values 0 = disabled, 1 = error
// 2) Loop: 12-byte frame header (uint64 ptsAndFlags | uint32 size)
// followed by `size` bytes of payload.
//
// We honour FLAG_CONFIG on the per-frame header: for opus the first
// frame is the OpusHead (sent with FLAG_CONFIG set), subsequent are
// opus packets. For raw PCM the server never sets FLAG_CONFIG.
//
// Takes a plain InputStream; caller owns stream lifecycle.
public final class AudioStream {
private static final long FLAG_CONFIG = 1L << 62;
// 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
// not drive the allocation below.
private static final int MAX_FRAME_SIZE = 256 * 1024;
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() {
thread = new Thread(this::run, "audio-reader");
thread.start();
}
public void stop() {
stop = true;
Thread t = thread;
if (t == null) return;
t.interrupt();
if (t == Thread.currentThread()) return;
try { t.join(1_000); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public void run() {
try {
byte[] four = new byte[4];
Wire.readFully(source, four);
int fourcc = Wire.readBe32(four, 0);
if (fourcc == 0) {
Log.w("audio: server reports stream disabled (target cannot capture)");
return;
}
if (fourcc == 1) {
throw new IOException("audio: server reports configuration error");
}
if (fourcc != Wire.CODEC_RAW && fourcc != Wire.CODEC_OPUS) {
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;
byte[] hdr = new byte[12];
byte[] payload = new byte[16 * 1024];
long frames = 0;
while (!stop) {
Wire.readFully(source, hdr);
long ptsAndFlags = Wire.readBe64(hdr, 0);
int size = Wire.readBe32(hdr, 8);
boolean cfg = (ptsAndFlags & FLAG_CONFIG) != 0;
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 (++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();
}
} 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());
}
}
|