From 378593f551300bf7db79b5a25ec7b713d2b732e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 30 Mar 2020 17:45:45 +0100 Subject: [PATCH] Add some playback tests for the IMA extension These are about 5% flaky, so for now they are excluded from continuous testing. PiperOrigin-RevId: 303760340 --- extensions/ima/build.gradle | 12 + .../ima/src/androidTest/AndroidManifest.xml | 38 +++ .../exoplayer2/ext/ima/ImaPlaybackTest.java | 231 ++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 extensions/ima/src/androidTest/AndroidManifest.xml create mode 100644 extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index f516cc5001..1bd0b8dbd4 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -26,6 +26,13 @@ android { minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + // Enable multidex for androidTests. + multiDexEnabled true + } + + sourceSets { + androidTest.assets.srcDir '../../testdata/src/test/assets/' } testOptions.unitTests.includeAndroidResources = true @@ -37,6 +44,11 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + androidTestImplementation project(modulePrefix + 'testutils') + androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion + androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion + androidTestImplementation 'com.android.support:multidex:1.0.3' + androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/ima/src/androidTest/AndroidManifest.xml b/extensions/ima/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..c8bd575f60 --- /dev/null +++ b/extensions/ima/src/androidTest/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java new file mode 100644 index 0000000000..ed9130bc72 --- /dev/null +++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.Uri; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Player.TimelineChangeReason; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.testutil.ExoHostedTest; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Playback tests using {@link ImaAdsLoader}. */ +@RunWith(AndroidJUnit4.class) +public final class ImaPlaybackTest { + + private static final long TIMEOUT_MS = 5 * 60 * C.MILLIS_PER_SECOND; + + private static final String CONTENT_URI = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4"; + private static final String PREROLL_ADS_RESPONSE_FILE_NAME = "ad-responses/preroll.xml"; + private static final String MIDROLL_ADS_RESPONSE_FILE_NAME = "ad-responses/midroll.xml"; + + private static final AdId CONTENT = new AdId(C.INDEX_UNSET, C.INDEX_UNSET); + + @Rule public ActivityTestRule testRule = new ActivityTestRule<>(HostActivity.class); + + @Test + public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception { + AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT}; + String adsResponse = + TestUtil.getString(/* context= */ testRule.getActivity(), PREROLL_ADS_RESPONSE_FILE_NAME); + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI), adsResponse, expectedAdIds); + + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + @Test + public void playbackWithMidrolls_playsAdAndContent() throws Exception { + AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT, ad(1), CONTENT, ad(2), CONTENT}; + String adsResponse = + TestUtil.getString(/* context= */ testRule.getActivity(), MIDROLL_ADS_RESPONSE_FILE_NAME); + ImaHostedTest hostedTest = + new ImaHostedTest(Uri.parse(CONTENT_URI), adsResponse, expectedAdIds); + + testRule.getActivity().runTest(hostedTest, TIMEOUT_MS); + } + + private static AdId ad(int groupIndex) { + return new AdId(groupIndex, /* indexInGroup= */ 0); + } + + private static final class AdId { + + public final int groupIndex; + public final int indexInGroup; + + public AdId(int groupIndex, int indexInGroup) { + this.groupIndex = groupIndex; + this.indexInGroup = indexInGroup; + } + + @Override + public String toString() { + return "(" + groupIndex + ", " + indexInGroup + ')'; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AdId that = (AdId) o; + + if (groupIndex != that.groupIndex) { + return false; + } + return indexInGroup == that.indexInGroup; + } + + @Override + public int hashCode() { + int result = groupIndex; + result = 31 * result + indexInGroup; + return result; + } + } + + private static final class ImaHostedTest extends ExoHostedTest implements EventListener { + + private final Uri contentUri; + private final String adsResponse; + private final List expectedAdIds; + private final List seenAdIds; + private @MonotonicNonNull ImaAdsLoader imaAdsLoader; + private @MonotonicNonNull SimpleExoPlayer player; + + private ImaHostedTest(Uri contentUri, String adsResponse, AdId... expectedAdIds) { + // fullPlaybackNoSeeking is false as the playback lasts longer than the content source + // duration due to ad playback, so the hosted test shouldn't assert the playing duration. + super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false); + this.contentUri = contentUri; + this.adsResponse = adsResponse; + this.expectedAdIds = Arrays.asList(expectedAdIds); + seenAdIds = new ArrayList<>(); + } + + @Override + protected SimpleExoPlayer buildExoPlayer( + HostActivity host, Surface surface, MappingTrackSelector trackSelector) { + player = super.buildExoPlayer(host, surface, trackSelector); + player.addAnalyticsListener( + new AnalyticsListener() { + @Override + public void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) { + maybeUpdateSeenAdIdentifiers(); + } + + @Override + public void onPositionDiscontinuity( + EventTime eventTime, @DiscontinuityReason int reason) { + maybeUpdateSeenAdIdentifiers(); + } + }); + Context context = host.getApplicationContext(); + imaAdsLoader = new ImaAdsLoader.Builder(context).buildForAdsResponse(adsResponse); + imaAdsLoader.setPlayer(player); + return player; + } + + @Override + protected MediaSource buildSource( + HostActivity host, + String userAgent, + DrmSessionManager drmSessionManager, + FrameLayout overlayFrameLayout) { + Context context = host.getApplicationContext(); + DataSource.Factory dataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName())); + MediaSource contentMediaSource = + DefaultMediaSourceFactory.newInstance(context) + .createMediaSource(MediaItem.fromUri(contentUri)); + return new AdsMediaSource( + contentMediaSource, + dataSourceFactory, + Assertions.checkNotNull(imaAdsLoader), + new AdViewProvider() { + @Override + public ViewGroup getAdViewGroup() { + return overlayFrameLayout; + } + + @Override + public View[] getAdOverlayViews() { + return new View[0]; + } + }); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + assertThat(seenAdIds).isEqualTo(expectedAdIds); + } + + private void maybeUpdateSeenAdIdentifiers() { + if (Assertions.checkNotNull(player) + .getCurrentTimeline() + .getWindow(/* windowIndex= */ 0, new Window()) + .isPlaceholder) { + // The window is still an initial placeholder so do nothing. + return; + } + AdId adId = new AdId(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup()); + if (seenAdIds.isEmpty() || !seenAdIds.get(seenAdIds.size() - 1).equals(adId)) { + seenAdIds.add(adId); + } + } + } +}