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); } }