aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/VideoStream.java
blob: 7ecb3f1d9f44b19e45455bfdf1a4cedbdc6a250f (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package invalid.lena.scrcpy;

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

// Reads the scrcpy video socket and drives a VideoFrames sink.
//
// Wire format (big-endian, scrcpy 3.x/4.x):
//   1) Stream meta:    uint32 fourcc
//                      special values 0 = disabled, 1 = error
//   2) Then a stream of variable packets. Each packet starts with 4 bytes
//      that disambiguate the type:
//      a) Session meta (12 bytes total): uint32 flags(bit31=1) | uint32 w | uint32 h
//         flags bit0 = client-resize.  May recur on rotation.
//      b) Frame header (12 bytes total): uint64 ptsAndFlags | uint32 size
//         ptsAndFlags top bits:
//             bit 63 = SESSION  (never set in frame headers)
//             bit 62 = CONFIG   (CSD, e.g. SPS/PPS for h264)
//             bit 61 = KEYFRAME
//             rest   = pts microseconds
//         followed by `size` bytes of encoded payload.
//
// The constructor takes a plain InputStream so this class is
// android-free and unit-testable. The caller owns stream lifecycle
// (close it to unblock reads on stop()).
public final class VideoStream {

    public interface SizeListener { void onSize(int w, int h); }

    private static final long FLAG_SESSION  = 1L << 63;
    private static final long FLAG_CONFIG   = 1L << 62;
    private static final long FLAG_KEYFRAME = 1L << 61;
    private static final long PTS_MASK      = ~(FLAG_SESSION | FLAG_CONFIG | FLAG_KEYFRAME);
    private static final int  FLAG_SESSION_INT_BIT = 0x80000000;

    private static final int MAX_FRAME_SIZE = 8 * 1024 * 1024;

    private final InputStream  source;
    private final VideoFrames  sink;
    private final SizeListener sizeListener;
    private volatile VideoRecorder recorder; // optional tap
    private volatile Runnable  onEnd;        // fired once when run() exits
    private Thread             thread;
    private volatile boolean   stop;

    // Parser state held across packets in run().
    private int     fourcc;
    private boolean configured;
    private int     curW, curH;

    public VideoStream(InputStream source, VideoFrames sink, SizeListener sizeListener) {
        this.source = source;
        this.sink = sink;
        this.sizeListener = sizeListener;
    }

    public void start() {
        thread = new Thread(this::run, "video-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 setRecorder(VideoRecorder r) {
        this.recorder = r;
    }

    // Fired exactly once, on the reader thread, when run() exits - whether
    // by EOF, error, or stop(). The video socket is the authoritative
    // stream: when it ends mid-session the link is gone, so Session uses
    // this to surface a disconnect to the UI instead of freezing.
    public void setOnEnd(Runnable r) {
        this.onEnd = r;
    }

    // Visible for tests: parse the same way the thread does, on the caller's thread.
    public void run() {
        try {
            byte[] four = new byte[4];
            Wire.readFully(source, four);
            fourcc = Wire.readBe32(four, 0);
            if (fourcc == 0)  throw new IOException("video: server reports stream disabled");
            if (fourcc == 1)  throw new IOException("video: server reports configuration error");
            Log.i("video meta codec=%s", Wire.fourccName(fourcc));

            byte[] tail8 = new byte[8];
            long frames = 0;

            while (!stop) {
                Wire.readFully(source, four);
                int hi = Wire.readBe32(four, 0);

                if ((hi & FLAG_SESSION_INT_BIT) != 0) {
                    parseSessionMeta(hi, tail8);
                } else {
                    parseFrame(hi, tail8);
                    if (++frames == 1) Log.i("video frame n=1");
                }
            }
        } catch (IOException e) {
            if (!stop) Log.e(e, "video reader");
        } catch (Exception e) {
            Log.e(e, "video reader unexpected");
        } finally {
            Log.i("video reader: end");
            Runnable r = onEnd;
            if (r != null) r.run();
        }
    }

    private void parseSessionMeta(int hi, byte[] tail8) throws IOException {
        Wire.readFully(source, tail8);
        int newW = Wire.readBe32(tail8, 0);
        int newH = Wire.readBe32(tail8, 4);
        boolean clientResize = (hi & 1) != 0;
        Log.i("video session meta %dx%d client_resize=%s", newW, newH, clientResize);

        if (!configured) {
            curW = newW; curH = newH;
            sink.configure(fourcc, curW, curH);
            configured = true;
        } else if (newW != curW || newH != curH) {
            Log.i("video resize %dx%d -> %dx%d", curW, curH, newW, newH);
            curW = newW; curH = newH;
            sink.reconfigure(fourcc, curW, curH);
        }
        if (sizeListener != null) sizeListener.onSize(curW, curH);
        VideoRecorder r = recorder;
        if (r != null) r.onMeta(fourcc, curW, curH);
    }

    private void parseFrame(int hi, byte[] tail8) throws IOException {
        Wire.readFully(source, tail8);
        long pts = ((long) hi << 32) | (Wire.readBe32(tail8, 0) & 0xffffffffL);
        int  size = Wire.readBe32(tail8, 4);
        boolean cfg = (pts & FLAG_CONFIG)   != 0;
        boolean key = (pts & FLAG_KEYFRAME) != 0;
        long ptsUs = pts & PTS_MASK;

        if (size <= 0 || size > MAX_FRAME_SIZE) {
            throw new IOException("video frame size out of range: " + size);
        }
        // Validate ordering BEFORE allocating the payload buffer - a
        // malformed early frame could otherwise OOM on small devices.
        if (!configured) throw new IOException("video frame before session meta");

        byte[] payload = new byte[size];
        Wire.readFully(source, payload);
        sink.feed(payload, ptsUs, cfg);
        VideoRecorder r = recorder;
        if (r != null) r.onFrame(payload, ptsUs, cfg, key);
    }
}