mirror of
https://github.com/androidx/media.git
synced 2025-05-07 23:50:44 +08:00
Cache rawPlaybackHeadPosition across reset due to track transition
Upon track transition of offloaded playback of gapless tracks, the framework will reset the playback head position. The AudioTrackPositionTracker must be made to expect the reset and cache accumulated sum of rawPlaybackHeadPosition. #minor-release PiperOrigin-RevId: 545602979
This commit is contained in:
parent
aa57d48347
commit
5737e415b8
@ -26,7 +26,9 @@ import android.media.AudioTrack;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -201,6 +203,19 @@ import java.lang.reflect.Method;
|
||||
private long previousModePositionUs;
|
||||
private long previousModeSystemTimeUs;
|
||||
|
||||
/**
|
||||
* Whether to expect a raw playback head reset.
|
||||
*
|
||||
* <p>When an {@link AudioTrack} is reused during offloaded playback, rawPlaybackHeadPosition is
|
||||
* reset upon track transition. {@link AudioTrackPositionTracker} must be notified of the
|
||||
* impending reset and keep track of total accumulated rawPlaybackHeadPosition.
|
||||
*/
|
||||
private boolean expectRawPlaybackHeadReset;
|
||||
|
||||
private long sumRawPlaybackHeadPosition;
|
||||
|
||||
private Clock clock = Clock.DEFAULT;
|
||||
|
||||
/**
|
||||
* Creates a new audio track position tracker.
|
||||
*
|
||||
@ -245,6 +260,8 @@ import java.lang.reflect.Method;
|
||||
bufferSizeUs = isOutputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
|
||||
rawPlaybackHeadPosition = 0;
|
||||
rawPlaybackHeadWrapCount = 0;
|
||||
expectRawPlaybackHeadReset = false;
|
||||
sumRawPlaybackHeadPosition = 0;
|
||||
passthroughWorkaroundPauseOffset = 0;
|
||||
hasData = false;
|
||||
stopTimestampUs = C.TIME_UNSET;
|
||||
@ -271,7 +288,7 @@ import java.lang.reflect.Method;
|
||||
|
||||
// If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
|
||||
// Otherwise, derive a smoothed position by sampling the track's frame position.
|
||||
long systemTimeUs = System.nanoTime() / 1000;
|
||||
long systemTimeUs = clock.nanoTime() / 1000;
|
||||
long positionUs;
|
||||
AudioTimestampPoller audioTimestampPoller = checkNotNull(this.audioTimestampPoller);
|
||||
boolean useGetTimestampMode = audioTimestampPoller.hasAdvancingTimestamp();
|
||||
@ -445,6 +462,14 @@ import java.lang.reflect.Method;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the position tracker to expect a reset in raw playback head position due to reusing an
|
||||
* {@link AudioTrack} and an impending track transition.
|
||||
*/
|
||||
public void expectRawPlaybackHeadReset() {
|
||||
expectRawPlaybackHeadReset = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the position tracker. Should be called when the audio track previously passed to {@link
|
||||
* #setAudioTrack(AudioTrack, boolean, int, int, int)} is no longer in use.
|
||||
@ -455,8 +480,18 @@ import java.lang.reflect.Method;
|
||||
audioTimestampPoller = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clock used for {@code nanoTime()} requests.
|
||||
*
|
||||
* @param clock The clock to be used for {@code nanoTime()} requests.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
/* package */ void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
private void maybeSampleSyncParams() {
|
||||
long systemTimeUs = System.nanoTime() / 1000;
|
||||
long systemTimeUs = clock.nanoTime() / 1000;
|
||||
if (systemTimeUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
|
||||
long playbackPositionUs = getPlaybackHeadPositionUs();
|
||||
if (playbackPositionUs == 0) {
|
||||
@ -608,7 +643,7 @@ import java.lang.reflect.Method;
|
||||
updateRawPlaybackHeadPosition(currentTimeMs);
|
||||
lastRawPlaybackHeadPositionSampleTimeMs = currentTimeMs;
|
||||
}
|
||||
return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
|
||||
return rawPlaybackHeadPosition + sumRawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
|
||||
}
|
||||
|
||||
private void updateRawPlaybackHeadPosition(long currentTimeMs) {
|
||||
@ -648,8 +683,13 @@ import java.lang.reflect.Method;
|
||||
}
|
||||
|
||||
if (this.rawPlaybackHeadPosition > rawPlaybackHeadPosition) {
|
||||
// The value must have wrapped around.
|
||||
rawPlaybackHeadWrapCount++;
|
||||
if (expectRawPlaybackHeadReset) {
|
||||
sumRawPlaybackHeadPosition += this.rawPlaybackHeadPosition;
|
||||
expectRawPlaybackHeadReset = false;
|
||||
} else {
|
||||
// The value must have wrapped around.
|
||||
rawPlaybackHeadWrapCount++;
|
||||
}
|
||||
}
|
||||
this.rawPlaybackHeadPosition = rawPlaybackHeadPosition;
|
||||
}
|
||||
|
@ -860,6 +860,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
// not have started yet. Do not call setOffloadEndOfStream as it would throw.
|
||||
if (audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
|
||||
audioTrack.setOffloadEndOfStream();
|
||||
audioTrackPositionTracker.expectRawPlaybackHeadReset();
|
||||
}
|
||||
audioTrack.setOffloadDelayPadding(
|
||||
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
|
||||
|
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 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 com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioTrack;
|
||||
import androidx.media3.common.AudioAttributes;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.test.utils.FakeClock;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link AudioTrackPositionTracker}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AudioTrackPositionTrackerTest {
|
||||
private static final android.media.AudioAttributes AUDIO_ATTRIBUTES =
|
||||
AudioAttributes.DEFAULT.getAudioAttributesV21().audioAttributes;
|
||||
private static final int BYTES_PER_FRAME_16_BIT = 2;
|
||||
private static final int CHANNEL_COUNT_STEREO = 2;
|
||||
private static final int OUTPUT_PCM_FRAME_SIZE =
|
||||
Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, CHANNEL_COUNT_STEREO);
|
||||
private static final int SAMPLE_RATE = 44100;
|
||||
private static final long START_TIME_MS = 9999L;
|
||||
private static final long TIME_TO_ADVANCE_MS = 1000L;
|
||||
private static final int MIN_BUFFER_SIZE =
|
||||
AudioTrack.getMinBufferSize(
|
||||
SAMPLE_RATE, Util.getAudioTrackChannelConfig(CHANNEL_COUNT_STEREO), C.ENCODING_PCM_16BIT);
|
||||
private static final AudioFormat AUDIO_FORMAT =
|
||||
Util.getAudioFormat(
|
||||
SAMPLE_RATE, Util.getAudioTrackChannelConfig(CHANNEL_COUNT_STEREO), C.ENCODING_PCM_16BIT);
|
||||
|
||||
private final AudioTrackPositionTracker audioTrackPositionTracker =
|
||||
new AudioTrackPositionTracker(mock(AudioTrackPositionTracker.Listener.class));
|
||||
private final AudioTrack audioTrack = createDefaultAudioTrack();
|
||||
private final FakeClock clock =
|
||||
new FakeClock(/* initialTimeMs= */ START_TIME_MS, /* isAutoAdvancing= */ true);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
audioTrackPositionTracker.setClock(clock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
getCurrentPositionUs_withoutExpectRawPlaybackHeadReset_returnsPositionWithWrappedValue() {
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack,
|
||||
/* isPassthrough= */ false,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
OUTPUT_PCM_FRAME_SIZE,
|
||||
MIN_BUFFER_SIZE);
|
||||
audioTrackPositionTracker.start();
|
||||
audioTrack.play();
|
||||
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false);
|
||||
}
|
||||
|
||||
// Reset audio track and write bytes to simulate position overflow.
|
||||
audioTrack.flush();
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isGreaterThan(4294967296L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCurrentPositionUs_withExpectRawPlaybackHeadReset_returnsAccumulatedPosition() {
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack,
|
||||
/* isPassthrough= */ false,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
OUTPUT_PCM_FRAME_SIZE,
|
||||
MIN_BUFFER_SIZE);
|
||||
audioTrackPositionTracker.start();
|
||||
audioTrack.play();
|
||||
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false);
|
||||
}
|
||||
|
||||
// Reset audio track to simulate track reuse and transition.
|
||||
// Set tracker to expect playback head reset.
|
||||
audioTrack.flush();
|
||||
audioTrackPositionTracker.expectRawPlaybackHeadReset();
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
|
||||
// Expected position is msToUs(# of writes)*TIME_TO_ADVANCE_MS.
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isEqualTo(3000000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCurrentPositionUs_withExpectPositionResetThenPause_returnsCorrectPosition() {
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack,
|
||||
/* isPassthrough= */ false,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
OUTPUT_PCM_FRAME_SIZE,
|
||||
MIN_BUFFER_SIZE);
|
||||
audioTrackPositionTracker.start();
|
||||
audioTrack.play();
|
||||
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false);
|
||||
}
|
||||
// Reset audio track to simulate track transition and set tracker to expect playback head reset.
|
||||
audioTrack.flush();
|
||||
audioTrackPositionTracker.expectRawPlaybackHeadReset();
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isEqualTo(3000000L);
|
||||
|
||||
// Pause tracker, pause audio track, and advance time to test that position does not change
|
||||
// during pause
|
||||
audioTrackPositionTracker.pause();
|
||||
audioTrack.pause();
|
||||
clock.advanceTime(TIME_TO_ADVANCE_MS);
|
||||
|
||||
// Expected position is msToUs(# of writes)*TIME_TO_ADVANCE_MS.
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isEqualTo(3000000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCurrentPositionUs_withSetAudioTrackResettingSumPosition_returnsCorrectPosition() {
|
||||
AudioTrack audioTrack1 = createDefaultAudioTrack();
|
||||
AudioTrack audioTrack2 = createDefaultAudioTrack();
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack1,
|
||||
/* isPassthrough= */ false,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
OUTPUT_PCM_FRAME_SIZE,
|
||||
MIN_BUFFER_SIZE);
|
||||
audioTrackPositionTracker.start();
|
||||
audioTrack1.play();
|
||||
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
advanceTimeAndWriteBytes(audioTrack1);
|
||||
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false);
|
||||
}
|
||||
// Reset audio track and set tracker to expect playback head reset to simulate track transition.
|
||||
audioTrack1.flush();
|
||||
audioTrackPositionTracker.expectRawPlaybackHeadReset();
|
||||
advanceTimeAndWriteBytes(audioTrack1);
|
||||
// Test for correct setup with current position being accumulated position.
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isEqualTo(3000000L);
|
||||
|
||||
// Set new audio track and reset position tracker to simulate transition to new AudioTrack.
|
||||
audioTrackPositionTracker.reset();
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack2,
|
||||
/* isPassthrough= */ false,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
OUTPUT_PCM_FRAME_SIZE,
|
||||
MIN_BUFFER_SIZE);
|
||||
audioTrack2.play();
|
||||
advanceTimeAndWriteBytes(audioTrack2);
|
||||
|
||||
// Expected position is msToUs(1 write)*TIME_TO_ADVANCE_MS.
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isEqualTo(1000000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCurrentPositionUs_withSetAudioTrackResetsExpectPosition_returnsWrappedValue() {
|
||||
// Set tracker to expect playback head reset to simulate expected track transition.
|
||||
audioTrackPositionTracker.expectRawPlaybackHeadReset();
|
||||
audioTrackPositionTracker.setAudioTrack(
|
||||
audioTrack,
|
||||
/* isPassthrough= */ false,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
OUTPUT_PCM_FRAME_SIZE,
|
||||
MIN_BUFFER_SIZE);
|
||||
audioTrack.play();
|
||||
|
||||
// Advance and write to audio track at least twice to move rawHeadPosition past wrap point.
|
||||
for (int i = 0; i < 2; i++) {
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false);
|
||||
}
|
||||
// Reset audio track and write bytes to simulate position overflow.
|
||||
audioTrack.flush();
|
||||
advanceTimeAndWriteBytes(audioTrack);
|
||||
|
||||
assertThat(audioTrackPositionTracker.getCurrentPositionUs(/* sourceEnded= */ false))
|
||||
.isGreaterThan(4294967296L);
|
||||
}
|
||||
|
||||
private void advanceTimeAndWriteBytes(AudioTrack audioTrack) {
|
||||
clock.advanceTime(TIME_TO_ADVANCE_MS);
|
||||
ByteBuffer byteBuffer = createDefaultSilenceBuffer();
|
||||
int bytesRemaining = byteBuffer.remaining();
|
||||
audioTrack.write(byteBuffer, bytesRemaining, AudioTrack.WRITE_NON_BLOCKING);
|
||||
}
|
||||
|
||||
/** Creates a one second silence buffer for 44.1 kHz stereo 16-bit audio. */
|
||||
private static ByteBuffer createDefaultSilenceBuffer() {
|
||||
return ByteBuffer.allocateDirect(SAMPLE_RATE * CHANNEL_COUNT_STEREO * BYTES_PER_FRAME_16_BIT)
|
||||
.order(ByteOrder.nativeOrder());
|
||||
}
|
||||
|
||||
/** Creates a basic {@link AudioTrack} with 16 bit PCM encoding type. */
|
||||
private static AudioTrack createDefaultAudioTrack() {
|
||||
return new AudioTrack.Builder()
|
||||
.setAudioAttributes(AUDIO_ATTRIBUTES)
|
||||
.setAudioFormat(AUDIO_FORMAT)
|
||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||
.setBufferSizeInBytes(MIN_BUFFER_SIZE)
|
||||
.build();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user