aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/AudioStream.java
blob: 7fe33f089995c97dc61ee4c8ce8d673fdb87992b (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
package invalid.lena.scrcpy;

import java.io.IOException;
import java.io.InputStream;

// 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 = 1024 * 1024;

    private final InputStream  source;
    private final AudioFrames  sink;
    private Thread             thread;
    private volatile boolean   stop;

    public AudioStream(InputStream source, AudioFrames sink) {
        this.source = source;
        this.sink = sink;
    }

    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) {
                Log.e("audio: server reports configuration error");
                return;
            }
            if (fourcc != Wire.CODEC_RAW && fourcc != Wire.CODEC_OPUS) {
                Log.w("audio: unexpected codec %s - keeping silent",
                        Wire.fourccName(fourcc));
                return;
            }
            Log.i("audio meta codec=%s", Wire.fourccName(fourcc));
            sink.start(fourcc);

            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);
                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");
        } catch (Exception e) {
            Log.e(e, "audio reader unexpected");
        } finally {
            Log.i("audio reader: end");
        }
    }
}