diff --git a/webrtc-javafx/src/test/java/dev/onvoid/webrtc/client/javafx/VideoViewTest.java b/webrtc-javafx/src/test/java/dev/onvoid/webrtc/client/javafx/VideoViewTest.java index 1859b6ef..3e1a17f0 100644 --- a/webrtc-javafx/src/test/java/dev/onvoid/webrtc/client/javafx/VideoViewTest.java +++ b/webrtc-javafx/src/test/java/dev/onvoid/webrtc/client/javafx/VideoViewTest.java @@ -143,6 +143,22 @@ void mirrors() throws Exception { assertTrue(paint(view).getPixelReader().getColor(8, 24).getBrightness() > 0.75); } + @Test + void anyRotationMultiple() throws Exception { + // A rotation outside 0..270 is shown as its equal inside it. + assertSamePixels(rotated(270), rotated(-90)); + assertSamePixels(rotated(90), rotated(450)); + + // Sideways, a 4:3 picture in a 4:3 view leaves bars on the sides. + VideoView view = sized(64, 48); + view.accept(frame(64, 48, 128, 128, -90)); + presented(view); + + Image painted = paint(view); + assertEquals(Color.BLACK, painted.getPixelReader().getColor(2, 24)); + assertGray(painted.getPixelReader().getColor(32, 24)); + } + @Test void showsRemoteVideo() throws Exception { SignalingPair link = Signaling.loopback(); @@ -219,11 +235,24 @@ private static T onFx(Callable action) throws Exception { return result.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); } + /** What a 64x48 view paints of a 64x48 two-tone frame with the given rotation. */ + private Image rotated(int rotation) throws Exception { + VideoView view = sized(64, 48); + view.accept(frame(64, 48, 16, 235, rotation)); + presented(view); + + return paint(view); + } + + private static VideoFrame frame(int width, int height, int left, int right) { + return frame(width, height, left, right, 0); + } + /** * A frame whose left half has luma {@code left} and right half {@code right}, with * neutral chroma. */ - private static VideoFrame frame(int width, int height, int left, int right) { + private static VideoFrame frame(int width, int height, int left, int right, int rotation) { NativeI420Buffer buffer = NativeI420Buffer.allocate(width, height); ByteBuffer y = buffer.getDataY(); @@ -236,7 +265,7 @@ private static VideoFrame frame(int width, int height, int left, int right) { fill(buffer.getDataU()); fill(buffer.getDataV()); - return new VideoFrame(buffer, System.nanoTime()); + return new VideoFrame(buffer, rotation, System.nanoTime()); } private static void fill(ByteBuffer plane) { @@ -245,6 +274,18 @@ private static void fill(ByteBuffer plane) { } } + private static void assertSamePixels(Image expected, Image actual) { + assertEquals((int) expected.getWidth(), (int) actual.getWidth()); + assertEquals((int) expected.getHeight(), (int) actual.getHeight()); + + for (int y = 0; y < (int) expected.getHeight(); y++) { + for (int x = 0; x < (int) expected.getWidth(); x++) { + assertEquals(expected.getPixelReader().getArgb(x, y), actual.getPixelReader().getArgb(x, y), + "Pixel " + x + "," + y); + } + } + } + private static void assertGray(Color color) { assertNotNull(color); assertTrue(Math.abs(color.getRed() - color.getGreen()) < 0.04 && Math.abs(color.getGreen() - color.getBlue()) < 0.04 diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoFrame.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoFrame.java index 248a9d4b..4aabc689 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoFrame.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoFrame.java @@ -32,7 +32,7 @@ public class VideoFrame implements RefCounted { /** The underlying frame buffer. */ public final VideoFrameBuffer buffer; - /** Rotation of the frame in degrees. */ + /** Clockwise rotation of the frame in degrees: 0, 90, 180 or 270. */ public final int rotation; /** Timestamp of the frame in nanoseconds. */ @@ -55,7 +55,9 @@ public VideoFrame(VideoFrameBuffer buffer, long timestampNs) { * Creates a new VideoFrame with the specified buffer, rotation and timestamp. * * @param buffer The video frame buffer containing the actual frame data. - * @param rotation The rotation of the frame in degrees (must be a multiple of 90). + * @param rotation The clockwise rotation of the frame in degrees (must be a multiple of 90). + * Any multiple is taken, and kept as its equal in 0, 90, 180 or 270, so that + * -90 and 450 are stored as 270 and 90. * @param timestampNs The timestamp of the frame in nanoseconds. * * @throws IllegalArgumentException If buffer is null or rotation is not a multiple of 90. @@ -69,7 +71,7 @@ public VideoFrame(VideoFrameBuffer buffer, int rotation, long timestampNs) { } this.buffer = buffer; - this.rotation = rotation; + this.rotation = ((rotation % 360) + 360) % 360; this.timestampNs = timestampNs; } diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/client/VideoViewTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/client/VideoViewTests.java index 7a373bab..afd92bc5 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/client/VideoViewTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/client/VideoViewTests.java @@ -16,6 +16,7 @@ package dev.onvoid.webrtc.client; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -80,6 +81,22 @@ void mirrors() { assertTrue(brightness(paint(view).getRGB(8, 24)) > 192); } + @Test + void anyRotationMultiple() { + // A rotation outside 0..270 is shown as its equal inside it. + assertArrayEquals(rotated(270), rotated(-90)); + assertArrayEquals(rotated(90), rotated(450)); + + // Sideways, a 4:3 picture in a 4:3 view leaves bars on the sides. + VideoView view = new VideoView(); + view.setSize(64, 48); + view.accept(frame(64, 48, 128, 128, -90)); + + BufferedImage painted = paint(view); + assertEquals(Color.BLACK, new Color(painted.getRGB(2, 24))); + assertGray(new Color(painted.getRGB(32, 24))); + } + @Test void showsRemoteVideo() throws Exception { Peer[] peers = pair(); @@ -112,11 +129,24 @@ void showsRemoteVideo() throws Exception { assertEquals(320, snapshot.getWidth()); } + /** The pixels a 64x48 view paints of a 64x48 two-tone frame with the given rotation. */ + private static int[] rotated(int rotation) { + VideoView view = new VideoView(); + view.setSize(64, 48); + view.accept(frame(64, 48, 16, 235, rotation)); + + return paint(view).getRGB(0, 0, 64, 48, null, 0, 64); + } + + private static VideoFrame frame(int width, int height, int left, int right) { + return frame(width, height, left, right, 0); + } + /** * A frame whose left half has luma {@code left} and right half {@code right}, with * neutral chroma. */ - private static VideoFrame frame(int width, int height, int left, int right) { + private static VideoFrame frame(int width, int height, int left, int right, int rotation) { NativeI420Buffer buffer = NativeI420Buffer.allocate(width, height); ByteBuffer y = buffer.getDataY(); @@ -129,7 +159,7 @@ private static VideoFrame frame(int width, int height, int left, int right) { fill(buffer.getDataU()); fill(buffer.getDataV()); - return new VideoFrame(buffer, System.nanoTime()); + return new VideoFrame(buffer, rotation, System.nanoTime()); } private static void fill(ByteBuffer plane) {