diff --git a/android/app/build.gradle b/android/app/build.gradle index 751e19f5..e3fc69f7 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,8 +23,8 @@ android { applicationId "com.openipc.decoder" minSdk 21 targetSdk 36 - versionCode 28 - versionName "2.03" + versionCode 29 + versionName "2.04" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" } diff --git a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java index 1f2026d0..f75d6ca0 100644 --- a/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java @@ -490,6 +490,30 @@ boolean decodeFrame(Frame buffer, boolean codecH265, long watchdogRef) { return needCreate; } + /** + * The picture's size: the crop rectangle when the decoder gives one. KEY_WIDTH / KEY_HEIGHT + * are the buffer, padded to the decoder's alignment (1080 → 1088; one H.265 decoder made + * 1920 wide 2304), which skewed the picture's aspect ratio and the size shown (#166). + */ + static int[] visibleSize(MediaFormat format) { + return visibleSize(format.getInteger(MediaFormat.KEY_WIDTH), + format.getInteger(MediaFormat.KEY_HEIGHT), + cropKey(format, "crop-left"), cropKey(format, "crop-top"), + cropKey(format, "crop-right"), cropKey(format, "crop-bottom")); + } + + private static int cropKey(MediaFormat format, String key) { + return format.containsKey(key) ? format.getInteger(key) : -1; + } + + /** As above from plain values; a crop value < 0 is missing. Crop right / bottom are inclusive. */ + static int[] visibleSize(int width, int height, int left, int top, int right, int bottom) { + // A crop edge outside the buffer is not believed: that axis keeps the buffer size + int w = left >= 0 && right > left && right < width ? right - left + 1 : width; + int h = top >= 0 && bottom > top && bottom < height ? bottom - top + 1 : height; + return new int[]{w, h}; + } + /** Renders every decoded frame that is ready; caller holds decoderLock. */ private void drainOutput(MediaCodec codec) { MediaCodec.BufferInfo info = bufferInfo; @@ -497,13 +521,11 @@ private void drainOutput(MediaCodec codec) { while ((outputBufferId = codec.dequeueOutputBuffer(info, 0)) >= 0 || outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - MediaFormat format = codec.getOutputFormat(); - int mWidth = format.getInteger(MediaFormat.KEY_WIDTH); - int mHeight = format.getInteger(MediaFormat.KEY_HEIGHT); - outWidth = mWidth; - outHeight = mHeight; + int[] size = visibleSize(codec.getOutputFormat()); + outWidth = size[0]; + outHeight = size[1]; if (listener != null) { - listener.onResolutionChanged(mWidth, mHeight); + listener.onResolutionChanged(size[0], size[1]); } } else { codec.releaseOutputBuffer(outputBufferId, true); diff --git a/android/app/src/test/java/com/openipc/decoder/VisibleSizeTest.java b/android/app/src/test/java/com/openipc/decoder/VisibleSizeTest.java new file mode 100644 index 00000000..a7f1da7c --- /dev/null +++ b/android/app/src/test/java/com/openipc/decoder/VisibleSizeTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0 + * + * VisibleSizeTest.java — picture size from the decoder's crop rectangle, not the padded buffer (#166) + * + */ + +package com.openipc.decoder; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; + +public class VisibleSizeTest { + @Test + public void cropGivesThePictureSize() { + // H.264 1080p: 16-line macroblocks pad the buffer to 1088 + assertArrayEquals(new int[]{1920, 1080}, + MediaCodecManager.visibleSize(1920, 1088, 0, 0, 1919, 1079)); + // H.265 1080p on a decoder that pads the width to 2304 (#166) + assertArrayEquals(new int[]{1920, 1080}, + MediaCodecManager.visibleSize(2304, 1088, 0, 0, 1919, 1079)); + // An offset crop + assertArrayEquals(new int[]{1280, 720}, + MediaCodecManager.visibleSize(1280, 736, 0, 8, 1279, 727)); + } + + @Test + public void withoutCropTheBufferSizeStays() { + assertArrayEquals(new int[]{1920, 1088}, + MediaCodecManager.visibleSize(1920, 1088, -1, -1, -1, -1)); + assertArrayEquals(new int[]{640, 480}, + MediaCodecManager.visibleSize(640, 480, 0, 0, 0, 0)); + } + + @Test + public void cropOutsideTheBufferIsIgnored() { + assertArrayEquals(new int[]{1920, 1088}, + MediaCodecManager.visibleSize(1920, 1088, 0, 0, 3839, 2159)); + // Wholly outside, narrower than the buffer (Qodo on #167) + assertArrayEquals(new int[]{1920, 1088}, + MediaCodecManager.visibleSize(1920, 1088, 3000, 2000, 3100, 2100)); + // One axis bad, the other kept + assertArrayEquals(new int[]{1920, 1080}, + MediaCodecManager.visibleSize(1920, 1088, 0, 0, 1920, 1079)); + } +}