Skip to content

Commit c8c65e7

Browse files
Merge pull request #2655 from stevemayhew:p-fixes-ad-error-handling
PiperOrigin-RevId: 828980223
2 parents 3ee6bd6 + 694e85c commit c8c65e7

File tree

3 files changed

+55
-5
lines changed

3 files changed

+55
-5
lines changed

RELEASENOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
side when `Presentation` is created with a single side length.
3232
* Muxers:
3333
* IMA extension:
34+
* Fix issue where content preparation error for content after an ad would
35+
be wrongly reported as an ad playback error
36+
([#2656](https://github.com/androidx/media/issues/2656)).
3437
* Session:
3538
* Add `CommandButton.executeAction` so that controllers can more easily
3639
trigger the intended action. Also allow to specify parameters for some

libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,21 @@ public void activate(Player player) {
391391
/** Deactivates playback. */
392392
public void deactivate() {
393393
Player player = checkNotNull(this.player);
394-
if (!AdPlaybackState.NONE.equals(adPlaybackState) && imaPausedContent) {
394+
// Post deactivation behind any already queued Player.Listener events to ensure that
395+
// any pending events are processed before the listener is removed and the ads manager paused.
396+
handler.post(() -> deactivateInternal(player));
397+
}
398+
399+
/**
400+
* Deactivates playback internally, after the Listener.onEvents() cycle completes so the complete
401+
* state change picture is clear. For example, if an error caused the deactivation, the error
402+
* callback can be handled first.
403+
*/
404+
private void deactivateInternal(Player player) {
405+
if (!adPlaybackState.equals(AdPlaybackState.NONE)
406+
&& imaPausedContent
407+
&& player.getPlayerError() == null) {
408+
// Only need to pause and store resume position if not in error state.
395409
if (adsManager != null) {
396410
adsManager.pause();
397411
}
@@ -402,9 +416,7 @@ public void deactivate() {
402416
lastVolumePercent = getPlayerVolumePercent();
403417
lastAdProgress = getAdVideoProgressUpdate();
404418
lastContentProgress = getContentVideoProgressUpdate();
405-
406-
// Post release of listener so that we can report any already pending errors via onPlayerError.
407-
handler.post(() -> player.removeListener(this));
419+
player.removeListener(this);
408420
this.player = null;
409421
}
410422

@@ -539,7 +551,7 @@ public void onPlayWhenReadyChanged(
539551

540552
@Override
541553
public void onPlayerError(PlaybackException error) {
542-
if (imaAdState != IMA_AD_STATE_NONE) {
554+
if (imaAdState != IMA_AD_STATE_NONE && checkNotNull(player).isPlayingAd()) {
543555
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
544556
for (int i = 0; i < adCallbacks.size(); i++) {
545557
adCallbacks.get(i).onError(adMediaInfo);

libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaAdsLoaderTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,41 @@ public void buildWithEnableContinuousPlayback_setsAdsRequestProperty() {
14811481
verify(mockAdsRequest).setContinuousPlayback(true);
14821482
}
14831483

1484+
@Test
1485+
public void contentErrorDuringAdPlayback_doesNotMarkAdAsFailed() throws IOException {
1486+
// Load and play a preroll ad.
1487+
imaAdsLoader.start(
1488+
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
1489+
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
1490+
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
1491+
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
1492+
videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
1493+
fakePlayer.setPlayingAdPosition(
1494+
/* periodIndex= */ 0,
1495+
/* adGroupIndex= */ 0,
1496+
/* adIndexInAdGroup= */ 0,
1497+
/* positionMs= */ 0,
1498+
/* contentPositionMs= */ 0);
1499+
fakePlayer.setState(Player.STATE_READY, /* playWhenReady= */ true);
1500+
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
1501+
1502+
// Simulate a content preparation error while the ad is playing.
1503+
// The player is not playing an ad from its perspective, so isPlayingAd() is false.
1504+
fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, /* positionMs= */ 0);
1505+
ExoPlaybackException error =
1506+
ExoPlaybackException.createForSource(
1507+
new IOException("Content preparation error"),
1508+
PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
1509+
fakePlayer.setPlayerError(error);
1510+
shadowOf(Looper.getMainLooper()).runToEndOfTasks();
1511+
1512+
// Verify that the ad is not marked as failed.
1513+
AdPlaybackState adPlaybackState = getAdPlaybackState(0);
1514+
assertThat(adPlaybackState.getAdGroup(0).states[0])
1515+
.isNotEqualTo(AdPlaybackState.AD_STATE_ERROR);
1516+
verify(mockVideoAdPlayerCallback, never()).onError(any());
1517+
}
1518+
14841519
private void setupMocks() {
14851520
ArgumentCaptor<Object> userRequestContextCaptor = ArgumentCaptor.forClass(Object.class);
14861521
doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture());

0 commit comments

Comments
 (0)