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
This commit is contained in:
andrewlewis 2025-02-13 06:04:00 -08:00 committed by Copybara-Service
parent 2a91d47ea9
commit 04d9a751c6
3 changed files with 74 additions and 0 deletions

View File

@ -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) { if (Util.SDK_INT >= 35) {
mediaFormat.setInteger(MediaFormat.KEY_IMPORTANCE, max(0, -codecPriority)); mediaFormat.setInteger(MediaFormat.KEY_IMPORTANCE, max(0, -codecPriority));
} }

View File

@ -82,6 +82,7 @@ public final class VideoEncoderSettings {
private float iFrameIntervalSeconds; private float iFrameIntervalSeconds;
private int operatingRate; private int operatingRate;
private int priority; private int priority;
private long repeatPreviousFrameIntervalUs;
private boolean enableHighQualityTargeting; private boolean enableHighQualityTargeting;
/** Creates a new instance. */ /** Creates a new instance. */
@ -93,6 +94,7 @@ public final class VideoEncoderSettings {
this.iFrameIntervalSeconds = DEFAULT_I_FRAME_INTERVAL_SECONDS; this.iFrameIntervalSeconds = DEFAULT_I_FRAME_INTERVAL_SECONDS;
this.operatingRate = NO_VALUE; this.operatingRate = NO_VALUE;
this.priority = NO_VALUE; this.priority = NO_VALUE;
this.repeatPreviousFrameIntervalUs = NO_VALUE;
} }
private Builder(VideoEncoderSettings videoEncoderSettings) { private Builder(VideoEncoderSettings videoEncoderSettings) {
@ -103,6 +105,7 @@ public final class VideoEncoderSettings {
this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds; this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds;
this.operatingRate = videoEncoderSettings.operatingRate; this.operatingRate = videoEncoderSettings.operatingRate;
this.priority = videoEncoderSettings.priority; this.priority = videoEncoderSettings.priority;
this.repeatPreviousFrameIntervalUs = videoEncoderSettings.repeatPreviousFrameIntervalUs;
this.enableHighQualityTargeting = videoEncoderSettings.enableHighQualityTargeting; this.enableHighQualityTargeting = videoEncoderSettings.enableHighQualityTargeting;
} }
@ -190,6 +193,21 @@ public final class VideoEncoderSettings {
return this; 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. * Sets whether to enable automatic adjustment of the bitrate to target a high quality encoding.
* *
@ -223,6 +241,7 @@ public final class VideoEncoderSettings {
iFrameIntervalSeconds, iFrameIntervalSeconds,
operatingRate, operatingRate,
priority, priority,
repeatPreviousFrameIntervalUs,
enableHighQualityTargeting); enableHighQualityTargeting);
} }
} }
@ -248,6 +267,12 @@ public final class VideoEncoderSettings {
/** The encoder {@link MediaFormat#KEY_PRIORITY priority}. */ /** The encoder {@link MediaFormat#KEY_PRIORITY priority}. */
public final int 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. */ /** Whether the encoder should automatically set the bitrate to target a high quality encoding. */
public final boolean enableHighQualityTargeting; public final boolean enableHighQualityTargeting;
@ -259,6 +284,7 @@ public final class VideoEncoderSettings {
float iFrameIntervalSeconds, float iFrameIntervalSeconds,
int operatingRate, int operatingRate,
int priority, int priority,
long repeatPreviousFrameIntervalUs,
boolean enableHighQualityTargeting) { boolean enableHighQualityTargeting) {
this.bitrate = bitrate; this.bitrate = bitrate;
this.bitrateMode = bitrateMode; this.bitrateMode = bitrateMode;
@ -267,6 +293,7 @@ public final class VideoEncoderSettings {
this.iFrameIntervalSeconds = iFrameIntervalSeconds; this.iFrameIntervalSeconds = iFrameIntervalSeconds;
this.operatingRate = operatingRate; this.operatingRate = operatingRate;
this.priority = priority; this.priority = priority;
this.repeatPreviousFrameIntervalUs = repeatPreviousFrameIntervalUs;
this.enableHighQualityTargeting = enableHighQualityTargeting; this.enableHighQualityTargeting = enableHighQualityTargeting;
} }
@ -293,6 +320,7 @@ public final class VideoEncoderSettings {
&& iFrameIntervalSeconds == that.iFrameIntervalSeconds && iFrameIntervalSeconds == that.iFrameIntervalSeconds
&& operatingRate == that.operatingRate && operatingRate == that.operatingRate
&& priority == that.priority && priority == that.priority
&& repeatPreviousFrameIntervalUs == that.repeatPreviousFrameIntervalUs
&& enableHighQualityTargeting == that.enableHighQualityTargeting; && enableHighQualityTargeting == that.enableHighQualityTargeting;
} }
@ -306,6 +334,9 @@ public final class VideoEncoderSettings {
result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds); result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds);
result = 31 * result + operatingRate; result = 31 * result + operatingRate;
result = 31 * result + priority; result = 31 * result + priority;
result =
31 * result
+ (int) (repeatPreviousFrameIntervalUs ^ (repeatPreviousFrameIntervalUs >>> 32));
result = 31 * result + (enableHighQualityTargeting ? 1 : 0); result = 31 * result + (enableHighQualityTargeting ? 1 : 0);
return result; return result;
} }

View File

@ -323,6 +323,42 @@ public class DefaultEncoderFactoryTest {
assertThat(configurationMediaFormat.containsKey(MediaFormat.KEY_OPERATING_RATE)).isFalse(); 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 @Test
public void createForVideoEncoding_withNoAvailableEncoderFromEncoderSelector_throws() { public void createForVideoEncoding_withNoAvailableEncoderFromEncoderSelector_throws() {
Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30);