Set the next live ad in ad group to avoid rebuffering
To avoid the `MediaPeriodQueue`to discard the reading period, we can set the next ad of an ad group early and then (possibly) only change it's duration once we receive the actual duration. This way we avoid a rebuffering as a result of the reading period being discarded. The change also takes care to properly set ad break and their durations when we join the live stream at the moment when an ad is playing. PiperOrigin-RevId: 423163467
This commit is contained in:
parent
a9e6bc60cb
commit
a1424c834f
@ -2539,6 +2539,20 @@ public final class Util {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of all summands of the given array.
|
||||
*
|
||||
* @param summands The summands to calculate the sum from.
|
||||
* @return The sum of all summands.
|
||||
*/
|
||||
public static long sum(long... summands) {
|
||||
long sum = 0;
|
||||
for (long summand : summands) {
|
||||
sum += summand;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getSystemProperty(String name) {
|
||||
try {
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer.source.ads;
|
||||
|
||||
import static androidx.media3.common.util.Util.sum;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
@ -36,23 +37,25 @@ public final class ServerSideAdInsertionUtil {
|
||||
/**
|
||||
* Adds a new server-side inserted ad group to an {@link AdPlaybackState}.
|
||||
*
|
||||
* <p>If the first ad with a non-zero duration is not the first ad in the group, all ads before
|
||||
* that ad are marked as skipped.
|
||||
*
|
||||
* @param adPlaybackState The existing {@link AdPlaybackState}.
|
||||
* @param fromPositionUs The position in the underlying server-side inserted ads stream at which
|
||||
* the ad group starts, in microseconds.
|
||||
* @param toPositionUs The position in the underlying server-side inserted ads stream at which the
|
||||
* ad group ends, in microseconds.
|
||||
* @param contentResumeOffsetUs The timestamp offset which should be added to the content stream
|
||||
* when resuming playback after the ad group. An offset of 0 collapses the ad group to a
|
||||
* single insertion point, an offset of {@code toPositionUs-fromPositionUs} keeps the original
|
||||
* stream timestamps after the ad group.
|
||||
* @param adDurationsUs The durations of the ads to be added to the group, in microseconds.
|
||||
* @return The updated {@link AdPlaybackState}.
|
||||
*/
|
||||
@CheckResult
|
||||
public static AdPlaybackState addAdGroupToAdPlaybackState(
|
||||
AdPlaybackState adPlaybackState,
|
||||
long fromPositionUs,
|
||||
long toPositionUs,
|
||||
long contentResumeOffsetUs) {
|
||||
long contentResumeOffsetUs,
|
||||
long... adDurationsUs) {
|
||||
long adGroupInsertionPositionUs =
|
||||
getMediaPeriodPositionUsForContent(
|
||||
fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
|
||||
@ -62,16 +65,21 @@ public final class ServerSideAdInsertionUtil {
|
||||
&& adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) {
|
||||
insertionIndex++;
|
||||
}
|
||||
long adDurationUs = toPositionUs - fromPositionUs;
|
||||
adPlaybackState =
|
||||
adPlaybackState
|
||||
.withNewAdGroup(insertionIndex, adGroupInsertionPositionUs)
|
||||
.withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true)
|
||||
.withAdCount(insertionIndex, /* adCount= */ 1)
|
||||
.withAdDurationsUs(insertionIndex, adDurationUs)
|
||||
.withAdCount(insertionIndex, /* adCount= */ adDurationsUs.length)
|
||||
.withAdDurationsUs(insertionIndex, adDurationsUs)
|
||||
.withContentResumeOffsetUs(insertionIndex, contentResumeOffsetUs);
|
||||
// Mark all ads as skipped that are before the first ad with a non-zero duration.
|
||||
int adIndex = 0;
|
||||
while (adIndex < adDurationsUs.length && adDurationsUs[adIndex] == 0) {
|
||||
adPlaybackState =
|
||||
adPlaybackState.withSkippedAd(insertionIndex, /* adIndexInAdGroup= */ adIndex++);
|
||||
}
|
||||
return correctFollowingAdGroupTimes(
|
||||
adPlaybackState, insertionIndex, adDurationUs, contentResumeOffsetUs);
|
||||
adPlaybackState, insertionIndex, sum(adDurationsUs), contentResumeOffsetUs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,20 +183,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
adPlaybackState,
|
||||
/* fromPositionUs= */ 0,
|
||||
/* toPositionUs= */ 200_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 200_000);
|
||||
adPlaybackState =
|
||||
addAdGroupToAdPlaybackState(
|
||||
adPlaybackState,
|
||||
/* fromPositionUs= */ 400_000,
|
||||
/* toPositionUs= */ 700_000,
|
||||
/* contentResumeOffsetUs= */ 1_000_000);
|
||||
/* contentResumeOffsetUs= */ 1_000_000,
|
||||
/* adDurationsUs...= */ 300_000);
|
||||
AdPlaybackState firstAdPlaybackState =
|
||||
addAdGroupToAdPlaybackState(
|
||||
adPlaybackState,
|
||||
/* fromPositionUs= */ 900_000,
|
||||
/* toPositionUs= */ 1_000_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 100_000);
|
||||
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||
mediaSourceRef.set(
|
||||
@ -253,8 +253,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
new AdPlaybackState(/* adsId= */ new Object()),
|
||||
/* fromPositionUs= */ 900_000,
|
||||
/* toPositionUs= */ 1_000_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 100_000);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||
mediaSourceRef.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
@ -281,8 +281,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
firstAdPlaybackState,
|
||||
/* fromPositionUs= */ 0,
|
||||
/* toPositionUs= */ 500_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 500_000);
|
||||
mediaSourceRef
|
||||
.get()
|
||||
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
|
||||
@ -324,8 +324,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
new AdPlaybackState(/* adsId= */ new Object()),
|
||||
/* fromPositionUs= */ 0,
|
||||
/* toPositionUs= */ 500_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 500_000);
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||
mediaSourceRef.set(
|
||||
new ServerSideAdInsertionMediaSource(
|
||||
@ -392,20 +392,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
adPlaybackState,
|
||||
/* fromPositionUs= */ 0,
|
||||
/* toPositionUs= */ 100_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 100_000);
|
||||
adPlaybackState =
|
||||
addAdGroupToAdPlaybackState(
|
||||
adPlaybackState,
|
||||
/* fromPositionUs= */ 600_000,
|
||||
/* toPositionUs= */ 700_000,
|
||||
/* contentResumeOffsetUs= */ 1_000_000);
|
||||
/* contentResumeOffsetUs= */ 1_000_000,
|
||||
/* adDurationsUs...= */ 100_000);
|
||||
AdPlaybackState firstAdPlaybackState =
|
||||
addAdGroupToAdPlaybackState(
|
||||
adPlaybackState,
|
||||
/* fromPositionUs= */ 900_000,
|
||||
/* toPositionUs= */ 1_000_000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 100_000);
|
||||
|
||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||
mediaSourceRef.set(
|
||||
@ -428,7 +428,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
||||
player.setMediaSource(mediaSourceRef.get());
|
||||
player.prepare();
|
||||
// Play to the first content part, then seek past the midroll.
|
||||
playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 100);
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 100);
|
||||
player.seekTo(/* positionMs= */ 1_600);
|
||||
runUntilPendingCommandsAreFullyHandled(player);
|
||||
long positionAfterSeekMs = player.getCurrentPosition();
|
||||
|
@ -22,6 +22,7 @@ import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.get
|
||||
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForAd;
|
||||
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.Arrays.stream;
|
||||
|
||||
import androidx.media3.common.AdPlaybackState;
|
||||
import androidx.media3.common.C;
|
||||
@ -47,8 +48,8 @@ public final class ServerSideAdInsertionUtilTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
state,
|
||||
/* fromPositionUs= */ 4300,
|
||||
/* toPositionUs= */ 4500,
|
||||
/* contentResumeOffsetUs= */ 400);
|
||||
/* contentResumeOffsetUs= */ 400,
|
||||
/* adDurationsUs...= */ 200);
|
||||
|
||||
assertThat(state)
|
||||
.isEqualTo(
|
||||
@ -65,8 +66,8 @@ public final class ServerSideAdInsertionUtilTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
state,
|
||||
/* fromPositionUs= */ 2100,
|
||||
/* toPositionUs= */ 2400,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 300);
|
||||
|
||||
assertThat(state)
|
||||
.isEqualTo(
|
||||
@ -87,8 +88,8 @@ public final class ServerSideAdInsertionUtilTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
state,
|
||||
/* fromPositionUs= */ 0,
|
||||
/* toPositionUs= */ 100,
|
||||
/* contentResumeOffsetUs= */ 50);
|
||||
/* contentResumeOffsetUs= */ 50,
|
||||
/* adDurationsUs...= */ 100);
|
||||
|
||||
assertThat(state)
|
||||
.isEqualTo(
|
||||
@ -113,8 +114,8 @@ public final class ServerSideAdInsertionUtilTest {
|
||||
addAdGroupToAdPlaybackState(
|
||||
state,
|
||||
/* fromPositionUs= */ 5000,
|
||||
/* toPositionUs= */ 6000,
|
||||
/* contentResumeOffsetUs= */ 0);
|
||||
/* contentResumeOffsetUs= */ 0,
|
||||
/* adDurationsUs...= */ 1000);
|
||||
|
||||
assertThat(state)
|
||||
.isEqualTo(
|
||||
@ -144,6 +145,33 @@ public final class ServerSideAdInsertionUtilTest {
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 5, /* adDurationsUs...= */ 1000));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addAdGroupToAdPlaybackState_emptyLeadingAds_markedAsSkipped() {
|
||||
AdPlaybackState state = new AdPlaybackState(ADS_ID);
|
||||
|
||||
state =
|
||||
addAdGroupToAdPlaybackState(
|
||||
state,
|
||||
/* fromPositionUs= */ 0,
|
||||
/* contentResumeOffsetUs= */ 50_000,
|
||||
/* adDurationsUs...= */ 0,
|
||||
0,
|
||||
10_000,
|
||||
40_000,
|
||||
0);
|
||||
|
||||
AdPlaybackState.AdGroup adGroup = state.getAdGroup(/* adGroupIndex= */ 0);
|
||||
assertThat(adGroup.durationsUs[0]).isEqualTo(0);
|
||||
assertThat(adGroup.states[0]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
|
||||
assertThat(adGroup.durationsUs[1]).isEqualTo(0);
|
||||
assertThat(adGroup.states[1]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
|
||||
assertThat(adGroup.durationsUs[2]).isEqualTo(10_000);
|
||||
assertThat(adGroup.states[2]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
|
||||
assertThat(adGroup.durationsUs[4]).isEqualTo(0);
|
||||
assertThat(adGroup.states[4]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
|
||||
assertThat(stream(adGroup.durationsUs).sum()).isEqualTo(50_000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getStreamPositionUsForAd_returnsCorrectPositions() {
|
||||
// stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content
|
||||
|
Loading…
x
Reference in New Issue
Block a user