diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b79fc3ba19..6a6836ad2e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -64,6 +64,9 @@ * Add support for 24/32-bit big endian PCM in MP4 and Matroska, and parse PCM encoding for `lpcm` in MP4. * Add support for extracting Vorbis audio in MP4. + * Fix a bug where `Player.getState()` never transitioned to `STATE_ENDED` + when playing very short files + ([#538](https://github.com/androidx/media/issues/538)). * Audio Offload: * Add `AudioSink.getFormatOffloadSupport(Format)` that retrieves level of offload support the sink can provide for the format through a diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 98d1113eda..cec35de3fe 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1514,6 +1514,40 @@ public final class Util { return (timeMs == C.TIME_UNSET || timeMs == C.TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } + /** + * Returns the total duration (in microseconds) of {@code sampleCount} samples of equal duration + * at {@code sampleRate}. + * + *

If {@code sampleRate} is less than {@link C#MICROS_PER_SECOND}, the duration produced by + * this method can be reversed to the original sample count using {@link + * #durationUsToSampleCount(long, int)}. + * + * @param sampleCount The number of samples. + * @param sampleRate The sample rate, in samples per second. + * @return The total duration, in microseconds, of {@code sampleCount} samples. + */ + @UnstableApi + public static long sampleCountToDurationUs(long sampleCount, int sampleRate) { + return (sampleCount * C.MICROS_PER_SECOND) / sampleRate; + } + + /** + * Returns the number of samples required to represent {@code durationUs} of media at {@code + * sampleRate}, assuming all samples are equal duration except the last one which may be shorter. + * + *

The result of this method cannot be generally reversed to the original duration with + * {@link #sampleCountToDurationUs(long, int)}, due to information lost when rounding to a whole + * number of samples. + * + * @param durationUs The duration in microseconds. + * @param sampleRate The sample rate in samples per second. + * @return The number of samples required to represent {@code durationUs}. + */ + @UnstableApi + public static long durationUsToSampleCount(long durationUs, int sampleRate) { + return Util.ceilDivide(durationUs * sampleRate, C.MICROS_PER_SECOND); + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java index 54b0f09a76..5b213edc71 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java @@ -27,6 +27,7 @@ import static androidx.media3.common.util.Util.parseXsDateTime; import static androidx.media3.common.util.Util.parseXsDuration; import static androidx.media3.common.util.Util.unescapeFileName; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -832,6 +833,21 @@ public class UtilTest { assertThrows(NoSuchElementException.class, () -> maxValue(new SparseLongArray())); } + @Test + public void sampleCountToDuration_thenDurationToSampleCount_returnsOriginalValue() { + // Use co-prime increments, to maximise 'discord' between sampleCount and sampleRate. + for (long originalSampleCount = 0; originalSampleCount < 100_000; originalSampleCount += 97) { + for (int sampleRate = 89; sampleRate < 1_000_000; sampleRate += 89) { + long calculatedSampleCount = + Util.durationUsToSampleCount( + Util.sampleCountToDurationUs(originalSampleCount, sampleRate), sampleRate); + assertWithMessage("sampleCount=%s, sampleRate=%s", originalSampleCount, sampleRate) + .that(calculatedSampleCount) + .isEqualTo(originalSampleCount); + } + } + } + @Test public void parseXsDuration_returnsParsedDurationInMillis() { assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java index a558f28a64..e4985fc7c1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioTrackPositionTracker.java @@ -17,7 +17,9 @@ package androidx.media3.exoplayer.audio; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.common.util.Util.durationUsToSampleCount; import static androidx.media3.common.util.Util.msToUs; +import static androidx.media3.common.util.Util.sampleCountToDurationUs; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; @@ -257,7 +259,10 @@ import java.lang.reflect.Method; outputSampleRate = audioTrack.getSampleRate(); needsPassthroughWorkarounds = isPassthrough && needsPassthroughWorkarounds(outputEncoding); isOutputPcm = Util.isEncodingLinearPcm(outputEncoding); - bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; + bufferSizeUs = + isOutputPcm + ? sampleCountToDurationUs(bufferSize / outputPcmFrameSize, outputSampleRate) + : C.TIME_UNSET; rawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 0; expectRawPlaybackHeadReset = false; @@ -295,7 +300,7 @@ import java.lang.reflect.Method; if (useGetTimestampMode) { // Calculate the speed-adjusted position using the timestamp (which may be in the future). long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); - long timestampPositionUs = framesToDurationUs(timestampPositionFrames); + long timestampPositionUs = sampleCountToDurationUs(timestampPositionFrames, outputSampleRate); long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); elapsedSinceTimestampUs = Util.getMediaDurationForPlayoutDuration(elapsedSinceTimestampUs, audioTrackPlaybackSpeed); @@ -441,7 +446,8 @@ import java.lang.reflect.Method; * @return Whether the audio track has any pending data to play out. */ public boolean hasPendingData(long writtenFrames) { - return writtenFrames > durationUsToFrames(getCurrentPositionUs(/* sourceEnded= */ false)) + long currentPositionUs = getCurrentPositionUs(/* sourceEnded= */ false); + return writtenFrames > durationUsToSampleCount(currentPositionUs, outputSampleRate) || forceHasPendingData(); } @@ -529,23 +535,18 @@ import java.lang.reflect.Method; } // Check the timestamp and accept/reject it. - long audioTimestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs(); - long audioTimestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); + long timestampSystemTimeUs = audioTimestampPoller.getTimestampSystemTimeUs(); + long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); long playbackPositionUs = getPlaybackHeadPositionUs(); - if (Math.abs(audioTimestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + if (Math.abs(timestampSystemTimeUs - systemTimeUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { listener.onSystemTimeUsMismatch( - audioTimestampPositionFrames, - audioTimestampSystemTimeUs, - systemTimeUs, - playbackPositionUs); + timestampPositionFrames, timestampSystemTimeUs, systemTimeUs, playbackPositionUs); audioTimestampPoller.rejectTimestamp(); - } else if (Math.abs(framesToDurationUs(audioTimestampPositionFrames) - playbackPositionUs) + } else if (Math.abs( + sampleCountToDurationUs(timestampPositionFrames, outputSampleRate) - playbackPositionUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { listener.onPositionFramesMismatch( - audioTimestampPositionFrames, - audioTimestampSystemTimeUs, - systemTimeUs, - playbackPositionUs); + timestampPositionFrames, timestampSystemTimeUs, systemTimeUs, playbackPositionUs); audioTimestampPoller.rejectTimestamp(); } else { audioTimestampPoller.acceptTimestamp(); @@ -577,14 +578,6 @@ import java.lang.reflect.Method; } } - private long framesToDurationUs(long frameCount) { - return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; - } - - private long durationUsToFrames(long durationUs) { - return (durationUs * outputSampleRate) / C.MICROS_PER_SECOND; - } - private void resetSyncParams() { smoothedPlayheadOffsetUs = 0; playheadOffsetCount = 0; @@ -616,7 +609,7 @@ import java.lang.reflect.Method; } private long getPlaybackHeadPositionUs() { - return framesToDurationUs(getPlaybackHeadPosition()); + return sampleCountToDurationUs(getPlaybackHeadPosition(), outputSampleRate); } /** @@ -634,7 +627,7 @@ import java.lang.reflect.Method; long elapsedTimeSinceStopUs = msToUs(currentTimeMs) - stopTimestampUs; long mediaTimeSinceStopUs = Util.getMediaDurationForPlayoutDuration(elapsedTimeSinceStopUs, audioTrackPlaybackSpeed); - long framesSinceStop = durationUsToFrames(mediaTimeSinceStopUs); + long framesSinceStop = durationUsToSampleCount(mediaTimeSinceStopUs, outputSampleRate); return min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); } if (currentTimeMs - lastRawPlaybackHeadPositionSampleTimeMs diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index 73fa7805a8..0e56471b09 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -2051,11 +2051,11 @@ public final class DefaultAudioSink implements AudioSink { } public long inputFramesToDurationUs(long frameCount) { - return (frameCount * C.MICROS_PER_SECOND) / inputFormat.sampleRate; + return Util.sampleCountToDurationUs(frameCount, inputFormat.sampleRate); } public long framesToDurationUs(long frameCount) { - return (frameCount * C.MICROS_PER_SECOND) / outputSampleRate; + return Util.sampleCountToDurationUs(frameCount, outputSampleRate); } public AudioTrack buildAudioTrack(