mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
0def3b215c
commit
50f9f35353
4 changed files with 50 additions and 8 deletions
|
|
@ -61,6 +61,9 @@
|
||||||
* Effect:
|
* Effect:
|
||||||
* Muxers:
|
* Muxers:
|
||||||
* IMA extension:
|
* IMA extension:
|
||||||
|
* Fix bug where clearing the playlist may cause an
|
||||||
|
`ArrayIndexOutOfBoundsException` in
|
||||||
|
`ImaServerSideAdInsertionMediaSource`.
|
||||||
* Session:
|
* Session:
|
||||||
* Add `MediaButtonReceiver.shouldStartForegroundService(Intent)` to allow
|
* Add `MediaButtonReceiver.shouldStartForegroundService(Intent)` to allow
|
||||||
apps to suppress a play command coming in for playback resumption by
|
apps to suppress a play command coming in for playback resumption by
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||||
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
testImplementation project(modulePrefix + 'test-utils')
|
testImplementation project(modulePrefix + 'test-utils')
|
||||||
|
testImplementation project(modulePrefix + 'test-utils-robolectric')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1021,7 +1021,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadata(Metadata metadata) {
|
public void onMetadata(Metadata metadata) {
|
||||||
if (!isCurrentAdPlaying(player, getMediaItem(), adsId)) {
|
if (!isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < metadata.length(); i++) {
|
for (int i = 0; i < metadata.length(); i++) {
|
||||||
|
|
@ -1041,14 +1041,15 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackStateChanged(@Player.State int state) {
|
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();
|
streamPlayer.onContentCompleted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onVolumeChanged(float volume) {
|
public void onVolumeChanged(float volume) {
|
||||||
if (!isCurrentAdPlaying(player, getMediaItem(), adsId)) {
|
if (!isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int volumePct = (int) Math.floor(volume * 100);
|
int volumePct = (int) Math.floor(volume * 100);
|
||||||
|
|
@ -1312,7 +1313,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VideoProgressUpdate getContentProgress() {
|
public VideoProgressUpdate getContentProgress() {
|
||||||
if (!isCurrentAdPlaying(player, mediaItem, adsId)) {
|
if (!isCurrentlyPlayingMediaPeriodFromThisSource(player, mediaItem, adsId)) {
|
||||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||||
} else if (adPlaybackStates.isEmpty()) {
|
} else if (adPlaybackStates.isEmpty()) {
|
||||||
return new VideoProgressUpdate(/* currentTimeMs= */ 0, /* durationMs= */ C.TIME_UNSET);
|
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) {
|
Player player, MediaItem mediaItem, @Nullable Object adsId) {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE || player.getMediaItemCount() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Timeline.Period period = new Timeline.Period();
|
Timeline.Period period = new Timeline.Period();
|
||||||
|
|
@ -1510,7 +1511,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||||
private class SinglePeriodLiveAdEventListener implements AdEventListener {
|
private class SinglePeriodLiveAdEventListener implements AdEventListener {
|
||||||
@Override
|
@Override
|
||||||
public void onAdEvent(AdEvent event) {
|
public void onAdEvent(AdEvent event) {
|
||||||
if (!Objects.equals(event.getType(), LOADED)) {
|
if (!Objects.equals(event.getType(), LOADED)
|
||||||
|
|| !isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AdPlaybackState newAdPlaybackState = adPlaybackState;
|
AdPlaybackState newAdPlaybackState = adPlaybackState;
|
||||||
|
|
@ -1541,7 +1543,8 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
|
||||||
private class MultiPeriodLiveAdEventListener implements AdEventListener {
|
private class MultiPeriodLiveAdEventListener implements AdEventListener {
|
||||||
@Override
|
@Override
|
||||||
public void onAdEvent(AdEvent event) {
|
public void onAdEvent(AdEvent event) {
|
||||||
if (!Objects.equals(event.getType(), LOADED)) {
|
if (!Objects.equals(event.getType(), LOADED)
|
||||||
|
|| !isCurrentlyPlayingMediaPeriodFromThisSource(player, getMediaItem(), adsId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AdPodInfo adPodInfo = event.getAd().getAdPodInfo();
|
AdPodInfo adPodInfo = event.getAd().getAdPodInfo();
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,21 @@
|
||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.ima;
|
package androidx.media3.exoplayer.ima;
|
||||||
|
|
||||||
|
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
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.AdPlaybackState;
|
||||||
import androidx.media3.common.C;
|
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.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.exoplayer.source.ads.ServerSideAdInsertionUtil;
|
||||||
|
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -70,4 +79,30 @@ public class ImaServerSideAdInsertionMediaSourceTest {
|
||||||
|
|
||||||
assertThat(State.fromBundle(state.toBundle())).isEqualTo(state);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue