Use ceiling divide logic in AudioTrackPositionTracker.hasPendingData
This fixes a bug with playing very short audio files, introduced by
fe710871aa
The existing code using floor integer division results in playback never
transitioning to `STATE_ENDED` because at the end of playback for the
short sample clip provided `currentPositionUs=189937`,
`outputSampleRate=16000` and `(189937 * 16000) / 1000000 = 3038.992`,
while `writtenFrames=3039`. This is fixed by using `Util.ceilDivide`
so we return `3039`, which means
`AudioTrackPositionTracker.hasPendingData()` returns `false` (since
`writtenFrames ==
durationUsToFrames(getCurrentPositionUs(/* sourceEnded= */ false))`).
#minor-release
Issue: androidx/media#538
PiperOrigin-RevId: 554481782
This commit is contained in:
parent
670658f3ae
commit
6e91f0d4c5
@ -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
|
||||
|
@ -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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>The result of this method <b>cannot</b> 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.
|
||||
*
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user