diff --git a/libraries/common/src/main/java/androidx/media3/common/Effect.java b/libraries/common/src/main/java/androidx/media3/common/Effect.java index 5504b5d9ef..cf86adc1fb 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Effect.java +++ b/libraries/common/src/main/java/androidx/media3/common/Effect.java @@ -20,4 +20,13 @@ import androidx.media3.common.util.UnstableApi; /** Marker interface for a video frame effect. */ @UnstableApi -public interface Effect {} +public interface Effect { + + /** + * Returns the expected duration of the output stream when the effect is applied given a input + * {@code durationUs}. + */ + default long getDurationAfterEffectApplied(long durationUs) { + return durationUs; + } +} diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java b/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java index 4d43fad9d1..e188debd9a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java @@ -128,6 +128,14 @@ public interface AudioProcessor { /** An empty, direct {@link ByteBuffer}. */ ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); + /** + * Returns the expected duration of the output stream when the processor is applied given a input + * {@code durationUs}. + */ + default long getDurationAfterProcessorApplied(long durationUs) { + return durationUs; + } + /** * Configures the processor to process input audio with the specified format. After calling this * method, call {@link #isActive()} to determine whether the audio processor is active. Returns diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java b/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java index 9a55137524..030b1e3dff 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/SonicAudioProcessor.java @@ -171,6 +171,11 @@ public class SonicAudioProcessor implements AudioProcessor { return inputBytes - checkNotNull(sonic).getPendingInputBytes(); } + @Override + public long getDurationAfterProcessorApplied(long durationUs) { + return getPlayoutDuration(durationUs); + } + @Override public final AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java b/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java index f6e8e69e0e..6226006122 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/SpeedChangingAudioProcessor.java @@ -22,6 +22,7 @@ import static java.lang.Math.round; import androidx.media3.common.C; import androidx.media3.common.util.LongArray; import androidx.media3.common.util.LongArrayQueue; +import androidx.media3.common.util.SpeedProviderUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import java.nio.ByteBuffer; @@ -75,6 +76,11 @@ public final class SpeedChangingAudioProcessor extends BaseAudioProcessor { currentSpeed = 1f; } + @Override + public long getDurationAfterProcessorApplied(long durationUs) { + return SpeedProviderUtil.getDurationAfterSpeedProviderApplied(speedProvider, durationUs); + } + @Override public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { diff --git a/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java new file mode 100644 index 0000000000..8a7bb0432a --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/util/SpeedProviderUtil.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.common.util; + +import static java.lang.Math.min; +import static java.lang.Math.round; + +import androidx.media3.common.C; +import androidx.media3.common.audio.SpeedProvider; + +/** Utilities for {@link SpeedProvider}. */ +@UnstableApi +public class SpeedProviderUtil { + + private SpeedProviderUtil() {} + + /** + * Returns the duration of the output when the given {@link SpeedProvider} is applied given an + * input stream with the given {@code durationUs}. + */ + public static long getDurationAfterSpeedProviderApplied( + SpeedProvider speedProvider, long durationUs) { + long speedChangeTimeUs = 0; + double outputDurationUs = 0; + while (speedChangeTimeUs < durationUs) { + long nextSpeedChangeTimeUs = speedProvider.getNextSpeedChangeTimeUs(speedChangeTimeUs); + if (nextSpeedChangeTimeUs == C.TIME_UNSET) { + nextSpeedChangeTimeUs = Long.MAX_VALUE; + } + outputDurationUs += + (min(nextSpeedChangeTimeUs, durationUs) - speedChangeTimeUs) + / (double) speedProvider.getSpeed(speedChangeTimeUs); + speedChangeTimeUs = nextSpeedChangeTimeUs; + } + return round(outputDurationUs); + } +} diff --git a/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java new file mode 100644 index 0000000000..8368348f56 --- /dev/null +++ b/libraries/common/src/test/java/androidx/media3/common/util/SpeedProviderUtilTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.common.util; + +import static androidx.media3.common.util.SpeedProviderUtil.getDurationAfterSpeedProviderApplied; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.audio.SpeedProvider; +import androidx.media3.test.utils.TestSpeedProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link SpeedProviderUtil}. */ +@RunWith(AndroidJUnit4.class) +public class SpeedProviderUtilTest { + + @Test + public void getDurationAfterProcessorApplied_returnsCorrectDuration() throws Exception { + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 120}, /* speeds= */ new float[] {3, 0.5f}); + + assertThat(getDurationAfterSpeedProviderApplied(speedProvider, /* durationUs= */ 150)) + .isEqualTo(100); + } + + @Test + public void getDurationAfterProcessorApplied_durationOnSpeedChange_returnsCorrectDuration() + throws Exception { + SpeedProvider speedProvider = + TestSpeedProvider.createWithStartTimes( + /* startTimesUs= */ new long[] {0, 113}, /* speeds= */ new float[] {2, 1}); + + assertThat(getDurationAfterSpeedProviderApplied(speedProvider, /* durationUs= */ 113)) + .isEqualTo(57); + } +} diff --git a/libraries/effect/src/main/java/androidx/media3/effect/SpeedChangeEffect.java b/libraries/effect/src/main/java/androidx/media3/effect/SpeedChangeEffect.java index f8e22c8a59..b592000015 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/SpeedChangeEffect.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/SpeedChangeEffect.java @@ -21,6 +21,7 @@ import android.content.Context; import androidx.annotation.FloatRange; import androidx.media3.common.C; import androidx.media3.common.audio.SpeedProvider; +import androidx.media3.common.util.SpeedProviderUtil; import androidx.media3.common.util.UnstableApi; /** @@ -72,4 +73,9 @@ public final class SpeedChangeEffect implements GlEffect { return speedProvider.getSpeed(/* timeUs= */ 0) == 1 && speedProvider.getNextSpeedChangeTimeUs(/* timeUs= */ 0) == C.TIME_UNSET; } + + @Override + public long getDurationAfterEffectApplied(long durationUs) { + return SpeedProviderUtil.getDurationAfterSpeedProviderApplied(speedProvider, durationUs); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/TrimmingAudioProcessor.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/TrimmingAudioProcessor.java index 0e0f5f06f4..759ee64c51 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/TrimmingAudioProcessor.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/TrimmingAudioProcessor.java @@ -70,6 +70,13 @@ import java.nio.ByteBuffer; return trimmedFrameCount; } + @Override + public long getDurationAfterProcessorApplied(long durationUs) { + return durationUs + - Util.sampleCountToDurationUs( + /* sampleCount= */ trimEndFrames + trimStartFrames, inputAudioFormat.sampleRate); + } + @Override public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException {