aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/AudioStream.java
blob: 13b39f5e08b16b2d6d6299eaada69a6adde98e19 (plain) (blame)
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());
    }
}