diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfig.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfig.kt index 152678261c..fdde89af77 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfig.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfig.kt @@ -34,11 +34,19 @@ internal object VideoCompressorConfigFactory { val newFrameRate = min(originalFrameRate, DEFAULT_FRAME_RATE) // If we need to resize the video, we also want to recalculate the bitrate - val newBitrate = resizer.calculateOptimalBitrate(Size(width, height), newFrameRate) + val optimalBitrate = resizer.calculateOptimalBitrate(Size(width, height), newFrameRate).toInt() + + // Never ask the encoder for more than the source already carries: re-encoding a video that is + // already efficiently encoded at a low bitrate otherwise inflates it, making the upload both + // slower and bigger than sending the original. METADATA_KEY_BITRATE is the container total, + // video plus audio, so it is a safe upper bound for the video track alone. + // See https://github.com/element-hq/element-x-android/issues/4191 + val sourceBitrate = metadata?.bitrate?.takeIf { it > 0 } + val newBitrate = sourceBitrate?.let { min(optimalBitrate.toLong(), it).toInt() } ?: optimalBitrate return VideoCompressorConfig( videoCompressorHelper = resizer, - newBitRate = newBitrate.toInt(), + newBitRate = newBitrate, newFrameRate = newFrameRate, ) } diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt index 6af0956541..52e6b6f0c3 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt @@ -33,6 +33,48 @@ class VideoCompressorConfigFactoryTest : RobolectricTest() { assertThat(videoCompressorConfig.newBitRate).isNotEqualTo(VideoEncoderSettings.NO_VALUE) } + @Test + fun `the bitrate is clamped to the source bitrate when the source is already low`() { + // A 720x720 clip encoded at 615 kbps: the calculated optimal is far higher, which would + // re-encode it into a much bigger file. + val metadata = VideoFileMetadata(width = 720, height = 720, bitrate = 615_000, frameRate = 25, rotation = 0) + + val videoCompressorConfig = VideoCompressorConfigFactory.create( + metadata = metadata, + preset = VideoCompressionPreset.STANDARD, + ) + + assertThat(videoCompressorConfig.newBitRate).isEqualTo(615_000) + } + + @Test + fun `the bitrate is not raised for a high bitrate source`() { + val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 20_000_000, frameRate = 30, rotation = 0) + + val videoCompressorConfig = VideoCompressorConfigFactory.create( + metadata = metadata, + preset = VideoCompressionPreset.STANDARD, + ) + + assertThat(videoCompressorConfig.newBitRate).isLessThan(20_000_000) + assertThat(videoCompressorConfig.newBitRate).isGreaterThan(0) + } + + @Test + fun `an unknown bitrate falls back to the calculated optimal`() { + val unknown = VideoCompressorConfigFactory.create( + metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = -1, frameRate = 30, rotation = 0), + preset = VideoCompressionPreset.STANDARD, + ) + val zero = VideoCompressorConfigFactory.create( + metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 0, frameRate = 30, rotation = 0), + preset = VideoCompressionPreset.STANDARD, + ) + + assertThat(unknown.newBitRate).isGreaterThan(0) + assertThat(zero.newBitRate).isEqualTo(unknown.newBitRate) + } + @Test fun `if the video should be compressed and is larger than 720p it will be resized`() { // Given