Skip to content

Commit

Permalink
Limit target buffer to media configured min/max values.
Browse files Browse the repository at this point in the history
Issue: #4904
PiperOrigin-RevId: 340653126
  • Loading branch information
tonihei authored and andrewlewis committed Nov 6, 2020
1 parent c9e80a2 commit 2416d99
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public Builder setFallbackMaxPlaybackSpeed(float fallbackMaxPlaybackSpeed) {
* @return This builder, for convenience.
*/
public Builder setMinUpdateIntervalMs(long minUpdateIntervalMs) {
Assertions.checkArgument(minUpdateIntervalMs >= 0);
Assertions.checkArgument(minUpdateIntervalMs > 0);
this.minUpdateIntervalMs = minUpdateIntervalMs;
return this;
}
Expand Down Expand Up @@ -160,8 +160,14 @@ public DefaultLivePlaybackSpeedControl build() {
private final long minUpdateIntervalMs;
private final float proportionalControlFactor;

private LiveConfiguration mediaConfiguration;
private long mediaConfigurationTargetLiveOffsetUs;
private long targetLiveOffsetOverrideUs;
private long minTargetLiveOffsetUs;
private long maxTargetLiveOffsetUs;
private long currentTargetLiveOffsetUs;

private float maxPlaybackSpeed;
private float minPlaybackSpeed;
private float adjustedPlaybackSpeed;
private long lastPlaybackSpeedUpdateMs;

Expand All @@ -174,28 +180,42 @@ private DefaultLivePlaybackSpeedControl(
this.fallbackMaxPlaybackSpeed = fallbackMaxPlaybackSpeed;
this.minUpdateIntervalMs = minUpdateIntervalMs;
this.proportionalControlFactor = proportionalControlFactor;
mediaConfiguration = LiveConfiguration.UNSET;
mediaConfigurationTargetLiveOffsetUs = C.TIME_UNSET;
targetLiveOffsetOverrideUs = C.TIME_UNSET;
minTargetLiveOffsetUs = C.TIME_UNSET;
maxTargetLiveOffsetUs = C.TIME_UNSET;
minPlaybackSpeed = fallbackMinPlaybackSpeed;
maxPlaybackSpeed = fallbackMaxPlaybackSpeed;
adjustedPlaybackSpeed = 1.0f;
lastPlaybackSpeedUpdateMs = C.TIME_UNSET;
currentTargetLiveOffsetUs = C.TIME_UNSET;
}

@Override
public void setLiveConfiguration(LiveConfiguration liveConfiguration) {
this.mediaConfiguration = liveConfiguration;
lastPlaybackSpeedUpdateMs = C.TIME_UNSET;
mediaConfigurationTargetLiveOffsetUs = C.msToUs(liveConfiguration.targetLiveOffsetMs);
minTargetLiveOffsetUs = C.msToUs(liveConfiguration.minLiveOffsetMs);
maxTargetLiveOffsetUs = C.msToUs(liveConfiguration.maxLiveOffsetMs);
minPlaybackSpeed =
liveConfiguration.minPlaybackSpeed != C.RATE_UNSET
? liveConfiguration.minPlaybackSpeed
: fallbackMinPlaybackSpeed;
maxPlaybackSpeed =
liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET
? liveConfiguration.maxPlaybackSpeed
: fallbackMaxPlaybackSpeed;
maybeResetTargetLiveOffsetUs();
}

@Override
public void setTargetLiveOffsetOverrideUs(long liveOffsetUs) {
this.targetLiveOffsetOverrideUs = liveOffsetUs;
lastPlaybackSpeedUpdateMs = C.TIME_UNSET;
targetLiveOffsetOverrideUs = liveOffsetUs;
maybeResetTargetLiveOffsetUs();
}

@Override
public float getAdjustedPlaybackSpeed(long liveOffsetUs) {
long targetLiveOffsetUs = getTargetLiveOffsetUs();
if (targetLiveOffsetUs == C.TIME_UNSET) {
if (mediaConfigurationTargetLiveOffsetUs == C.TIME_UNSET) {
return 1f;
}
if (lastPlaybackSpeedUpdateMs != C.TIME_UNSET
Expand All @@ -204,34 +224,40 @@ public float getAdjustedPlaybackSpeed(long liveOffsetUs) {
}
lastPlaybackSpeedUpdateMs = SystemClock.elapsedRealtime();

long liveOffsetErrorUs = liveOffsetUs - targetLiveOffsetUs;
long liveOffsetErrorUs = liveOffsetUs - currentTargetLiveOffsetUs;
if (Math.abs(liveOffsetErrorUs) < MAXIMUM_LIVE_OFFSET_ERROR_US_FOR_UNIT_SPEED) {
adjustedPlaybackSpeed = 1f;
} else {
float calculatedSpeed = 1f + proportionalControlFactor * liveOffsetErrorUs;
adjustedPlaybackSpeed =
Util.constrainValue(calculatedSpeed, getMinPlaybackSpeed(), getMaxPlaybackSpeed());
Util.constrainValue(calculatedSpeed, minPlaybackSpeed, maxPlaybackSpeed);
}
return adjustedPlaybackSpeed;
}

@Override
public long getTargetLiveOffsetUs() {
return targetLiveOffsetOverrideUs != C.TIME_UNSET
&& mediaConfiguration.targetLiveOffsetMs != C.TIME_UNSET
? targetLiveOffsetOverrideUs
: C.msToUs(mediaConfiguration.targetLiveOffsetMs);
}

private float getMinPlaybackSpeed() {
return mediaConfiguration.minPlaybackSpeed != C.RATE_UNSET
? mediaConfiguration.minPlaybackSpeed
: fallbackMinPlaybackSpeed;
return currentTargetLiveOffsetUs;
}

private float getMaxPlaybackSpeed() {
return mediaConfiguration.maxPlaybackSpeed != C.RATE_UNSET
? mediaConfiguration.maxPlaybackSpeed
: fallbackMaxPlaybackSpeed;
private void maybeResetTargetLiveOffsetUs() {
long idealOffsetUs = C.TIME_UNSET;
if (mediaConfigurationTargetLiveOffsetUs != C.TIME_UNSET) {
idealOffsetUs =
targetLiveOffsetOverrideUs != C.TIME_UNSET
? targetLiveOffsetOverrideUs
: mediaConfigurationTargetLiveOffsetUs;
if (minTargetLiveOffsetUs != C.TIME_UNSET && idealOffsetUs < minTargetLiveOffsetUs) {
idealOffsetUs = minTargetLiveOffsetUs;
}
if (maxTargetLiveOffsetUs != C.TIME_UNSET && idealOffsetUs > maxTargetLiveOffsetUs) {
idealOffsetUs = maxTargetLiveOffsetUs;
}

This comment has been minimized.

Copy link
@stevemayhew

stevemayhew Nov 17, 2023

Contributor

@tonihei I almost entered a bug on this, till I saw a test case explicitly tests for maxTargetLiveOffsetUs has precedence over targetLiveOffsetOverrideUs. For our uses cases we completely disable live offset adjustment when the targetLiveOffsetOverrideUs is != C.TIME_UNSET, our rationale is when the user seeks back from the live position for some reason they want to playback from that position independent of it's distance from realtime live.

With the code as it is, if the user seeks back to a value less than maxTargetLiveOffsetUs then live adjustment will force playback back to the maxTargetLiveOffsetUs value, possibly aggressively depending on the min/max speed settings. I'm struggling to see the use case for this?

This comment has been minimized.

Copy link
@stevemayhew

stevemayhew Nov 17, 2023

Contributor

Actually, in the standard seems pretty vague on how this is to be handled (https://dashif.org/docs/CR-Low-Latency-Live-r8.pdf), it says:

Indicates a content provider’s desire for the content not to be presented if the latency exceeds the maximum latency.

The DASH-IF reference player uses it as a guideline for when to stop adjustment (a use case that makes far more sense to me), https://github.com/Dash-Industry-Forum/dash.js/blob/0982920ec72e061b88a09b80762488b2c2c7845c/src/dash/controllers/ServiceDescriptionController.js#L174

Also, see the bug for Dash-Industry-Forum/dash.js#4183

Happy to open this discussion as a bug if that is better, but for our use case targetLiveOffsetOverrideUs is broken for seeks outside of the Latency@min/max range. Far better, for us, would be to simply disable live adjustment completely when a targetLiveOffsetOverrideUs is outside the range of min/max

This comment has been minimized.

Copy link
@stevemayhew

stevemayhew Nov 17, 2023

Contributor

Little more research into reference player, they do not consider Latency@min/max in the override at all. https://github.com/Dash-Industry-Forum/dash.js/blob/cb75b77052c3470fa54bdd5a395d219862365e46/src/streaming/controllers/PlaybackController.js#L278

This comment has been minimized.

Copy link
@tonihei

tonihei Nov 20, 2023

Author Collaborator

dash.js actually uses the derived value of maxDrift to decide to auto-seek to live instead of using speed adjustment: https://github.com/Dash-Industry-Forum/dash.js/blob/cb75b77052c3470fa54bdd5a395d219862365e46/src/streaming/controllers/CatchupController.js#L213C14-L213C14

We haven't enabled such a feature because auto-seeking it not what all apps want by default (although we could provide an option for this of course).

With the code as it is, if the user seeks back to a value less than maxTargetLiveOffsetUs then live adjustment will force playback back to the maxTargetLiveOffsetUs value

That sounds wrong indeed. Not sure if there was a reason to make it work like this other than trying to strictly follow the limits defined in the manifest. Will make a change to fix this. Thanks for reporting!

This comment has been minimized.

Copy link
@stevemayhew

stevemayhew Nov 20, 2023

Contributor

I'll enter a bug and steps to reproduce using the demo player in androidx/media so we can track it there.

Auto-seek is pretty violent way to do this, we have found most live streams support AAC along with AC-3 so simply forcing track selection to AAC and using aggressive speed adjustment to quickly bring live into sync works very well. Might be a good RFE for constraint track selection to prefer audio that supports speed adjustment (AAC and MP3).

This comment has been minimized.

Copy link
@tonihei

tonihei Nov 21, 2023

Author Collaborator

I'll enter a bug and steps to reproduce using the demo player in androidx/media so we can track it there.

No need, we already submitted the change for this, see af0282b.

}
if (currentTargetLiveOffsetUs == idealOffsetUs) {
return;
}
currentTargetLiveOffsetUs = idealOffsetUs;
lastPlaybackSpeedUpdateMs = C.TIME_UNSET;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ public void getTargetLiveOffsetUs_returnsUnset() {
}

@Test
public void getTargetLiveOffsetUs_afterUpdateLiveConfiguration_usesMediaLiveOffset() {
public void getTargetLiveOffsetUs_afterSetLiveConfiguration_usesMediaLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 42,
/* minLiveOffsetMs= */ 200,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));
Expand All @@ -52,27 +52,99 @@ public void getTargetLiveOffsetUs_afterUpdateLiveConfiguration_usesMediaLiveOffs
}

@Test
public void getTargetLiveOffsetUs_withOverrideTargetLiveOffsetUs_usesOverride() {
public void
getTargetLiveOffsetUs_afterSetLiveConfigurationWithTargetGreaterThanMax_usesMaxLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 4321,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));

assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(400_000);
}

@Test
public void
getTargetLiveOffsetUs_afterSetLiveConfigurationWithTargetLessThanMin_usesMinLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 3,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));

assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(5_000);
}

@Test
public void getTargetLiveOffsetUs_withSetTargetLiveOffsetOverrideUs_usesOverride() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();

defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(321_000);
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 42,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));

long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs();

assertThat(targetLiveOffsetUs).isEqualTo(321_000);
}

@Test
public void
getTargetLiveOffsetUs_withSetTargetLiveOffsetOverrideUsGreaterThanMax_usesMaxLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();

defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789);
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 42,
/* minLiveOffsetMs= */ 200,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));

long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs();

assertThat(targetLiveOffsetUs).isEqualTo(123_456_789);
assertThat(targetLiveOffsetUs).isEqualTo(400_000);
}

@Test
public void
getTargetLiveOffsetUs_afterOverrideTargetLiveOffset_withoutMediaConfiguration_returnsUnset() {
getTargetLiveOffsetUs_withSetTargetLiveOffsetOverrideUsLessThanMin_usesMinLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();

defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(3_141);
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 42,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));

long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs();

assertThat(targetLiveOffsetUs).isEqualTo(5_000);
}

@Test
public void
getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideWithoutMediaConfiguration_returnsUnset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789);
Expand All @@ -84,14 +156,14 @@ public void getTargetLiveOffsetUs_withOverrideTargetLiveOffsetUs_usesOverride()

@Test
public void
getTargetLiveOffsetUs_afterOverrideTargetLiveOffsetUsWithTimeUnset_usesMediaLiveOffset() {
getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideWithTimeUnset_usesMediaLiveOffset() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().build();
defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(123_456_789);
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 42,
/* minLiveOffsetMs= */ 200,
/* minLiveOffsetMs= */ 5,
/* maxLiveOffsetMs= */ 400,
/* minPlaybackSpeed= */ 1f,
/* maxPlaybackSpeed= */ 1f));
Expand Down Expand Up @@ -294,7 +366,8 @@ public void adjustPlaybackSpeed_repeatedCallWithinMinUpdateInterval_returnsSameA
}

@Test
public void adjustPlaybackSpeed_repeatedCallAfterUpdateLiveConfiguration_updatesSpeedAgain() {
public void
adjustPlaybackSpeed_repeatedCallAfterUpdateLiveConfigurationWithSameOffset_returnsSameAdjustedSpeed() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
Expand All @@ -317,6 +390,34 @@ public void adjustPlaybackSpeed_repeatedCallAfterUpdateLiveConfiguration_updates
float adjustedSpeed2 =
defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed(/* liveOffsetUs= */ 2_500_000);

assertThat(adjustedSpeed1).isEqualTo(adjustedSpeed2);
}

@Test
public void
adjustPlaybackSpeed_repeatedCallAfterUpdateLiveConfigurationWithNewOffset_updatesSpeedAgain() {
DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl =
new DefaultLivePlaybackSpeedControl.Builder().setMinUpdateIntervalMs(123).build();
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 2_000,
/* minLiveOffsetMs= */ C.TIME_UNSET,
/* maxLiveOffsetMs= */ C.TIME_UNSET,
/* minPlaybackSpeed= */ C.RATE_UNSET,
/* maxPlaybackSpeed= */ C.RATE_UNSET));

float adjustedSpeed1 =
defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed(/* liveOffsetUs= */ 1_500_000);
defaultLivePlaybackSpeedControl.setLiveConfiguration(
new LiveConfiguration(
/* targetLiveOffsetMs= */ 1_000,
/* minLiveOffsetMs= */ C.TIME_UNSET,
/* maxLiveOffsetMs= */ C.TIME_UNSET,
/* minPlaybackSpeed= */ C.RATE_UNSET,
/* maxPlaybackSpeed= */ C.RATE_UNSET));
float adjustedSpeed2 =
defaultLivePlaybackSpeedControl.getAdjustedPlaybackSpeed(/* liveOffsetUs= */ 2_500_000);

assertThat(adjustedSpeed1).isNotEqualTo(adjustedSpeed2);
}

Expand Down

0 comments on commit 2416d99

Please sign in to comment.