diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 5774bee08c6..b3c982b670e 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1373,6 +1373,38 @@ public static long msToUs(long timeMs) { 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. + */ + 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}. + */ + 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/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 7cc7e57e5f2..6164228e308 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -27,6 +27,7 @@ import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.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 void sparseLongArrayMaxValue_emptyArray_throws() { 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/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index ad83c8408cc..497018c4c58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -17,6 +17,8 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Util.castNonNull; +import static com.google.android.exoplayer2.util.Util.durationUsToSampleCount; +import static com.google.android.exoplayer2.util.Util.sampleCountToDurationUs; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; @@ -244,7 +246,10 @@ public void setAudioTrack( 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; passthroughWorkaroundPauseOffset = 0; @@ -280,7 +285,7 @@ public long getCurrentPositionUs(boolean sourceEnded) { 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); @@ -426,7 +431,8 @@ public void handleEndOfStream(long writtenFrames) { * @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(); } @@ -497,23 +503,18 @@ private void maybePollAndCheckTimestamp(long systemTimeUs) { } // 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(); @@ -545,14 +546,6 @@ private void maybeUpdateLatency(long systemTimeUs) { } } - 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; @@ -584,7 +577,7 @@ private static boolean needsPassthroughWorkarounds(@C.Encoding int outputEncodin } private long getPlaybackHeadPositionUs() { - return framesToDurationUs(getPlaybackHeadPosition()); + return sampleCountToDurationUs(getPlaybackHeadPosition(), outputSampleRate); } /** @@ -602,7 +595,7 @@ private long getPlaybackHeadPosition() { long elapsedTimeSinceStopUs = (currentTimeMs * 1000) - 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/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 6f291e48910..5cc33caec2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -2079,11 +2079,11 @@ public boolean canReuseAudioTrack(Configuration newConfiguration) { } 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(