From 04d9a751c67038ad452d7bdf2e9e6f15ca12f1e2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 13 Feb 2025 06:04:00 -0800 Subject: [PATCH] Add encoder setting to set frame repeat interval This can be used for screen recording to produce files without large gaps between frames. PiperOrigin-RevId: 726451010 --- .../transformer/DefaultEncoderFactory.java | 7 ++++ .../transformer/VideoEncoderSettings.java | 31 ++++++++++++++++ .../DefaultEncoderFactoryTest.java | 36 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index 50cb39c5a4..b57be871ea 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -385,6 +385,13 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } } + long repeatPreviousFrameIntervalUs = + supportedVideoEncoderSettings.repeatPreviousFrameIntervalUs; + if (repeatPreviousFrameIntervalUs != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setLong( + MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, repeatPreviousFrameIntervalUs); + } + if (Util.SDK_INT >= 35) { mediaFormat.setInteger(MediaFormat.KEY_IMPORTANCE, max(0, -codecPriority)); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java index 927e788a75..e760af5ca6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -82,6 +82,7 @@ public final class VideoEncoderSettings { private float iFrameIntervalSeconds; private int operatingRate; private int priority; + private long repeatPreviousFrameIntervalUs; private boolean enableHighQualityTargeting; /** Creates a new instance. */ @@ -93,6 +94,7 @@ public final class VideoEncoderSettings { this.iFrameIntervalSeconds = DEFAULT_I_FRAME_INTERVAL_SECONDS; this.operatingRate = NO_VALUE; this.priority = NO_VALUE; + this.repeatPreviousFrameIntervalUs = NO_VALUE; } private Builder(VideoEncoderSettings videoEncoderSettings) { @@ -103,6 +105,7 @@ public final class VideoEncoderSettings { this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds; this.operatingRate = videoEncoderSettings.operatingRate; this.priority = videoEncoderSettings.priority; + this.repeatPreviousFrameIntervalUs = videoEncoderSettings.repeatPreviousFrameIntervalUs; this.enableHighQualityTargeting = videoEncoderSettings.enableHighQualityTargeting; } @@ -190,6 +193,21 @@ public final class VideoEncoderSettings { return this; } + /** + * Sets the threshold duration between input frames beyond which to repeat the previous frame if + * no new frame has been received, in microseconds. The default value is {@link #NO_VALUE}, + * which means that frames are not automatically repeated. + * + * @param repeatPreviousFrameIntervalUs The {@linkplain + * MediaFormat#KEY_REPEAT_PREVIOUS_FRAME_AFTER frame repeat interval}, in microseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setRepeatPreviousFrameIntervalUs(long repeatPreviousFrameIntervalUs) { + this.repeatPreviousFrameIntervalUs = repeatPreviousFrameIntervalUs; + return this; + } + /** * Sets whether to enable automatic adjustment of the bitrate to target a high quality encoding. * @@ -223,6 +241,7 @@ public final class VideoEncoderSettings { iFrameIntervalSeconds, operatingRate, priority, + repeatPreviousFrameIntervalUs, enableHighQualityTargeting); } } @@ -248,6 +267,12 @@ public final class VideoEncoderSettings { /** The encoder {@link MediaFormat#KEY_PRIORITY priority}. */ public final int priority; + /** + * The {@linkplain MediaFormat#KEY_REPEAT_PREVIOUS_FRAME_AFTER frame repeat interval}, in + * microseconds. + */ + public final long repeatPreviousFrameIntervalUs; + /** Whether the encoder should automatically set the bitrate to target a high quality encoding. */ public final boolean enableHighQualityTargeting; @@ -259,6 +284,7 @@ public final class VideoEncoderSettings { float iFrameIntervalSeconds, int operatingRate, int priority, + long repeatPreviousFrameIntervalUs, boolean enableHighQualityTargeting) { this.bitrate = bitrate; this.bitrateMode = bitrateMode; @@ -267,6 +293,7 @@ public final class VideoEncoderSettings { this.iFrameIntervalSeconds = iFrameIntervalSeconds; this.operatingRate = operatingRate; this.priority = priority; + this.repeatPreviousFrameIntervalUs = repeatPreviousFrameIntervalUs; this.enableHighQualityTargeting = enableHighQualityTargeting; } @@ -293,6 +320,7 @@ public final class VideoEncoderSettings { && iFrameIntervalSeconds == that.iFrameIntervalSeconds && operatingRate == that.operatingRate && priority == that.priority + && repeatPreviousFrameIntervalUs == that.repeatPreviousFrameIntervalUs && enableHighQualityTargeting == that.enableHighQualityTargeting; } @@ -306,6 +334,9 @@ public final class VideoEncoderSettings { result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds); result = 31 * result + operatingRate; result = 31 * result + priority; + result = + 31 * result + + (int) (repeatPreviousFrameIntervalUs ^ (repeatPreviousFrameIntervalUs >>> 32)); result = 31 * result + (enableHighQualityTargeting ? 1 : 0); return result; } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java index cf92f68d50..f660b98e8f 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java @@ -323,6 +323,42 @@ public class DefaultEncoderFactoryTest { assertThat(configurationMediaFormat.containsKey(MediaFormat.KEY_OPERATING_RATE)).isFalse(); } + @Test + public void + createForVideoEncoding_withRepeatPreviousFrameIntervalUs_configuresEncoderWithRepeatPreviousFrameIntervalUs() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + DefaultCodec videoEncoder = + new DefaultEncoderFactory.Builder(context) + .setRequestedVideoEncoderSettings( + new VideoEncoderSettings.Builder().setRepeatPreviousFrameIntervalUs(33_333).build()) + .build() + .createForVideoEncoding(requestedVideoFormat); + + assertThat( + videoEncoder + .getConfigurationMediaFormat() + .getLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER)) + .isEqualTo(33_333); + } + + @Test + public void + createForVideoEncoding_withDefaultEncoderSettings_doesNotConfigureRepeatPreviousFrameIntervalUs() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + DefaultCodec videoEncoder = + new DefaultEncoderFactory.Builder(context) + .build() + .createForVideoEncoding(requestedVideoFormat); + + assertThat( + videoEncoder + .getConfigurationMediaFormat() + .containsKey(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER)) + .isFalse(); + } + @Test public void createForVideoEncoding_withNoAvailableEncoderFromEncoderSelector_throws() { Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30);