aboutsummaryrefslogtreecommitdiff
path: root/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java
diff options
context:
space:
mode:
authorLena <lena@omega>2026-01-01 00:00:00 +0000
committerLena <lena@omega>2026-06-24 22:14:50 +0300
commiteb0c8951196c637e44daf3c0617b131b997d5d2c (patch)
tree2f83b6a41cee745467e53fc401942b82cea286ea /app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java
downloadscrcpy-android-eb0c8951196c637e44daf3c0617b131b997d5d2c.tar.gz
scrcpy-android: mirror an Android device over wireless ADB0.1
Native Java app for Android 12+ that mirrors another Android device over wireless ADB, forwarding video, audio, touch input, and clipboard. Bundles a pinned scrcpy-server.jar and the vendored libadb-android stack. Supports h264/h265/av1 video and raw/opus audio with in-app codec selection. No NDK, no Kotlin. Includes JVM unit tests and a Docker-based emulator e2e rig.
Diffstat (limited to 'app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java')
-rw-r--r--app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java245
1 files changed, 245 insertions, 0 deletions
diff --git a/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java b/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java
new file mode 100644
index 0000000..9d132f6
--- /dev/null
+++ b/app/src/test/java/invalid/lena/scrcpy/VideoStreamTest.java
@@ -0,0 +1,245 @@
+package invalid.lena.scrcpy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class VideoStreamTest {
+
+ // ---- recording sink ----
+
+ private static final class RecordingFrames implements VideoFrames {
+ static final class Cfg { final int fourcc, w, h; Cfg(int f,int w,int h){this.fourcc=f;this.w=w;this.h=h;} }
+ static final class Feed { final byte[] data; final long ptsUs; final boolean isCfg;
+ Feed(byte[] d,long p,boolean c){data=d;ptsUs=p;isCfg=c;} }
+
+ final List<Cfg> configs = new ArrayList<>();
+ final List<Cfg> reconfigs = new ArrayList<>();
+ final List<Feed> feeds = new ArrayList<>();
+ int releases = 0;
+
+ @Override public void configure(int f, int w, int h) { configs.add(new Cfg(f, w, h)); }
+ @Override public void reconfigure(int f, int w, int h) { reconfigs.add(new Cfg(f, w, h)); }
+ @Override public void feed(byte[] d, long pts, boolean c){ feeds.add(new Feed(d, pts, c)); }
+ @Override public void release() { releases++; }
+ }
+
+ // ---- byte builders ----
+
+ private static byte[] fourcc(int v) throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ new DataOutputStream(bos).writeInt(v);
+ return bos.toByteArray();
+ }
+
+ private static byte[] sessionMeta(int w, int h, boolean clientResize) throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bos);
+ out.writeInt(0x80000000 | (clientResize ? 1 : 0));
+ out.writeInt(w);
+ out.writeInt(h);
+ return bos.toByteArray();
+ }
+
+ private static byte[] frame(long ptsAndFlags, byte[] payload) throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bos);
+ out.writeLong(ptsAndFlags);
+ out.writeInt(payload.length);
+ out.write(payload);
+ return bos.toByteArray();
+ }
+
+ private static byte[] cat(byte[]... parts) throws IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ for (byte[] p : parts) bos.write(p);
+ return bos.toByteArray();
+ }
+
+ private static void runUntilEof(VideoStream s) {
+ s.run(); // returns when the InputStream hits EOF (IOException)
+ }
+
+ // ---- tests ----
+
+ @Test
+ public void happyPath_configureCsdKeyframeDelta() throws Exception {
+ byte[] csd = new byte[]{0x67, 0x42, (byte)0xe0, 0x1e}; // bogus SPS-ish
+ byte[] keyframe = new byte[]{1, 2, 3, 4, 5};
+ byte[] delta = new byte[]{6, 7, 8};
+
+ long PTS_CFG = 1L << 62; // CONFIG
+ long PTS_KEY = (1L << 61) | 1_000_000L; // KEYFRAME, pts=1s
+ long PTS_DELT = 2_000_000L; // pts=2s
+
+ byte[] bytes = cat(
+ fourcc(Wire.CODEC_H264),
+ sessionMeta(1080, 2400, false),
+ frame(PTS_CFG, csd),
+ frame(PTS_KEY, keyframe),
+ frame(PTS_DELT, delta));
+
+ RecordingFrames sink = new RecordingFrames();
+ VideoStream s = new VideoStream(new ByteArrayInputStream(bytes), sink,
+ (w, h) -> {});
+ runUntilEof(s);
+
+ assertEquals(1, sink.configs.size());
+ assertEquals(Wire.CODEC_H264, sink.configs.get(0).fourcc);
+ assertEquals(1080, sink.configs.get(0).w);
+ assertEquals(2400, sink.configs.get(0).h);
+
+ assertEquals(3, sink.feeds.size());
+ assertTrue("first is config", sink.feeds.get(0).isCfg);
+ assertEquals(0L, sink.feeds.get(0).ptsUs); // CONFIG strips pts to 0
+ assertEquals(4, sink.feeds.get(0).data.length);
+
+ // keyframe: keyframe flag is stripped from ptsUs
+ assertEquals(1_000_000L, sink.feeds.get(1).ptsUs);
+ assertEquals(5, sink.feeds.get(1).data.length);
+
+ assertEquals(2_000_000L, sink.feeds.get(2).ptsUs);
+ }
+
+ @Test
+ public void sessionMetaResizeTriggersReconfigure() throws Exception {
+ byte[] bytes = cat(
+ fourcc(Wire.CODEC_H264),
+ sessionMeta(1080, 2400, false),
+ frame(1L << 62, new byte[]{1}), // config
+ sessionMeta(720, 1600, true), // resize
+ frame(1L << 61, new byte[]{2})); // keyframe at new size
+
+ RecordingFrames sink = new RecordingFrames();
+ new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
+
+ assertEquals(1, sink.configs.size());
+ assertEquals(1, sink.reconfigs.size());
+ assertEquals(720, sink.reconfigs.get(0).w);
+ assertEquals(1600, sink.reconfigs.get(0).h);
+ }
+
+ @Test
+ public void sizeListenerFiresOnEachSessionMeta() throws Exception {
+ byte[] bytes = cat(
+ fourcc(Wire.CODEC_H264),
+ sessionMeta(1080, 2400, false),
+ frame(1L << 62, new byte[]{1}),
+ sessionMeta(720, 1600, false),
+ frame(1L << 61, new byte[]{2}));
+
+ List<int[]> sizes = new ArrayList<>();
+ RecordingFrames sink = new RecordingFrames();
+ new VideoStream(new ByteArrayInputStream(bytes), sink,
+ (w, h) -> sizes.add(new int[]{w, h})).run();
+
+ assertEquals(2, sizes.size());
+ assertEquals(1080, sizes.get(0)[0]);
+ assertEquals(720, sizes.get(1)[0]);
+ }
+
+ @Test
+ public void disabledCodecExits() throws Exception {
+ byte[] bytes = fourcc(0);
+ RecordingFrames sink = new RecordingFrames();
+ new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
+ assertEquals(0, sink.configs.size());
+ assertEquals(0, sink.feeds.size());
+ }
+
+ @Test
+ public void frameBeforeSessionMetaIsRejected() throws Exception {
+ // A frame header arriving without a preceding session-meta should
+ // raise - the stream is unconfigured.
+ byte[] bytes = cat(
+ fourcc(Wire.CODEC_H264),
+ frame(1L << 62, new byte[]{1, 2, 3})); // config-flag set, but no meta first
+ RecordingFrames sink = new RecordingFrames();
+ new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
+ // The loop should have aborted without feeding anything.
+ assertEquals(0, sink.feeds.size());
+ assertEquals(0, sink.configs.size());
+ }
+
+ @Test
+ public void recorderTapSeesMetaAndFramesWithFlags() throws Exception {
+ byte[] csd = new byte[]{0x67, 0x42, (byte)0xe0, 0x1e};
+ byte[] keyframe = new byte[]{10, 11, 12};
+ byte[] delta = new byte[]{20, 21};
+ long PTS_CFG = 1L << 62;
+ long PTS_KEY = (1L << 61) | 1_000_000L;
+ long PTS_DELT = 2_000_000L;
+
+ byte[] bytes = cat(
+ fourcc(Wire.CODEC_H264),
+ sessionMeta(800, 600, false),
+ frame(PTS_CFG, csd),
+ frame(PTS_KEY, keyframe),
+ frame(PTS_DELT, delta));
+
+ RecordingFrames sink = new RecordingFrames();
+ RecordingRecorder rec = new RecordingRecorder();
+ VideoStream s = new VideoStream(new ByteArrayInputStream(bytes), sink, null);
+ s.setRecorder(rec);
+ s.run();
+
+ assertEquals(1, rec.metas.size());
+ assertEquals(800, rec.metas.get(0)[1]);
+ assertEquals(600, rec.metas.get(0)[2]);
+
+ assertEquals(3, rec.frames.size());
+ assertTrue("cfg frame", rec.frames.get(0).isConfig);
+ assertTrue("key frame", rec.frames.get(1).isKeyframe);
+ assertTrue("delta is not config or key",
+ !rec.frames.get(2).isConfig && !rec.frames.get(2).isKeyframe);
+ assertEquals(csd.length, rec.frames.get(0).data.length);
+ assertEquals(keyframe.length, rec.frames.get(1).data.length);
+ assertEquals(delta.length, rec.frames.get(2).data.length);
+ }
+
+ private static final class RecordingRecorder implements VideoRecorder {
+ static final class FrameRec {
+ final byte[] data; final long ptsUs;
+ final boolean isConfig, isKeyframe;
+ FrameRec(byte[] d, long p, boolean c, boolean k) {
+ data = d; ptsUs = p; isConfig = c; isKeyframe = k;
+ }
+ }
+ final List<int[]> metas = new ArrayList<>();
+ final List<FrameRec> frames = new ArrayList<>();
+ int closes = 0;
+ @Override public void onMeta(int f, int w, int h) { metas.add(new int[]{f, w, h}); }
+ @Override public void onFrame(byte[] d, long p, boolean c, boolean k) {
+ frames.add(new FrameRec(d, p, c, k));
+ }
+ @Override public void close() { closes++; }
+ }
+
+ @Test
+ public void payloadIntegrity() throws Exception {
+ byte[] payload = new byte[1024];
+ for (int i = 0; i < payload.length; i++) payload[i] = (byte) (i & 0xff);
+ byte[] bytes = cat(
+ fourcc(Wire.CODEC_H264),
+ sessionMeta(640, 480, false),
+ frame(1L << 62, payload));
+
+ RecordingFrames sink = new RecordingFrames();
+ new VideoStream(new ByteArrayInputStream(bytes), sink, null).run();
+ assertNotNull(sink.feeds.isEmpty() ? null : sink.feeds.get(0));
+ byte[] got = sink.feeds.get(0).data;
+ assertEquals(payload.length, got.length);
+ for (int i = 0; i < payload.length; i++) {
+ assertEquals("byte " + i, payload[i], got[i]);
+ }
+ }
+}