Add LivePositionSupplier util to SimpleBasePlayer

In some cases the position is supplied from a "live" source
that keeps changing its value at repeated calls. This typically
happens when forwarding to another backend with unpredictable
position values. The problem is that as soon as the backend
system has a position discontinuity, any previously created
PositionSupplier now returns a position that no longer makes
sense in the context of the State object it belongs to.

ForwardingSimpleBasePlayer already works around this issue by
having a util class to disconnect these live sources. This
change moves the same util to SimpleBasePlayer itself so it
can be reused by other implementations.

PiperOrigin-RevId: 713658046
This commit is contained in:
tonihei 2025-01-09 07:02:13 -08:00 committed by Copybara-Service
parent 8a709a7d76
commit 9608ae4e3d
3 changed files with 116 additions and 52 deletions

View File

@ -60,7 +60,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
private final Player player; private final Player player;
private ForwardingPositionSupplier currentPositionSupplier; private LivePositionSuppliers livePositionSuppliers;
private Metadata lastTimedMetadata; private Metadata lastTimedMetadata;
private @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason; private @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason;
private @Player.DiscontinuityReason int pendingDiscontinuityReason; private @Player.DiscontinuityReason int pendingDiscontinuityReason;
@ -78,7 +78,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
this.lastTimedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); this.lastTimedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET);
this.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; this.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
this.pendingDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL; this.pendingDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL;
this.currentPositionSupplier = new ForwardingPositionSupplier(player); this.livePositionSuppliers = new LivePositionSuppliers(player);
player.addListener( player.addListener(
new Listener() { new Listener() {
@Override @Override
@ -99,15 +99,8 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
@Player.DiscontinuityReason int reason) { @Player.DiscontinuityReason int reason) {
pendingDiscontinuityReason = reason; pendingDiscontinuityReason = reason;
pendingPositionDiscontinuityNewPositionMs = newPosition.positionMs; pendingPositionDiscontinuityNewPositionMs = newPosition.positionMs;
// Any previously created State will directly call through to player.getCurrentPosition livePositionSuppliers.disconnect(oldPosition.positionMs, oldPosition.contentPositionMs);
// via the existing position supplier. From this point onwards, this is wrong as the livePositionSuppliers = new LivePositionSuppliers(player);
// player had a discontinuity and will now return a new position unrelated to the old
// State. We can disconnect these old State objects from the underlying Player by fixing
// the position to the one before the discontinuity and using a new (live) position
// supplier for future State objects.
currentPositionSupplier.setConstant(
oldPosition.positionMs, oldPosition.contentPositionMs);
currentPositionSupplier = new ForwardingPositionSupplier(player);
} }
@Override @Override
@ -132,18 +125,18 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
protected State getState() { protected State getState() {
// Ordered alphabetically by State.Builder setters. // Ordered alphabetically by State.Builder setters.
State.Builder state = new State.Builder(); State.Builder state = new State.Builder();
ForwardingPositionSupplier positionSupplier = currentPositionSupplier; LivePositionSuppliers positionSuppliers = livePositionSuppliers;
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
state.setAdBufferedPositionMs(positionSupplier::getBufferedPositionMs); state.setAdBufferedPositionMs(positionSuppliers.bufferedPositionSupplier);
state.setAdPositionMs(positionSupplier::getCurrentPositionMs); state.setAdPositionMs(positionSuppliers.currentPositionSupplier);
} }
if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) { if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) {
state.setAudioAttributes(player.getAudioAttributes()); state.setAudioAttributes(player.getAudioAttributes());
} }
state.setAvailableCommands(player.getAvailableCommands()); state.setAvailableCommands(player.getAvailableCommands());
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
state.setContentBufferedPositionMs(positionSupplier::getContentBufferedPositionMs); state.setContentBufferedPositionMs(positionSuppliers.contentBufferedPositionSupplier);
state.setContentPositionMs(positionSupplier::getContentPositionMs); state.setContentPositionMs(positionSuppliers.contentPositionSupplier);
if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) {
state.setCurrentAd(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup()); state.setCurrentAd(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup());
} }
@ -194,7 +187,7 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
state.setSurfaceSize(player.getSurfaceSize()); state.setSurfaceSize(player.getSurfaceSize());
state.setTimedMetadata(lastTimedMetadata); state.setTimedMetadata(lastTimedMetadata);
if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
state.setTotalBufferedDurationMs(positionSupplier::getTotalBufferedDurationMs); state.setTotalBufferedDurationMs(positionSuppliers.totalBufferedPositionSupplier);
} }
state.setTrackSelectionParameters(player.getTrackSelectionParameters()); state.setTrackSelectionParameters(player.getTrackSelectionParameters());
state.setVideoSize(player.getVideoSize()); state.setVideoSize(player.getVideoSize());
@ -456,44 +449,29 @@ public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
* Forwards to the changing position values of the wrapped player until the forwarding is * Forwards to the changing position values of the wrapped player until the forwarding is
* deactivated with constant values. * deactivated with constant values.
*/ */
private static final class ForwardingPositionSupplier { private static final class LivePositionSuppliers {
private final Player player; public final LivePositionSupplier currentPositionSupplier;
public final LivePositionSupplier bufferedPositionSupplier;
public final LivePositionSupplier contentPositionSupplier;
public final LivePositionSupplier contentBufferedPositionSupplier;
public final LivePositionSupplier totalBufferedPositionSupplier;
private long positionsMs; public LivePositionSuppliers(Player player) {
private long contentPositionMs; currentPositionSupplier = new LivePositionSupplier(player::getCurrentPosition);
bufferedPositionSupplier = new LivePositionSupplier(player::getBufferedPosition);
public ForwardingPositionSupplier(Player player) { contentPositionSupplier = new LivePositionSupplier(player::getContentPosition);
this.player = player; contentBufferedPositionSupplier =
this.positionsMs = C.TIME_UNSET; new LivePositionSupplier(player::getContentBufferedPosition);
this.contentPositionMs = C.TIME_UNSET; totalBufferedPositionSupplier = new LivePositionSupplier(player::getTotalBufferedDuration);
} }
public void setConstant(long positionMs, long contentPositionMs) { public void disconnect(long positionMs, long contentPositionMs) {
this.positionsMs = positionMs; currentPositionSupplier.disconnect(positionMs);
this.contentPositionMs = contentPositionMs; bufferedPositionSupplier.disconnect(positionMs);
} contentPositionSupplier.disconnect(contentPositionMs);
contentBufferedPositionSupplier.disconnect(contentPositionMs);
public long getCurrentPositionMs() { totalBufferedPositionSupplier.disconnect(/* finalValue= */ 0);
return positionsMs == C.TIME_UNSET ? player.getCurrentPosition() : positionsMs;
}
public long getBufferedPositionMs() {
return positionsMs == C.TIME_UNSET ? player.getBufferedPosition() : positionsMs;
}
public long getContentPositionMs() {
return contentPositionMs == C.TIME_UNSET ? player.getContentPosition() : contentPositionMs;
}
public long getContentBufferedPositionMs() {
return contentPositionMs == C.TIME_UNSET
? player.getContentBufferedPosition()
: contentPositionMs;
}
public long getTotalBufferedDurationMs() {
return positionsMs == C.TIME_UNSET ? player.getTotalBufferedDuration() : 0;
} }
} }
} }

View File

@ -2069,7 +2069,21 @@ public abstract class SimpleBasePlayer extends BasePlayer {
} }
} }
/** A supplier for a position. */ /**
* A supplier for a position.
*
* <p>Convenience methods and classes for creating position suppliers:
*
* <ul>
* <li>Use {@link #getConstant} for constant or non-moving positions.
* <li>Use {@link #getExtrapolating} for positions advancing with the system clock from a
* provided start time.
* <li>Use {@link LivePositionSupplier} for positions that can be directly obtained from a live
* system. Note that these suppliers should be {@linkplain LivePositionSupplier#disconnect
* disconnected} from the live source as soon as the position is no longer valid, for
* example after a position discontinuity.
* </ul>
*/
protected interface PositionSupplier { protected interface PositionSupplier {
/** An instance returning a constant position of zero. */ /** An instance returning a constant position of zero. */
@ -2102,6 +2116,48 @@ public abstract class SimpleBasePlayer extends BasePlayer {
long get(); long get();
} }
/**
* A {@link PositionSupplier} connected to a live provider that returns a new value on each
* invocation until it is {@linkplain #disconnect disconnected} from the live source.
*
* <p>The recommended usage of this class is to create a new instance connected to the live source
* and keep returning this instance as long as the position source is still valid. As soon as the
* position source becomes invalid, for example when handling a position discontinuity, call
* {@link #disconnect} with the final position that will be returned for all future invocations.
*/
protected static final class LivePositionSupplier implements PositionSupplier {
private final PositionSupplier livePosition;
private long finalValue;
/**
* Creates the live position supplier.
*
* @param livePosition The function returning the live position.
*/
public LivePositionSupplier(PositionSupplier livePosition) {
this.livePosition = livePosition;
this.finalValue = C.TIME_UNSET;
}
/**
* Disconnects the position supplier from the live source.
*
* <p>All future invocations of {@link #get()} will return the provided final position.
*
* @param finalValue The final position value.
*/
public void disconnect(long finalValue) {
this.finalValue = finalValue;
}
@Override
public long get() {
return finalValue != C.TIME_UNSET ? finalValue : livePosition.get();
}
}
/** /**
* Position difference threshold below which we do not automatically report a position * Position difference threshold below which we do not automatically report a position
* discontinuity, in milliseconds. * discontinuity, in milliseconds.

View File

@ -8698,6 +8698,36 @@ public class SimpleBasePlayerTest {
assertThat(callForwarded.get()).isFalse(); assertThat(callForwarded.get()).isFalse();
} }
@Test
public void livePositionProvider_returnsChangingLivePosition() {
AtomicInteger livePositionMs = new AtomicInteger(/* initialValue= */ 100);
SimpleBasePlayer.LivePositionSupplier livePositionSupplier =
new SimpleBasePlayer.LivePositionSupplier(livePositionMs::get);
long position1Ms = livePositionSupplier.get();
livePositionMs.set(200);
long position2Ms = livePositionSupplier.get();
livePositionMs.set(300);
long position3Ms = livePositionSupplier.get();
assertThat(position1Ms).isEqualTo(100);
assertThat(position2Ms).isEqualTo(200);
assertThat(position3Ms).isEqualTo(300);
}
@Test
public void livePositionProvider_disconnect_returnsFinalPosition() {
AtomicInteger livePositionMs = new AtomicInteger(/* initialValue= */ 100);
SimpleBasePlayer.LivePositionSupplier livePositionSupplier =
new SimpleBasePlayer.LivePositionSupplier(livePositionMs::get);
livePositionSupplier.disconnect(/* finalValue= */ 150);
livePositionMs.set(200);
long positionMs = livePositionSupplier.get();
assertThat(positionMs).isEqualTo(150);
}
private static Object[] getAnyArguments(Method method) { private static Object[] getAnyArguments(Method method) {
Object[] arguments = new Object[method.getParameterCount()]; Object[] arguments = new Object[method.getParameterCount()];
Class<?>[] argumentTypes = method.getParameterTypes(); Class<?>[] argumentTypes = method.getParameterTypes();