Guard timeline access in ImaSSAIMS against empty timelines

All methods check if the player is currently handling the ad source
by calling isCurrentAdPlaying(). This method was missing a check
for empty timelines that throws an exception when trying to access
a non-existent period.

Also add this check to two methods that assume the current item
is the ads source, but didn't check it yet.

PiperOrigin-RevId: 653963557
This commit is contained in:
tonihei 2024-07-19 04:25:01 -07:00 committed by Copybara-Service
parent 0def3b215c
commit 50f9f35353
4 changed files with 50 additions and 8 deletions

View File

@ -61,6 +61,9 @@
* Effect:
* Muxers:
* IMA extension:
* Fix bug where clearing the playlist may cause an
`ArrayIndexOutOfBoundsException` in
`ImaServerSideAdInsertionMediaSource`.
* Session:
* Add `MediaButtonReceiver.shouldStartForegroundService(Intent)` to allow
apps to suppress a play command coming in for playback resumption by

View File

@ -39,6 +39,7 @@ dependencies {
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'test-utils')
testImplementation project(modulePrefix + 'test-utils-robolectric')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View File

@ -1021,7 +1021,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public void onMetadata(Metadata metadata) {
if (!isCurrentAdPlaying(player, getMediaItem(), adsId)) {
if (!isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
return;
}
for (int i = 0; i < metadata.length(); i++) {
@ -1041,14 +1041,15 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public void onPlaybackStateChanged(@Player.State int state) {
if (state == Player.STATE_ENDED && isCurrentAdPlaying(player, getMediaItem(), adsId)) {
if (state == Player.STATE_ENDED
&& isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
streamPlayer.onContentCompleted();
}
}
@Override
public void onVolumeChanged(float volume) {
if (!isCurrentAdPlaying(player, getMediaItem(), adsId)) {
if (!isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
return;
}
int volumePct = (int) Math.floor(volume * 100);
@ -1312,7 +1313,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public VideoProgressUpdate getContentProgress() {
if (!isCurrentAdPlaying(player, mediaItem, adsId)) {
if (!isCurrentlyPlayingMediaPeriodFromThisSource(player, mediaItem, adsId)) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else if (adPlaybackStates.isEmpty()) {
return new VideoProgressUpdate(/* currentTimeMs= */ 0, /* durationMs= */ C.TIME_UNSET);
@ -1428,9 +1429,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
}
}
private static boolean isCurrentAdPlaying(
private static boolean isCurrentlyPlayingMediaPeriodFromThisSource(
Player player, MediaItem mediaItem, @Nullable Object adsId) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (player.getPlaybackState() == Player.STATE_IDLE || player.getMediaItemCount() == 0) {
return false;
}
Timeline.Period period = new Timeline.Period();
@ -1510,7 +1511,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private class SinglePeriodLiveAdEventListener implements AdEventListener {
@Override
public void onAdEvent(AdEvent event) {
if (!Objects.equals(event.getType(), LOADED)) {
if (!Objects.equals(event.getType(), LOADED)
|| !isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
return;
}
AdPlaybackState newAdPlaybackState = adPlaybackState;
@ -1541,7 +1543,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private class MultiPeriodLiveAdEventListener implements AdEventListener {
@Override
public void onAdEvent(AdEvent event) {
if (!Objects.equals(event.getType(), LOADED)) {
if (!Objects.equals(event.getType(), LOADED)
|| !isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
return;
}
AdPodInfo adPodInfo = event.getAd().getAdPodInfo();

View File

@ -15,12 +15,21 @@
*/
package androidx.media3.exoplayer.ima;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.widget.LinearLayout;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
@ -70,4 +79,30 @@ public class ImaServerSideAdInsertionMediaSourceTest {
assertThat(State.fromBundle(state.toBundle())).isEqualTo(state);
}
@Test
public void clearPlaylist_withAdsSource_handlesCleanupWithoutThrowing() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
ExoPlayer player = new TestExoPlayerBuilder(context).build();
ImaServerSideAdInsertionMediaSource.AdsLoader adsLoader =
new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(
context, /* adViewProvider= */ () -> new LinearLayout(context))
.build();
adsLoader.setPlayer(player);
MediaSource mediaSource =
new ImaServerSideAdInsertionMediaSource.Factory(
adsLoader, new DefaultMediaSourceFactory(context))
.createMediaSource(
MediaItem.fromUri("ssai://dai.google.com/?assetKey=ABC&format=0&adsId=2"));
player.setMediaSource(mediaSource);
player.prepare();
run(player).untilPendingCommandsAreFullyHandled();
// Clearing the playlist will cause internal state of the ads source to be invalid and
// potentially accessing empty timelines. See b/354026260. The test simply ensures that clearing
// the playlist will not throw any exceptions.
player.clearMediaItems();
run(player).untilPendingCommandsAreFullyHandled();
player.release();
}
}