diff --git a/app/src/main/java/nl/rogro82/pipup/PopupProps.kt b/app/src/main/java/nl/rogro82/pipup/PopupProps.kt index 3f42f97..7234c84 100644 --- a/app/src/main/java/nl/rogro82/pipup/PopupProps.kt +++ b/app/src/main/java/nl/rogro82/pipup/PopupProps.kt @@ -104,8 +104,12 @@ data class PopupProps( // hardware decoder), false = always hardware, null/absent = automatic (software on // Android < 8 and on Amlogic SoCs, whose hardware decoder shares the video layer with // the HDMI input and freezes it when released - reported on a Xiaomi projector). + // height: optional explicit box height. When set, the stream area (and its poster) + // uses it until the first frame reports the stream's display aspect; without it the + // box takes the poster's aspect, which is wrong for anamorphic NVR snapshots. data class Video(val uri: String, val width: Int = DEFAULT_MEDIA_WIDTH, val muted: Boolean = false, - val poster: String? = null, val softwareDecoder: Boolean? = null): Media() + val poster: String? = null, val softwareDecoder: Boolean? = null, + val height: Int? = null): Media() @JsonIgnoreProperties(ignoreUnknown = true) data class Image(val uri: String, val width: Int = DEFAULT_MEDIA_WIDTH): Media() @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/app/src/main/java/nl/rogro82/pipup/PopupView.kt b/app/src/main/java/nl/rogro82/pipup/PopupView.kt index a9e2d5d..24f8475 100644 --- a/app/src/main/java/nl/rogro82/pipup/PopupView.kt +++ b/app/src/main/java/nl/rogro82/pipup/PopupView.kt @@ -418,10 +418,13 @@ sealed class PopupView(context: Context, val popup: PopupProps) : LinearLayout(c this@Video.visibility = View.VISIBLE } frame.addView(textureView) + val provisionalHeight = provisionalVideoHeight(media.width, media.height) attachPoster(frame, media.poster, - FrameLayout.LayoutParams(media.width, media.width * 9 / 16).apply { gravity = Gravity.CENTER } - ) { pw, ph -> sizeTo(media.width, (media.width.toLong() * ph / pw).toInt()) } - sizeTo(media.width, media.width * 9 / 16) + FrameLayout.LayoutParams(media.width, provisionalHeight).apply { gravity = Gravity.CENTER } + ) { pw, ph -> + posterVideoHeight(media.width, media.height, pw, ph)?.let { sizeTo(media.width, it) } + } + sizeTo(media.width, provisionalHeight) val isRtsp = uri.startsWith("rtsp://", ignoreCase = true) || uri.startsWith("rtsps://", ignoreCase = true) val item = MediaItem.fromUri(uri) @@ -967,3 +970,14 @@ internal fun heightForDisplayAspect(targetWidth: Int, width: Int, height: Int, p val displayWidth = width * ratio return Math.round(targetWidth * height / displayWidth) } + +/// Stream-area height before the first frame: an explicit [requestedHeight], else 16:9. +internal fun provisionalVideoHeight(width: Int, requestedHeight: Int?): Int = + requestedHeight?.takeIf { it > 0 } ?: (width * 9 / 16) + +/// Height to resize the stream area to once the poster has decoded, or null to keep the box. +/// With an explicit [requestedHeight] the box stays put: a camera snapshot can be anamorphic +/// (an NVR's 704x480 still of a 4:3 picture), so its pixel size is not the stream's shape. +internal fun posterVideoHeight(width: Int, requestedHeight: Int?, posterWidth: Int, posterHeight: Int): Int? = + if (requestedHeight != null && requestedHeight > 0) null + else (width.toLong() * posterHeight / posterWidth).toInt() diff --git a/app/src/test/java/nl/rogro82/pipup/VideoPosterHeightTest.kt b/app/src/test/java/nl/rogro82/pipup/VideoPosterHeightTest.kt new file mode 100644 index 0000000..fdf9099 --- /dev/null +++ b/app/src/test/java/nl/rogro82/pipup/VideoPosterHeightTest.kt @@ -0,0 +1,32 @@ +package nl.rogro82.pipup + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class VideoPosterHeightTest { + @Test + fun withoutRequestedHeightTheBoxStartsSixteenNine() { + assertEquals(360, provisionalVideoHeight(640, null)) + } + + @Test + fun requestedHeightSetsTheProvisionalBox() { + assertEquals(480, provisionalVideoHeight(640, 480)) + } + + @Test + fun nonPositiveRequestedHeightIsIgnored() { + assertEquals(360, provisionalVideoHeight(640, 0)) + } + + @Test + fun withoutRequestedHeightThePosterSetsTheShape() { + assertEquals(436, posterVideoHeight(640, null, 704, 480)) + } + + @Test + fun requestedHeightKeepsTheBoxForAnAnamorphicPoster() { + assertNull(posterVideoHeight(640, 480, 704, 480)) + } +}