Consider audio output latency when source has ended

With a [previous change](f05e6a7d6e), we makes `hasPendingData()` return `false` once we've found that the `AudioTrack` has played out all the written frames, to avoid it permanently stays `true` even when the source has ended. However, this is aggressive as the audio output device can still have latency in playing out those frames. So `hasPendingData()` should stay `true` a bit longer (for the duration of `latencyUs`) until finally turn to `false`, as well as the `getCurrentPositionUs()` should increment smoothly without a jump for the duration of `latencyUs`.

PiperOrigin-RevId: 738004292
This commit is contained in:
tianyifeng 2025-03-18 07:54:13 -07:00 committed by Copybara-Service
parent 0991dbcd7d
commit 6470c97af4
3 changed files with 44 additions and 46 deletions

View File

@ -284,7 +284,7 @@ import java.lang.reflect.Method;
resetSyncParams(); resetSyncParams();
} }
public long getCurrentPositionUs(boolean sourceEnded) { public long getCurrentPositionUs() {
AudioTrack audioTrack = checkNotNull(this.audioTrack); AudioTrack audioTrack = checkNotNull(this.audioTrack);
if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) { if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
maybeSampleSyncParams(); maybeSampleSyncParams();
@ -307,7 +307,11 @@ import java.lang.reflect.Method;
} else { } else {
if (playheadOffsetCount == 0) { if (playheadOffsetCount == 0) {
// The AudioTrack has started, but we don't have any samples to compute a smoothed position. // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
positionUs = getPlaybackHeadPositionUs(); positionUs =
stopTimestampUs != C.TIME_UNSET
? sampleCountToDurationUs(
getSimulatedPlaybackHeadPositionAfterStop(), outputSampleRate)
: getPlaybackHeadPositionUs();
} else { } else {
// getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off // getPlaybackHeadPositionUs() only has a granularity of ~20 ms, so we base the position off
// the system clock (and a smoothed offset between it and the playhead position) so as to // the system clock (and a smoothed offset between it and the playhead position) so as to
@ -316,8 +320,11 @@ import java.lang.reflect.Method;
Util.getMediaDurationForPlayoutDuration( Util.getMediaDurationForPlayoutDuration(
systemTimeUs + smoothedPlayheadOffsetUs, audioTrackPlaybackSpeed); systemTimeUs + smoothedPlayheadOffsetUs, audioTrackPlaybackSpeed);
} }
if (!sourceEnded) {
positionUs = max(0, positionUs - latencyUs); positionUs = max(0, positionUs - latencyUs);
if (stopTimestampUs != C.TIME_UNSET) {
positionUs =
min(sampleCountToDurationUs(endPlaybackHeadPosition, outputSampleRate), positionUs);
} }
} }
@ -450,14 +457,9 @@ import java.lang.reflect.Method;
* @return Whether the audio track has any pending data to play out. * @return Whether the audio track has any pending data to play out.
*/ */
public boolean hasPendingData(long writtenFrames) { public boolean hasPendingData(long writtenFrames) {
if (stopTimestampUs != C.TIME_UNSET) { return writtenFrames > durationUsToSampleCount(getCurrentPositionUs(), outputSampleRate)
return writtenFrames > getPlaybackHeadPosition() || forceHasPendingData();
} else {
long currentPositionUs = getCurrentPositionUs(/* sourceEnded= */ false);
return writtenFrames > durationUsToSampleCount(currentPositionUs, outputSampleRate)
|| forceHasPendingData(); || forceHasPendingData();
} }
}
/** /**
* Pauses the audio track position tracker, returning whether the audio track needs to be paused * Pauses the audio track position tracker, returning whether the audio track needs to be paused
@ -633,19 +635,11 @@ import java.lang.reflect.Method;
* @return The playback head position, in frames. * @return The playback head position, in frames.
*/ */
private long getPlaybackHeadPosition() { private long getPlaybackHeadPosition() {
long currentTimeMs = clock.elapsedRealtime();
if (stopTimestampUs != C.TIME_UNSET) { if (stopTimestampUs != C.TIME_UNSET) {
if (checkNotNull(this.audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED) { long simulatedPlaybackHeadPositionAfterStop = getSimulatedPlaybackHeadPositionAfterStop();
// If AudioTrack is paused while stopping, then return cached playback head position. return min(endPlaybackHeadPosition, simulatedPlaybackHeadPositionAfterStop);
return stopPlaybackHeadPosition;
}
// Simulate the playback head position up to the total number of frames submitted.
long elapsedTimeSinceStopUs = msToUs(currentTimeMs) - stopTimestampUs;
long mediaTimeSinceStopUs =
Util.getMediaDurationForPlayoutDuration(elapsedTimeSinceStopUs, audioTrackPlaybackSpeed);
long framesSinceStop = durationUsToSampleCount(mediaTimeSinceStopUs, outputSampleRate);
return min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);
} }
long currentTimeMs = clock.elapsedRealtime();
if (currentTimeMs - lastRawPlaybackHeadPositionSampleTimeMs if (currentTimeMs - lastRawPlaybackHeadPositionSampleTimeMs
>= RAW_PLAYBACK_HEAD_POSITION_UPDATE_INTERVAL_MS) { >= RAW_PLAYBACK_HEAD_POSITION_UPDATE_INTERVAL_MS) {
updateRawPlaybackHeadPosition(currentTimeMs); updateRawPlaybackHeadPosition(currentTimeMs);
@ -654,6 +648,19 @@ import java.lang.reflect.Method;
return rawPlaybackHeadPosition + sumRawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); return rawPlaybackHeadPosition + sumRawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
} }
private long getSimulatedPlaybackHeadPositionAfterStop() {
if (checkNotNull(this.audioTrack).getPlayState() == AudioTrack.PLAYSTATE_PAUSED) {
// If AudioTrack is paused while stopping, then return cached playback head position.
return stopPlaybackHeadPosition;
}
// Simulate the playback head position up to the total number of frames submitted.
long elapsedTimeSinceStopUs = msToUs(clock.elapsedRealtime()) - stopTimestampUs;
long mediaTimeSinceStopUs =
Util.getMediaDurationForPlayoutDuration(elapsedTimeSinceStopUs, audioTrackPlaybackSpeed);
long framesSinceStop = durationUsToSampleCount(mediaTimeSinceStopUs, outputSampleRate);
return stopPlaybackHeadPosition + framesSinceStop;
}
private void updateRawPlaybackHeadPosition(long currentTimeMs) { private void updateRawPlaybackHeadPosition(long currentTimeMs) {
AudioTrack audioTrack = checkNotNull(this.audioTrack); AudioTrack audioTrack = checkNotNull(this.audioTrack);
int state = audioTrack.getPlayState(); int state = audioTrack.getPlayState();

View File

@ -677,7 +677,7 @@ public final class DefaultAudioSink implements AudioSink {
if (!isAudioTrackInitialized() || startMediaTimeUsNeedsInit) { if (!isAudioTrackInitialized() || startMediaTimeUsNeedsInit) {
return CURRENT_POSITION_NOT_SET; return CURRENT_POSITION_NOT_SET;
} }
long positionUs = audioTrackPositionTracker.getCurrentPositionUs(sourceEnded); long positionUs = audioTrackPositionTracker.getCurrentPositionUs();
positionUs = min(positionUs, configuration.framesToDurationUs(getWrittenFrames())); positionUs = min(positionUs, configuration.framesToDurationUs(getWrittenFrames()));
return applySkipping(applyMediaPositionParameters(positionUs)); return applySkipping(applyMediaPositionParameters(positionUs));
} }

View File

@ -75,15 +75,14 @@ public class AudioTrackPositionTrackerTest {
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point. // Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false); audioTrackPositionTracker.getCurrentPositionUs();
} }
// Reset audio track and write bytes to simulate position overflow. // Reset audio track and write bytes to simulate position overflow.
audioTrack.flush(); audioTrack.flush();
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isGreaterThan(4294967296L);
.isGreaterThan(4294967296L);
} }
@Test @Test
@ -99,7 +98,7 @@ public class AudioTrackPositionTrackerTest {
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point. // Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false); audioTrackPositionTracker.getCurrentPositionUs();
} }
// Reset audio track to simulate track reuse and transition. // Reset audio track to simulate track reuse and transition.
@ -109,8 +108,7 @@ public class AudioTrackPositionTrackerTest {
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
// Expected position is msToUs(# of writes)*TIME_TO_ADVANCE_MS. // Expected position is msToUs(# of writes)*TIME_TO_ADVANCE_MS.
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(3000000L);
.isEqualTo(3000000L);
} }
@Test @Test
@ -126,14 +124,13 @@ public class AudioTrackPositionTrackerTest {
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point. // Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false); audioTrackPositionTracker.getCurrentPositionUs();
} }
// Reset audio track to simulate track transition and set tracker to expect playback head reset. // Reset audio track to simulate track transition and set tracker to expect playback head reset.
audioTrack.flush(); audioTrack.flush();
audioTrackPositionTracker.expectRawPlaybackHeadReset(); audioTrackPositionTracker.expectRawPlaybackHeadReset();
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(3000000L);
.isEqualTo(3000000L);
// Pause tracker, pause audio track, and advance time to test that position does not change // Pause tracker, pause audio track, and advance time to test that position does not change
// during pause // during pause
@ -142,8 +139,7 @@ public class AudioTrackPositionTrackerTest {
clock.advanceTime(TIME_TO_ADVANCE_MS); clock.advanceTime(TIME_TO_ADVANCE_MS);
// Expected position is msToUs(# of writes)*TIME_TO_ADVANCE_MS. // Expected position is msToUs(# of writes)*TIME_TO_ADVANCE_MS.
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(3000000L);
.isEqualTo(3000000L);
} }
@Test @Test
@ -161,15 +157,14 @@ public class AudioTrackPositionTrackerTest {
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point. // Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
writeBytesAndAdvanceTime(audioTrack1); writeBytesAndAdvanceTime(audioTrack1);
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false); audioTrackPositionTracker.getCurrentPositionUs();
} }
// Reset audio track and set tracker to expect playback head reset to simulate track transition. // Reset audio track and set tracker to expect playback head reset to simulate track transition.
audioTrack1.flush(); audioTrack1.flush();
audioTrackPositionTracker.expectRawPlaybackHeadReset(); audioTrackPositionTracker.expectRawPlaybackHeadReset();
writeBytesAndAdvanceTime(audioTrack1); writeBytesAndAdvanceTime(audioTrack1);
// Test for correct setup with current position being accumulated position. // Test for correct setup with current position being accumulated position.
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(3000000L);
.isEqualTo(3000000L);
// Set new audio track and reset position tracker to simulate transition to new AudioTrack. // Set new audio track and reset position tracker to simulate transition to new AudioTrack.
audioTrackPositionTracker.reset(); audioTrackPositionTracker.reset();
@ -184,8 +179,7 @@ public class AudioTrackPositionTrackerTest {
writeBytesAndAdvanceTime(audioTrack2); writeBytesAndAdvanceTime(audioTrack2);
// Expected position is msToUs(1 write)*TIME_TO_ADVANCE_MS. // Expected position is msToUs(1 write)*TIME_TO_ADVANCE_MS.
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(1000000L);
.isEqualTo(1000000L);
} }
@Test @Test
@ -204,14 +198,13 @@ public class AudioTrackPositionTrackerTest {
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point. // Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false); audioTrackPositionTracker.getCurrentPositionUs();
} }
// Reset audio track and write bytes to simulate position overflow. // Reset audio track and write bytes to simulate position overflow.
audioTrack.flush(); audioTrack.flush();
writeBytesAndAdvanceTime(audioTrack); writeBytesAndAdvanceTime(audioTrack);
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isGreaterThan(4294967296L);
.isGreaterThan(4294967296L);
} }
@Test @Test
@ -234,8 +227,7 @@ public class AudioTrackPositionTrackerTest {
// Advance time during paused state. // Advance time during paused state.
clock.advanceTime(2_000L); clock.advanceTime(2_000L);
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(2_000_000L);
.isEqualTo(2_000_000L);
} }
@Test @Test
@ -261,8 +253,7 @@ public class AudioTrackPositionTrackerTest {
audioTrackPositionTracker.start(); audioTrackPositionTracker.start();
audioTrack.play(); audioTrack.play();
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false)) assertThat(audioTrackPositionTracker.getCurrentPositionUs()).isEqualTo(2_000_000L);
.isEqualTo(2_000_000L);
} }
private void writeBytesAndAdvanceTime(AudioTrack audioTrack) { private void writeBytesAndAdvanceTime(AudioTrack audioTrack) {