diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7ed4a03e29c..76d7982f5c8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ stays at its current behavior of `true`. * Extractors: * Audio: + * Fix DTS Express audio buffer underflow issue + ([#650](https://github.com/androidx/media/pull/650)). * Video: * Text: * Remove `ExoplayerCuesDecoder`. Text tracks with `sampleMimeType = diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java index 5e178f0523d..d1a00a21a59 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java @@ -63,6 +63,11 @@ public class DefaultAudioTrackBufferSizeProvider */ private static final int AC3_BUFFER_MULTIPLICATION_FACTOR = 2; + /** + * Default multiplication factor to apply to DTS Express passthrough buffer to avoid underruns. + */ + private static final int DTSHD_BUFFER_MULTIPLICATION_FACTOR = 4; + /** A builder to create {@link DefaultAudioTrackBufferSizeProvider} instances. */ public static class Builder { @@ -72,6 +77,7 @@ public static class Builder { private int passthroughBufferDurationUs; private int offloadBufferDurationUs; private int ac3BufferMultiplicationFactor; + private int dtshdBufferMultiplicationFactor; /** Creates a new builder. */ public Builder() { @@ -81,6 +87,7 @@ public Builder() { passthroughBufferDurationUs = PASSTHROUGH_BUFFER_DURATION_US; offloadBufferDurationUs = OFFLOAD_BUFFER_DURATION_US; ac3BufferMultiplicationFactor = AC3_BUFFER_MULTIPLICATION_FACTOR; + dtshdBufferMultiplicationFactor = DTSHD_BUFFER_MULTIPLICATION_FACTOR; } /** @@ -143,6 +150,16 @@ public Builder setAc3BufferMultiplicationFactor(int ac3BufferMultiplicationFacto return this; } + /** + * Sets the multiplication factor to apply to the passthrough buffer for DTS-HD (DTS Express) to + * avoid underruns. Default is {@link #DTSHD_BUFFER_MULTIPLICATION_FACTOR}. + */ + @CanIgnoreReturnValue + public Builder setDtshdBufferMultiplicationFactor(int dtshdBufferMultiplicationFactor) { + this.dtshdBufferMultiplicationFactor = dtshdBufferMultiplicationFactor; + return this; + } + /** Build the {@link DefaultAudioTrackBufferSizeProvider}. */ public DefaultAudioTrackBufferSizeProvider build() { return new DefaultAudioTrackBufferSizeProvider(this); @@ -170,6 +187,12 @@ public DefaultAudioTrackBufferSizeProvider build() { */ public final int ac3BufferMultiplicationFactor; + /** + * The multiplication factor to apply to DTS-HD (DTS Express) passthrough buffer to avoid + * underruns. + */ + public final int dtshdBufferMultiplicationFactor; + protected DefaultAudioTrackBufferSizeProvider(Builder builder) { minPcmBufferDurationUs = builder.minPcmBufferDurationUs; maxPcmBufferDurationUs = builder.maxPcmBufferDurationUs; @@ -177,6 +200,7 @@ protected DefaultAudioTrackBufferSizeProvider(Builder builder) { passthroughBufferDurationUs = builder.passthroughBufferDurationUs; offloadBufferDurationUs = builder.offloadBufferDurationUs; ac3BufferMultiplicationFactor = builder.ac3BufferMultiplicationFactor; + dtshdBufferMultiplicationFactor = builder.dtshdBufferMultiplicationFactor; } @Override @@ -232,7 +256,13 @@ protected int getPassthroughBufferSizeInBytes(@C.Encoding int encoding, int bitr int bufferSizeUs = passthroughBufferDurationUs; if (encoding == C.ENCODING_AC3) { bufferSizeUs *= ac3BufferMultiplicationFactor; + } else if (encoding == C.ENCODING_DTS_HD) { + // DTS-HD (DTS Express) for streaming uses a frame size (number of audio samples per channel + // per frame) of 4096. This requires a higher multiple for the buffersize computation. + // Otherwise, there will be buffer underflow during DASH playback. + bufferSizeUs *= dtshdBufferMultiplicationFactor; } + int byteRate = bitrate != Format.NO_VALUE ? divide(bitrate, 8, RoundingMode.CEILING) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderDTSHDTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderDTSHDTest.java new file mode 100644 index 00000000000..bd518167a40 --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProviderDTSHDTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 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.exoplayer.audio; + +import static androidx.media3.common.C.MICROS_PER_SECOND; +import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; +import static androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link DefaultAudioTrackBufferSizeProvider} DTS-HD (DTS Express) audio. */ +@RunWith(AndroidJUnit4.class) +public class DefaultAudioTrackBufferSizeProviderDTSHDTest { + + private static final DefaultAudioTrackBufferSizeProvider DEFAULT = + new DefaultAudioTrackBufferSizeProvider.Builder().build(); + + @Test + public void + getBufferSizeInBytes_passthroughDtshdAndNoBitrate_assumesMaxByteRateTimesMultiplicationFactor() { + int bufferSize = + DEFAULT.getBufferSizeInBytes( + /* minBufferSizeInBytes= */ 0, + /* encoding= */ C.ENCODING_DTS_HD, + /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, + /* pcmFrameSize= */ 1, + /* sampleRate= */ 0, + /* bitrate= */ Format.NO_VALUE, + /* maxAudioTrackPlaybackSpeed= */ 1); + + assertThat(bufferSize) + .isEqualTo( + durationUsToDtshdMaxBytes(DEFAULT.passthroughBufferDurationUs) + * DEFAULT.dtshdBufferMultiplicationFactor); + } + + @Test + public void + getBufferSizeInBytes_passthroughDtshdAt384Kbits_isPassthroughBufferSizeTimesMultiplicationFactor() { + int bufferSize = + DEFAULT.getBufferSizeInBytes( + /* minBufferSizeInBytes= */ 0, + /* encoding= */ C.ENCODING_DTS_HD, + /* outputMode= */ OUTPUT_MODE_PASSTHROUGH, + /* pcmFrameSize= */ 1, + /* sampleRate= */ 0, + /* bitrate= */ 384_000, + /* maxAudioTrackPlaybackSpeed= */ 1); + + // Default buffer duration 0.25s => 0.25 * 384000 / 8 = 12000 + assertThat(bufferSize).isEqualTo(12000 * DEFAULT.dtshdBufferMultiplicationFactor); + } + + private static int durationUsToDtshdMaxBytes(long durationUs) { + return (int) + (durationUs * getMaximumEncodedRateBytesPerSecond(C.ENCODING_DTS_HD) / MICROS_PER_SECOND); + } +}