diff options
Diffstat (limited to 'app/src/main/java/invalid/lena/scrcpy/VideoStream.java')
| -rw-r--r-- | app/src/main/java/invalid/lena/scrcpy/VideoStream.java | 156 |
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); + } +} |