Add position interpolation to MediaControllerImplLegacy

Without this, the position won't udpate until the session sends
a new playback state.

PiperOrigin-RevId: 568889286
This commit is contained in:
tonihei 2023-09-27 10:21:04 -07:00 committed by Copybara-Service
parent b4771e07b5
commit 77ba0292ad
6 changed files with 162 additions and 78 deletions

View File

@ -76,6 +76,8 @@
controller is always the media notification controller and apps can controller is always the media notification controller and apps can
easily recognize calls coming from the notification in the same way on easily recognize calls coming from the notification in the same way on
all supported API levels. all supported API levels.
* Fix bug where `MediaController.getCurrentPosition()` is not advancing
when connected to a legacy `MediaSessionCompat`.
* UI: * UI:
* Downloads: * Downloads:
* OkHttp Extension: * OkHttp Extension:

View File

@ -583,7 +583,12 @@ import org.checkerframework.checker.nullness.qual.NonNull;
@Override @Override
public long getCurrentPosition() { public long getCurrentPosition() {
maybeUpdateCurrentPositionMs(); currentPositionMs =
MediaUtils.getUpdatedCurrentPositionMs(
playerInfo,
currentPositionMs,
lastSetPlayWhenReadyCalledTimeMs,
getInstance().getTimeDiffMs());
return currentPositionMs; return currentPositionMs;
} }
@ -2191,7 +2196,12 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
// Update position and then stop estimating until a new positionInfo arrives from the player. // Update position and then stop estimating until a new positionInfo arrives from the player.
maybeUpdateCurrentPositionMs(); currentPositionMs =
MediaUtils.getUpdatedCurrentPositionMs(
this.playerInfo,
currentPositionMs,
lastSetPlayWhenReadyCalledTimeMs,
getInstance().getTimeDiffMs());
lastSetPlayWhenReadyCalledTimeMs = SystemClock.elapsedRealtime(); lastSetPlayWhenReadyCalledTimeMs = SystemClock.elapsedRealtime();
PlayerInfo newPlayerInfo = PlayerInfo newPlayerInfo =
this.playerInfo.copyWithPlayWhenReady( this.playerInfo.copyWithPlayWhenReady(
@ -2991,34 +3001,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
return playerInfo; return playerInfo;
} }
private void maybeUpdateCurrentPositionMs() {
boolean receivedUpdatedPositionInfo =
lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs;
if (!playerInfo.isPlaying) {
if (receivedUpdatedPositionInfo || currentPositionMs == C.TIME_UNSET) {
currentPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs;
}
return;
}
if (!receivedUpdatedPositionInfo && currentPositionMs != C.TIME_UNSET) {
// Need an updated current position in order to make a new position estimation
return;
}
long elapsedTimeMs =
(getInstance().getTimeDiffMs() != C.TIME_UNSET)
? getInstance().getTimeDiffMs()
: SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs;
long estimatedPositionMs =
playerInfo.sessionPositionInfo.positionInfo.positionMs
+ (long) (elapsedTimeMs * playerInfo.playbackParameters.speed);
if (playerInfo.sessionPositionInfo.durationMs != C.TIME_UNSET) {
estimatedPositionMs = min(estimatedPositionMs, playerInfo.sessionPositionInfo.durationMs);
}
currentPositionMs = estimatedPositionMs;
}
private static Period getPeriodWithNewWindowIndex( private static Period getPeriodWithNewWindowIndex(
Timeline timeline, int periodIndex, int windowIndex) { Timeline timeline, int periodIndex, int windowIndex) {
Period period = new Period(); Period period = new Period();

View File

@ -107,6 +107,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
private LegacyPlayerInfo legacyPlayerInfo; private LegacyPlayerInfo legacyPlayerInfo;
private LegacyPlayerInfo pendingLegacyPlayerInfo; private LegacyPlayerInfo pendingLegacyPlayerInfo;
private ControllerInfo controllerInfo; private ControllerInfo controllerInfo;
private long currentPositionMs;
private long lastSetPlayWhenReadyCalledTimeMs;
public MediaControllerImplLegacy( public MediaControllerImplLegacy(
Context context, Context context,
@ -130,6 +132,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerCompatCallback = new ControllerCompatCallback(applicationLooper); controllerCompatCallback = new ControllerCompatCallback(applicationLooper);
this.token = token; this.token = token;
this.bitmapLoader = bitmapLoader; this.bitmapLoader = bitmapLoader;
currentPositionMs = C.TIME_UNSET;
lastSetPlayWhenReadyCalledTimeMs = C.TIME_UNSET;
} }
/* package */ MediaController getInstance() { /* package */ MediaController getInstance() {
@ -227,50 +231,12 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override @Override
public void play() { public void play() {
if (controllerInfo.playerInfo.playWhenReady) { setPlayWhenReady(true);
return;
}
ControllerInfo maskedControllerInfo =
new ControllerInfo(
controllerInfo.playerInfo.copyWithPlayWhenReady(
/* playWhenReady= */ true,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE),
controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands,
controllerInfo.customLayout);
updateStateMaskedControllerInfo(
maskedControllerInfo,
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
if (isPrepared() && hasMedia()) {
controllerCompat.getTransportControls().play();
}
} }
@Override @Override
public void pause() { public void pause() {
if (!controllerInfo.playerInfo.playWhenReady) { setPlayWhenReady(false);
return;
}
ControllerInfo maskedControllerInfo =
new ControllerInfo(
controllerInfo.playerInfo.copyWithPlayWhenReady(
/* playWhenReady= */ false,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE),
controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands,
controllerInfo.customLayout);
updateStateMaskedControllerInfo(
maskedControllerInfo,
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
if (isPrepared() && hasMedia()) {
controllerCompat.getTransportControls().pause();
}
} }
@Override @Override
@ -459,7 +425,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override @Override
public long getCurrentPosition() { public long getCurrentPosition() {
return controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs; currentPositionMs =
MediaUtils.getUpdatedCurrentPositionMs(
controllerInfo.playerInfo,
currentPositionMs,
lastSetPlayWhenReadyCalledTimeMs,
getInstance().getTimeDiffMs());
return currentPositionMs;
} }
@Override @Override
@ -758,6 +730,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
if (newCurrentMediaItemIndex == C.INDEX_UNSET) { if (newCurrentMediaItemIndex == C.INDEX_UNSET) {
newCurrentMediaItemIndex = newCurrentMediaItemIndex =
Util.constrainValue(fromIndex, /* min= */ 0, newQueueTimeline.getWindowCount() - 1); Util.constrainValue(fromIndex, /* min= */ 0, newQueueTimeline.getWindowCount() - 1);
// TODO: b/302114474 - This also needs to reset the current position.
Log.w( Log.w(
TAG, TAG,
"Currently playing item is removed. Assumes item at " "Currently playing item is removed. Assumes item at "
@ -1213,10 +1186,37 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override @Override
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
if (playWhenReady) { if (controllerInfo.playerInfo.playWhenReady == playWhenReady) {
play(); return;
} else { }
pause(); // Update position and then stop estimating until a new positionInfo arrives from the session.
currentPositionMs =
MediaUtils.getUpdatedCurrentPositionMs(
controllerInfo.playerInfo,
currentPositionMs,
lastSetPlayWhenReadyCalledTimeMs,
getInstance().getTimeDiffMs());
lastSetPlayWhenReadyCalledTimeMs = SystemClock.elapsedRealtime();
ControllerInfo maskedControllerInfo =
new ControllerInfo(
controllerInfo.playerInfo.copyWithPlayWhenReady(
playWhenReady,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE),
controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands,
controllerInfo.customLayout);
updateStateMaskedControllerInfo(
maskedControllerInfo,
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
if (isPrepared() && hasMedia()) {
if (playWhenReady) {
controllerCompat.getTransportControls().play();
} else {
controllerCompat.getTransportControls().pause();
}
} }
} }
@ -2233,7 +2233,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
new SessionPositionInfo( new SessionPositionInfo(
/* positionInfo= */ positionInfo, /* positionInfo= */ positionInfo,
/* isPlayingAd= */ isPlayingAd, /* isPlayingAd= */ isPlayingAd,
/* eventTimeMs= */ C.TIME_UNSET, /* eventTimeMs= */ SystemClock.elapsedRealtime(),
/* durationMs= */ durationMs, /* durationMs= */ durationMs,
/* bufferedPositionMs= */ bufferedPositionMs, /* bufferedPositionMs= */ bufferedPositionMs,
/* bufferedPercentage= */ bufferedPercentage, /* bufferedPercentage= */ bufferedPercentage,

View File

@ -48,6 +48,7 @@ import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.constrainValue; import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -1500,6 +1501,52 @@ import java.util.concurrent.TimeoutException;
&& info1.positionInfo.adIndexInAdGroup == info2.positionInfo.adIndexInAdGroup; && info1.positionInfo.adIndexInAdGroup == info2.positionInfo.adIndexInAdGroup;
} }
/**
* Returns updated value for a media controller position estimate.
*
* @param playerInfo The current {@link PlayerInfo}.
* @param currentPositionMs The current known position estimate in milliseconds, or {@link
* C#TIME_UNSET} if still unknown.
* @param lastSetPlayWhenReadyCalledTimeMs The {@link SystemClock#elapsedRealtime()} when the
* controller was last used to call {@link MediaController#setPlayWhenReady}, or {@link
* C#TIME_UNSET} if it was never called.
* @param timeDiffMs A time difference override since the last {@link PlayerInfo} update. Should
* be {@link C#TIME_UNSET} except for testing.
* @return The updated position estimate in milliseconds.
*/
public static long getUpdatedCurrentPositionMs(
PlayerInfo playerInfo,
long currentPositionMs,
long lastSetPlayWhenReadyCalledTimeMs,
long timeDiffMs) {
boolean receivedUpdatedPositionInfo =
lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs;
if (!playerInfo.isPlaying) {
if (receivedUpdatedPositionInfo || currentPositionMs == C.TIME_UNSET) {
return playerInfo.sessionPositionInfo.positionInfo.positionMs;
} else {
return currentPositionMs;
}
}
if (!receivedUpdatedPositionInfo && currentPositionMs != C.TIME_UNSET) {
// Need an updated current position in order to make a new position estimation
return currentPositionMs;
}
long elapsedTimeMs =
timeDiffMs != C.TIME_UNSET
? timeDiffMs
: SystemClock.elapsedRealtime() - playerInfo.sessionPositionInfo.eventTimeMs;
long estimatedPositionMs =
playerInfo.sessionPositionInfo.positionInfo.positionMs
+ (long) (elapsedTimeMs * playerInfo.playbackParameters.speed);
if (playerInfo.sessionPositionInfo.durationMs != C.TIME_UNSET) {
estimatedPositionMs = min(estimatedPositionMs, playerInfo.sessionPositionInfo.durationMs);
}
return estimatedPositionMs;
}
private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { private static byte[] convertToByteArray(Bitmap bitmap) throws IOException {
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);

View File

@ -23,7 +23,6 @@ import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -630,7 +629,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
sessionPositionInfo.positionInfo.adGroupIndex, sessionPositionInfo.positionInfo.adGroupIndex,
sessionPositionInfo.positionInfo.adIndexInAdGroup), sessionPositionInfo.positionInfo.adIndexInAdGroup),
sessionPositionInfo.isPlayingAd, sessionPositionInfo.isPlayingAd,
/* eventTimeMs= */ SystemClock.elapsedRealtime(), sessionPositionInfo.eventTimeMs,
sessionPositionInfo.durationMs, sessionPositionInfo.durationMs,
sessionPositionInfo.bufferedPositionMs, sessionPositionInfo.bufferedPositionMs,
sessionPositionInfo.bufferedPercentage, sessionPositionInfo.bufferedPercentage,

View File

@ -48,6 +48,7 @@ import android.graphics.Bitmap;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
@ -1833,6 +1834,59 @@ public class MediaControllerWithMediaSessionCompatTest {
assertThat(currentPositionMs).isEqualTo(testDurationMs); assertThat(currentPositionMs).isEqualTo(testDurationMs);
} }
@Test
public void getCurrentPosition_withDelayWhileNotPlaying_doesNotAdvance() throws Exception {
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(
PlaybackStateCompat.STATE_PAUSED, /* position= */ 500, /* playbackSpeed= */ 2.0f)
.build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
long currentPositionMs =
threadTestRule
.getHandler()
.postAndSync(
() -> {
Thread.sleep(100);
return controller.getCurrentPosition();
});
assertThat(currentPositionMs).isEqualTo(500);
}
@Test
public void getCurrentPosition_withTimeDiffWhilePlaying_advancesWithTimeDiff() throws Exception {
long timeBeforeSetPlaybackState = SystemClock.elapsedRealtime();
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(
PlaybackStateCompat.STATE_PLAYING, /* position= */ 500, /* playbackSpeed= */ 2.0f)
.build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
long timeAfterControllerCreated = SystemClock.elapsedRealtime();
AtomicLong timeBeforeGetCurrentPosition = new AtomicLong();
AtomicLong timeAfterGetCurrentPosition = new AtomicLong();
AtomicLong currentPositionMs = new AtomicLong();
threadTestRule
.getHandler()
.postAndSync(
() -> {
Thread.sleep(100);
timeBeforeGetCurrentPosition.set(SystemClock.elapsedRealtime());
currentPositionMs.set(controller.getCurrentPosition());
timeAfterGetCurrentPosition.set(SystemClock.elapsedRealtime());
});
long minTimeElapsedMs = timeBeforeGetCurrentPosition.get() - timeAfterControllerCreated;
long maxTimeElapsedMs = timeAfterGetCurrentPosition.get() - timeBeforeSetPlaybackState;
long minExpectedPositionMs = 500 + minTimeElapsedMs * 2;
long maxExpectedPositionMs = 500 + maxTimeElapsedMs * 2;
assertThat(currentPositionMs.get())
.isIn(Range.closed(minExpectedPositionMs, maxExpectedPositionMs));
}
@Test @Test
public void getContentPosition_byDefault_returnsZero() throws Exception { public void getContentPosition_byDefault_returnsZero() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken()); MediaController controller = controllerTestRule.createController(session.getSessionToken());