aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/VideoStream.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/VideoStream.java')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/VideoStream.java156
1 files changed, 156 insertions, 0 deletions
diff --git a/app/src/main/java/invalid/lena/scrcpy/VideoStream.java b/app/src/main/java/invalid/lena/scrcpy/VideoStream.java
new file mode 100644
index 0000000..4eb62d3
--- /dev/null
+++ b/app/src/main/java/invalid/lena/scrcpy/VideoStream.java
@@ -0,0 +1,156 @@
+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;
+ if (thread != null) thread.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);
+ }
+}