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();
|
.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
|
@Nullable
|
||||||
private static String getSystemProperty(String name) {
|
private static String getSystemProperty(String name) {
|
||||||
try {
|
try {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.source.ads;
|
package androidx.media3.exoplayer.source.ads;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Util.sum;
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
import androidx.annotation.CheckResult;
|
import androidx.annotation.CheckResult;
|
||||||
@ -36,23 +37,25 @@ public final class ServerSideAdInsertionUtil {
|
|||||||
/**
|
/**
|
||||||
* Adds a new server-side inserted ad group to an {@link AdPlaybackState}.
|
* 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 adPlaybackState The existing {@link AdPlaybackState}.
|
||||||
* @param fromPositionUs The position in the underlying server-side inserted ads stream at which
|
* @param fromPositionUs The position in the underlying server-side inserted ads stream at which
|
||||||
* the ad group starts, in microseconds.
|
* 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
|
* @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
|
* 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
|
* single insertion point, an offset of {@code toPositionUs-fromPositionUs} keeps the original
|
||||||
* stream timestamps after the ad group.
|
* 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}.
|
* @return The updated {@link AdPlaybackState}.
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
@CheckResult
|
||||||
public static AdPlaybackState addAdGroupToAdPlaybackState(
|
public static AdPlaybackState addAdGroupToAdPlaybackState(
|
||||||
AdPlaybackState adPlaybackState,
|
AdPlaybackState adPlaybackState,
|
||||||
long fromPositionUs,
|
long fromPositionUs,
|
||||||
long toPositionUs,
|
long contentResumeOffsetUs,
|
||||||
long contentResumeOffsetUs) {
|
long... adDurationsUs) {
|
||||||
long adGroupInsertionPositionUs =
|
long adGroupInsertionPositionUs =
|
||||||
getMediaPeriodPositionUsForContent(
|
getMediaPeriodPositionUsForContent(
|
||||||
fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
|
fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
|
||||||
@ -62,16 +65,21 @@ public final class ServerSideAdInsertionUtil {
|
|||||||
&& adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) {
|
&& adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) {
|
||||||
insertionIndex++;
|
insertionIndex++;
|
||||||
}
|
}
|
||||||
long adDurationUs = toPositionUs - fromPositionUs;
|
|
||||||
adPlaybackState =
|
adPlaybackState =
|
||||||
adPlaybackState
|
adPlaybackState
|
||||||
.withNewAdGroup(insertionIndex, adGroupInsertionPositionUs)
|
.withNewAdGroup(insertionIndex, adGroupInsertionPositionUs)
|
||||||
.withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true)
|
.withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true)
|
||||||
.withAdCount(insertionIndex, /* adCount= */ 1)
|
.withAdCount(insertionIndex, /* adCount= */ adDurationsUs.length)
|
||||||
.withAdDurationsUs(insertionIndex, adDurationUs)
|
.withAdDurationsUs(insertionIndex, adDurationsUs)
|
||||||
.withContentResumeOffsetUs(insertionIndex, contentResumeOffsetUs);
|
.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(
|
return correctFollowingAdGroupTimes(
|
||||||
adPlaybackState, insertionIndex, adDurationUs, contentResumeOffsetUs);
|
adPlaybackState, insertionIndex, sum(adDurationsUs), contentResumeOffsetUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,20 +183,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
adPlaybackState,
|
adPlaybackState,
|
||||||
/* fromPositionUs= */ 0,
|
/* fromPositionUs= */ 0,
|
||||||
/* toPositionUs= */ 200_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 200_000);
|
||||||
adPlaybackState =
|
adPlaybackState =
|
||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
adPlaybackState,
|
adPlaybackState,
|
||||||
/* fromPositionUs= */ 400_000,
|
/* fromPositionUs= */ 400_000,
|
||||||
/* toPositionUs= */ 700_000,
|
/* contentResumeOffsetUs= */ 1_000_000,
|
||||||
/* contentResumeOffsetUs= */ 1_000_000);
|
/* adDurationsUs...= */ 300_000);
|
||||||
AdPlaybackState firstAdPlaybackState =
|
AdPlaybackState firstAdPlaybackState =
|
||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
adPlaybackState,
|
adPlaybackState,
|
||||||
/* fromPositionUs= */ 900_000,
|
/* fromPositionUs= */ 900_000,
|
||||||
/* toPositionUs= */ 1_000_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 100_000);
|
||||||
|
|
||||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||||
mediaSourceRef.set(
|
mediaSourceRef.set(
|
||||||
@ -253,8 +253,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
new AdPlaybackState(/* adsId= */ new Object()),
|
new AdPlaybackState(/* adsId= */ new Object()),
|
||||||
/* fromPositionUs= */ 900_000,
|
/* fromPositionUs= */ 900_000,
|
||||||
/* toPositionUs= */ 1_000_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 100_000);
|
||||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||||
mediaSourceRef.set(
|
mediaSourceRef.set(
|
||||||
new ServerSideAdInsertionMediaSource(
|
new ServerSideAdInsertionMediaSource(
|
||||||
@ -281,8 +281,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
firstAdPlaybackState,
|
firstAdPlaybackState,
|
||||||
/* fromPositionUs= */ 0,
|
/* fromPositionUs= */ 0,
|
||||||
/* toPositionUs= */ 500_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 500_000);
|
||||||
mediaSourceRef
|
mediaSourceRef
|
||||||
.get()
|
.get()
|
||||||
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
|
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
|
||||||
@ -324,8 +324,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
new AdPlaybackState(/* adsId= */ new Object()),
|
new AdPlaybackState(/* adsId= */ new Object()),
|
||||||
/* fromPositionUs= */ 0,
|
/* fromPositionUs= */ 0,
|
||||||
/* toPositionUs= */ 500_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 500_000);
|
||||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||||
mediaSourceRef.set(
|
mediaSourceRef.set(
|
||||||
new ServerSideAdInsertionMediaSource(
|
new ServerSideAdInsertionMediaSource(
|
||||||
@ -392,20 +392,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
adPlaybackState,
|
adPlaybackState,
|
||||||
/* fromPositionUs= */ 0,
|
/* fromPositionUs= */ 0,
|
||||||
/* toPositionUs= */ 100_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 100_000);
|
||||||
adPlaybackState =
|
adPlaybackState =
|
||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
adPlaybackState,
|
adPlaybackState,
|
||||||
/* fromPositionUs= */ 600_000,
|
/* fromPositionUs= */ 600_000,
|
||||||
/* toPositionUs= */ 700_000,
|
/* contentResumeOffsetUs= */ 1_000_000,
|
||||||
/* contentResumeOffsetUs= */ 1_000_000);
|
/* adDurationsUs...= */ 100_000);
|
||||||
AdPlaybackState firstAdPlaybackState =
|
AdPlaybackState firstAdPlaybackState =
|
||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
adPlaybackState,
|
adPlaybackState,
|
||||||
/* fromPositionUs= */ 900_000,
|
/* fromPositionUs= */ 900_000,
|
||||||
/* toPositionUs= */ 1_000_000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 100_000);
|
||||||
|
|
||||||
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
|
||||||
mediaSourceRef.set(
|
mediaSourceRef.set(
|
||||||
@ -428,7 +428,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
|
|||||||
player.setMediaSource(mediaSourceRef.get());
|
player.setMediaSource(mediaSourceRef.get());
|
||||||
player.prepare();
|
player.prepare();
|
||||||
// Play to the first content part, then seek past the midroll.
|
// 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);
|
player.seekTo(/* positionMs= */ 1_600);
|
||||||
runUntilPendingCommandsAreFullyHandled(player);
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
long positionAfterSeekMs = player.getCurrentPosition();
|
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.getStreamPositionUsForAd;
|
||||||
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent;
|
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
|
||||||
import androidx.media3.common.AdPlaybackState;
|
import androidx.media3.common.AdPlaybackState;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -47,8 +48,8 @@ public final class ServerSideAdInsertionUtilTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
state,
|
state,
|
||||||
/* fromPositionUs= */ 4300,
|
/* fromPositionUs= */ 4300,
|
||||||
/* toPositionUs= */ 4500,
|
/* contentResumeOffsetUs= */ 400,
|
||||||
/* contentResumeOffsetUs= */ 400);
|
/* adDurationsUs...= */ 200);
|
||||||
|
|
||||||
assertThat(state)
|
assertThat(state)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
@ -65,8 +66,8 @@ public final class ServerSideAdInsertionUtilTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
state,
|
state,
|
||||||
/* fromPositionUs= */ 2100,
|
/* fromPositionUs= */ 2100,
|
||||||
/* toPositionUs= */ 2400,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 300);
|
||||||
|
|
||||||
assertThat(state)
|
assertThat(state)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
@ -87,8 +88,8 @@ public final class ServerSideAdInsertionUtilTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
state,
|
state,
|
||||||
/* fromPositionUs= */ 0,
|
/* fromPositionUs= */ 0,
|
||||||
/* toPositionUs= */ 100,
|
/* contentResumeOffsetUs= */ 50,
|
||||||
/* contentResumeOffsetUs= */ 50);
|
/* adDurationsUs...= */ 100);
|
||||||
|
|
||||||
assertThat(state)
|
assertThat(state)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
@ -113,8 +114,8 @@ public final class ServerSideAdInsertionUtilTest {
|
|||||||
addAdGroupToAdPlaybackState(
|
addAdGroupToAdPlaybackState(
|
||||||
state,
|
state,
|
||||||
/* fromPositionUs= */ 5000,
|
/* fromPositionUs= */ 5000,
|
||||||
/* toPositionUs= */ 6000,
|
/* contentResumeOffsetUs= */ 0,
|
||||||
/* contentResumeOffsetUs= */ 0);
|
/* adDurationsUs...= */ 1000);
|
||||||
|
|
||||||
assertThat(state)
|
assertThat(state)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
@ -144,6 +145,33 @@ public final class ServerSideAdInsertionUtilTest {
|
|||||||
.withAdDurationsUs(/* adGroupIndex= */ 5, /* adDurationsUs...= */ 1000));
|
.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
|
@Test
|
||||||
public void getStreamPositionUsForAd_returnsCorrectPositions() {
|
public void getStreamPositionUsForAd_returnsCorrectPositions() {
|
||||||
// stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content
|
// stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content
|
||||||
|
Loading…
x
Reference in New Issue
Block a user