From d538d6ae37acbcaf02f7fc452ecd80413b973366 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Apr 2020 15:52:18 +0100 Subject: [PATCH 01/80] Fix capabilities check for low frame-rate content Issue: #6054 Issue: #474 PiperOrigin-RevId: 306437452 --- .../google/android/exoplayer2/mediacodec/MediaCodecInfo.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 64517feec9..60c29f6183 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -573,7 +573,9 @@ public final class MediaCodecInfo { width = alignedSize.x; height = alignedSize.y; - if (frameRate == Format.NO_VALUE || frameRate <= 0) { + // VideoCapabilities.areSizeAndRateSupported incorrectly returns false if frameRate < 1 on some + // versions of Android, so we only check the size in this case [Internal ref: b/153940404]. + if (frameRate == Format.NO_VALUE || frameRate < 1) { return capabilities.isSizeSupported(width, height); } else { // The signaled frame rate may be slightly higher than the actual frame rate, so we take the From bdc0db30fdddc3cbc01827acdf46b56dfc802bb5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Apr 2020 21:34:11 +0100 Subject: [PATCH 02/80] Don't select trick-play tracks by default Issue: #6054 Issue: #474 PiperOrigin-RevId: 306504362 --- .../src/main/java/com/google/android/exoplayer2/C.java | 8 ++++++-- .../trackselection/DefaultTrackSelector.java | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 43cedf985b..3eee0a1891 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -1056,7 +1056,8 @@ public final class C { * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY}, - * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}. + * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ} and {@link + * #ROLE_FLAG_TRICK_PLAY}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -1076,7 +1077,8 @@ public final class C { ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, ROLE_FLAG_TRANSCRIBES_DIALOG, - ROLE_FLAG_EASY_TO_READ + ROLE_FLAG_EASY_TO_READ, + ROLE_FLAG_TRICK_PLAY }) public @interface RoleFlags {} /** Indicates a main track. */ @@ -1122,6 +1124,8 @@ public final class C { public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12; /** Indicates the track contains a text that has been edited for ease of reading. */ public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13; + /** Indicates the track is intended for trick play. */ + public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 822fd03fdf..5330894dab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -1990,6 +1990,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { int maxVideoHeight, int maxVideoFrameRate, int maxVideoBitrate) { + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + // Ignore trick-play tracks for now. + return false; + } return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) @@ -2013,9 +2017,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + Format format = trackGroup.getFormat(trackIndex); + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + // Ignore trick-play tracks for now. + continue; + } if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { - Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) From 2df94913835e7a187e4e08d6f62b4a33169b1803 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 15 Apr 2020 14:33:01 +0100 Subject: [PATCH 03/80] Avoid throwing an exception for sample default values Allows playback of content when the default value is not valid, but not used for any samples. Issue: #7207 PiperOrigin-RevId: 306631376 --- RELEASENOTES.md | 6 ++ .../extractor/mp4/FragmentedMp4Extractor.java | 62 +++++++++++++------ 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8820273a73..3ae572c5b2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,11 @@ # Release notes # +### Next release ### + +* Avoid throwing an exception while parsing fragmented MP4 default sample + values where the most-significant bit is set + ([#7207](https://github.com/google/ExoPlayer/issues/7207)). + ### 2.11.4 (2020-04-08) ### * Add `SimpleExoPlayer.setWakeMode` to allow automatic `WifiLock` and `WakeLock` diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 35f85d0a08..42aeab64b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -664,9 +664,9 @@ public class FragmentedMp4Extractor implements Extractor { private static Pair parseTrex(ParsableByteArray trex) { trex.setPosition(Atom.FULL_HEADER_SIZE); int trackId = trex.readInt(); - int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; - int defaultSampleDuration = trex.readUnsignedIntToInt(); - int defaultSampleSize = trex.readUnsignedIntToInt(); + int defaultSampleDescriptionIndex = trex.readInt() - 1; + int defaultSampleDuration = trex.readInt(); + int defaultSampleSize = trex.readInt(); int defaultSampleFlags = trex.readInt(); return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, @@ -751,8 +751,9 @@ public class FragmentedMp4Extractor implements Extractor { } } - private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime, - @Flags int flags) { + private static void parseTruns( + ContainerAtom traf, TrackBundle trackBundle, long decodeTime, @Flags int flags) + throws ParserException { int trunCount = 0; int totalSampleCount = 0; List leafChildren = traf.leafChildren; @@ -871,13 +872,20 @@ public class FragmentedMp4Extractor implements Extractor { DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; int defaultSampleDescriptionIndex = ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) - ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; - int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) - ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; - int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) - ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; - int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) - ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; + ? tfhd.readInt() - 1 + : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = + ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.duration; + int defaultSampleSize = + ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.size; + int defaultSampleFlags = + ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.flags; trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, defaultSampleSize, defaultSampleFlags); return trackBundle; @@ -910,16 +918,22 @@ public class FragmentedMp4Extractor implements Extractor { /** * Parses a trun atom (defined in 14496-12). * - * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into - * which parsed data should be placed. + * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into which + * parsed data should be placed. * @param index Index of the track run in the fragment. * @param decodeTime The decode time of the first sample in the fragment run. * @param flags Flags to allow any required workaround to be executed. * @param trun The trun atom to decode. * @return The starting position of samples for the next run. */ - private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, - @Flags int flags, ParsableByteArray trun, int trackRunStart) { + private static int parseTrun( + TrackBundle trackBundle, + int index, + long decodeTime, + @Flags int flags, + ParsableByteArray trun, + int trackRunStart) + throws ParserException { trun.setPosition(Atom.HEADER_SIZE); int fullAtom = trun.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); @@ -937,7 +951,7 @@ public class FragmentedMp4Extractor implements Extractor { boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; int firstSampleFlags = defaultSampleValues.flags; if (firstSampleFlagsPresent) { - firstSampleFlags = trun.readUnsignedIntToInt(); + firstSampleFlags = trun.readInt(); } boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; @@ -972,9 +986,10 @@ public class FragmentedMp4Extractor implements Extractor { long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime; for (int i = trackRunStart; i < trackRunEnd; i++) { // Use trun values if present, otherwise tfhd, otherwise trex. - int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() - : defaultSampleValues.duration; - int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size; + int sampleDuration = + checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration); + int sampleSize = + checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size); int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; if (sampleCompositionTimeOffsetsPresent) { @@ -1000,6 +1015,13 @@ public class FragmentedMp4Extractor implements Extractor { return trackRunEnd; } + private static int checkNonNegative(int value) throws ParserException { + if (value < 0) { + throw new ParserException("Unexpected negtive value: " + value); + } + return value; + } + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, byte[] extendedTypeScratch) throws ParserException { uuid.setPosition(Atom.HEADER_SIZE); From 48081dd073e97e792b8a8859340a01f25edcaf6b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Apr 2020 15:47:10 +0100 Subject: [PATCH 04/80] Parse trick-play role flags from DASH manifests Issue: #6054 PiperOrigin-RevId: 306641689 --- .../source/dash/manifest/AdaptationSet.java | 29 +++++------ .../source/dash/manifest/DashManifest.java | 11 +++-- .../dash/manifest/DashManifestParser.java | 49 +++++++++++++++++-- .../src/test/assets/sample_mpd_trick_play | 32 ++++++++++++ .../source/dash/DashMediaPeriodTest.java | 1 + .../exoplayer2/source/dash/DashUtilTest.java | 17 +++++-- .../dash/manifest/DashManifestParserTest.java | 43 +++++++++++++++- .../dash/manifest/DashManifestTest.java | 8 ++- 8 files changed, 161 insertions(+), 29 deletions(-) create mode 100644 library/dash/src/test/assets/sample_mpd_trick_play diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index d962374745..b0689eeb11 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -50,9 +50,10 @@ public class AdaptationSet { */ public final List accessibilityDescriptors; - /** - * Supplemental properties in the adaptation set. - */ + /** Essential properties in the adaptation set. */ + public final List essentialProperties; + + /** Supplemental properties in the adaptation set. */ public final List supplementalProperties; /** @@ -62,21 +63,21 @@ public class AdaptationSet { * {@code TRACK_TYPE_*} constants. * @param representations {@link Representation}s in the adaptation set. * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. + * @param essentialProperties Essential properties in the adaptation set. * @param supplementalProperties Supplemental properties in the adaptation set. */ - public AdaptationSet(int id, int type, List representations, - List accessibilityDescriptors, List supplementalProperties) { + public AdaptationSet( + int id, + int type, + List representations, + List accessibilityDescriptors, + List essentialProperties, + List supplementalProperties) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); - this.accessibilityDescriptors = - accessibilityDescriptors == null - ? Collections.emptyList() - : Collections.unmodifiableList(accessibilityDescriptors); - this.supplementalProperties = - supplementalProperties == null - ? Collections.emptyList() - : Collections.unmodifiableList(supplementalProperties); + this.accessibilityDescriptors = Collections.unmodifiableList(accessibilityDescriptors); + this.essentialProperties = Collections.unmodifiableList(essentialProperties); + this.supplementalProperties = Collections.unmodifiableList(supplementalProperties); } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 2d8909f8b4..c21af45d15 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -224,9 +224,14 @@ public class DashManifest implements FilterableManifest { key = keys.poll(); } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex); - copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, - copyRepresentations, adaptationSet.accessibilityDescriptors, - adaptationSet.supplementalProperties)); + copyAdaptationSets.add( + new AdaptationSet( + adaptationSet.id, + adaptationSet.type, + copyRepresentations, + adaptationSet.accessibilityDescriptors, + adaptationSet.essentialProperties, + adaptationSet.supplementalProperties)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 95129d68c4..6d25c50cf6 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -289,6 +289,7 @@ public class DashManifestParser extends DefaultHandler ArrayList inbandEventStreams = new ArrayList<>(); ArrayList accessibilityDescriptors = new ArrayList<>(); ArrayList roleDescriptors = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(); ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); @@ -317,6 +318,8 @@ public class DashManifestParser extends DefaultHandler audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { @@ -334,6 +337,7 @@ public class DashManifestParser extends DefaultHandler language, roleDescriptors, accessibilityDescriptors, + essentialProperties, supplementalProperties, segmentBase, periodDurationMs); @@ -369,14 +373,28 @@ public class DashManifestParser extends DefaultHandler inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, + return buildAdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, supplementalProperties); } - protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations, List accessibilityDescriptors, + protected AdaptationSet buildAdaptationSet( + int id, + int contentType, + List representations, + List accessibilityDescriptors, + List essentialProperties, List supplementalProperties) { - return new AdaptationSet(id, contentType, representations, accessibilityDescriptors, + return new AdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, supplementalProperties); } @@ -505,6 +523,7 @@ public class DashManifestParser extends DefaultHandler @Nullable String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, + List adaptationSetEssentialProperties, List adaptationSetSupplementalProperties, @Nullable SegmentBase segmentBase, long periodDurationMs) @@ -522,7 +541,9 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); - ArrayList supplementalProperties = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(adaptationSetEssentialProperties); + ArrayList supplementalProperties = + new ArrayList<>(adaptationSetSupplementalProperties); boolean seenFirstBaseUrl = false; do { @@ -555,6 +576,8 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else { @@ -576,6 +599,7 @@ public class DashManifestParser extends DefaultHandler adaptationSetRoleDescriptors, adaptationSetAccessibilityDescriptors, codecs, + essentialProperties, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); @@ -596,11 +620,14 @@ public class DashManifestParser extends DefaultHandler List roleDescriptors, List accessibilityDescriptors, @Nullable String codecs, + List essentialProperties, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors); roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); + roleFlags |= parseRoleFlagsFromProperties(essentialProperties); + roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); if (sampleMimeType != null) { if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); @@ -1233,6 +1260,18 @@ public class DashManifestParser extends DefaultHandler return result; } + @C.RoleFlags + protected int parseRoleFlagsFromProperties(List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_TRICK_PLAY; + } + } + return result; + } + @C.RoleFlags protected int parseDashRoleSchemeValue(@Nullable String value) { if (value == null) { diff --git a/library/dash/src/test/assets/sample_mpd_trick_play b/library/dash/src/test/assets/sample_mpd_trick_play new file mode 100644 index 0000000000..b35c906b5f --- /dev/null +++ b/library/dash/src/test/assets/sample_mpd_trick_play @@ -0,0 +1,32 @@ + + + + + + + + + + + https://test.com/0 + + + + + https://test.com/0 + + + + + + https://test.com/0 + + + + + + https://test.com/0 + + + + diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index f39a493e9f..53a9d854e2 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -165,6 +165,7 @@ public final class DashMediaPeriodTest { trackType, Arrays.asList(representations), /* accessibilityDescriptors= */ Collections.emptyList(), + /* essentialProperties= */ Collections.emptyList(), descriptor == null ? Collections.emptyList() : Collections.singletonList(descriptor)); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index 6e769b72e1..6b8bc8ad25 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegm import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,21 +39,21 @@ public final class DashUtilTest { @Test public void testLoadDrmInitDataFromManifest() throws Exception { - Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData()))); + Period period = newPeriod(newAdaptationSet(newRepresentations(newDrmInitData()))); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isEqualTo(newDrmInitData()); } @Test public void testLoadDrmInitDataMissing() throws Exception { - Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */))); + Period period = newPeriod(newAdaptationSet(newRepresentations(null /* no init data */))); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } @Test public void testLoadDrmInitDataNoRepresentations() throws Exception { - Period period = newPeriod(newAdaptationSets(/* no representation */ )); + Period period = newPeriod(newAdaptationSet(/* no representation */ )); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } @@ -68,8 +69,14 @@ public final class DashUtilTest { return new Period("", 0, Arrays.asList(adaptationSets)); } - private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null); + private static AdaptationSet newAdaptationSet(Representation... representations) { + return new AdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + Arrays.asList(representations), + /* accessibilityDescriptors= */ Collections.emptyList(), + /* essentialProperties= */ Collections.emptyList(), + /* supplementalProperties= */ Collections.emptyList()); } private static Representation newRepresentations(DrmInitData drmInitData) { diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 390a18d2cc..ea03770c89 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -45,6 +45,7 @@ public class DashManifestParserTest { private static final String SAMPLE_MPD_SEGMENT_TEMPLATE = "sample_mpd_segment_template"; private static final String SAMPLE_MPD_EVENT_STREAM = "sample_mpd_event_stream"; private static final String SAMPLE_MPD_LABELS = "sample_mpd_labels"; + private static final String SAMPLE_MPD_TRICK_PLAY = "sample_mpd_trick_play"; private static final String NEXT_TAG_NAME = "Next"; private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; @@ -169,7 +170,7 @@ public class DashManifestParserTest { DashManifestParser parser = new DashManifestParser(); DashManifest mpd = parser.parse( - Uri.parse("Https://example.com/test.mpd"), + Uri.parse("https://example.com/test.mpd"), TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD)); ProgramInformation expectedProgramInformation = new ProgramInformation( @@ -192,6 +193,46 @@ public class DashManifestParserTest { assertThat(adaptationSets.get(1).representations.get(0).format.label).isEqualTo("video label"); } + @Test + public void parseMediaPresentationDescription_trickPlay() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_TRICK_PLAY)); + + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + AdaptationSet adaptationSet = adaptationSets.get(0); + assertThat(adaptationSet.essentialProperties).isEmpty(); + assertThat(adaptationSet.supplementalProperties).isEmpty(); + assertThat(adaptationSet.representations.get(0).format.roleFlags).isEqualTo(0); + + adaptationSet = adaptationSets.get(1); + assertThat(adaptationSet.essentialProperties).isEmpty(); + assertThat(adaptationSet.supplementalProperties).isEmpty(); + assertThat(adaptationSet.representations.get(0).format.roleFlags).isEqualTo(0); + + adaptationSet = adaptationSets.get(2); + assertThat(adaptationSet.essentialProperties).hasSize(1); + assertThat(adaptationSet.essentialProperties.get(0).schemeIdUri) + .isEqualTo("http://dashif.org/guidelines/trickmode"); + assertThat(adaptationSet.essentialProperties.get(0).value).isEqualTo("0"); + assertThat(adaptationSet.supplementalProperties).isEmpty(); + assertThat(adaptationSet.representations.get(0).format.roleFlags) + .isEqualTo(C.ROLE_FLAG_TRICK_PLAY); + + adaptationSet = adaptationSets.get(3); + assertThat(adaptationSet.essentialProperties).isEmpty(); + assertThat(adaptationSet.supplementalProperties).hasSize(1); + assertThat(adaptationSet.supplementalProperties.get(0).schemeIdUri) + .isEqualTo("http://dashif.org/guidelines/trickmode"); + assertThat(adaptationSet.supplementalProperties.get(0).value).isEqualTo("1"); + assertThat(adaptationSet.representations.get(0).format.roleFlags) + .isEqualTo(C.ROLE_FLAG_TRICK_PLAY); + } + @Test public void parseSegmentTimeline_repeatCount() throws Exception { DashManifestParser parser = new DashManifestParser(); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index a336602965..3f3b35b5b9 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -239,6 +239,12 @@ public class DashManifestTest { } private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { - return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null); + return new AdaptationSet( + ++seed, + ++seed, + Arrays.asList(representations), + /* accessibilityDescriptors= */ Collections.emptyList(), + /* essentialProperties= */ Collections.emptyList(), + /* supplementalProperties= */ Collections.emptyList()); } } From f2d2d561096b418759454ab0fadd8c615a46d1bb Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 15 Apr 2020 18:49:16 +0100 Subject: [PATCH 05/80] Fix H265Reader Update H265Reader to output the same samples after a seek to 0. PiperOrigin-RevId: 306675050 --- .../exoplayer2/extractor/ts/H265Reader.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 88bde53746..b361e4972c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -161,9 +161,8 @@ public final class H265Reader implements ElementaryStreamReader { } private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { - if (hasOutputFormat) { - sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); - } else { + sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs, hasOutputFormat); + if (!hasOutputFormat) { vps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType); @@ -173,9 +172,8 @@ public final class H265Reader implements ElementaryStreamReader { } private void nalUnitData(byte[] dataArray, int offset, int limit) { - if (hasOutputFormat) { - sampleReader.readNalUnitData(dataArray, offset, limit); - } else { + sampleReader.readNalUnitData(dataArray, offset, limit); + if (!hasOutputFormat) { vps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit); @@ -185,9 +183,8 @@ public final class H265Reader implements ElementaryStreamReader { } private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { - if (hasOutputFormat) { - sampleReader.endNalUnit(position, offset); - } else { + sampleReader.endNalUnit(position, offset, hasOutputFormat); + if (!hasOutputFormat) { vps.endNalUnit(discardPadding); sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); @@ -427,7 +424,8 @@ public final class H265Reader implements ElementaryStreamReader { writingParameterSets = false; } - public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + public void startNalUnit( + long position, int offset, int nalUnitType, long pesTimeUs, boolean hasOutputFormat) { isFirstSlice = false; isFirstParameterSet = false; nalUnitTimeUs = pesTimeUs; @@ -437,7 +435,9 @@ public final class H265Reader implements ElementaryStreamReader { if (nalUnitType >= VPS_NUT) { if (!writingParameterSets && readingSample) { // This is a non-VCL NAL unit, so flush the previous sample. - outputSample(offset); + if (hasOutputFormat) { + outputSample(offset); + } readingSample = false; } if (nalUnitType <= PPS_NUT) { @@ -464,14 +464,14 @@ public final class H265Reader implements ElementaryStreamReader { } } - public void endNalUnit(long position, int offset) { + public void endNalUnit(long position, int offset, boolean hasOutputFormat) { if (writingParameterSets && isFirstSlice) { // This sample has parameter sets. Reset the key-frame flag based on the first slice. sampleIsKeyframe = nalUnitHasKeyframeData; writingParameterSets = false; } else if (isFirstParameterSet || isFirstSlice) { // This NAL unit is at the start of a new sample (access unit). - if (readingSample) { + if (hasOutputFormat && readingSample) { // Output the sample ending before this NAL unit. int nalUnitLength = (int) (position - nalUnitStartPosition); outputSample(offset + nalUnitLength); From 4dc1d317c3381ce6d5103f5bfca6940527cc1c5c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Apr 2020 10:29:57 +0100 Subject: [PATCH 06/80] Fix TeeAudioProcessor sink configuration TeeAudioProcessor needs to configure its sink when it is initially set up. PiperOrigin-RevId: 306808871 --- .../exoplayer2/audio/TeeAudioProcessor.java | 7 ++- .../audio/TeeAudioProcessorTest.java | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java index b6a063bd14..a9afa47198 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -79,6 +79,11 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { replaceOutputBuffer(remaining).put(inputBuffer).flip(); } + @Override + protected void onFlush() { + flushSinkIfActive(); + } + @Override protected void onQueueEndOfStream() { flushSinkIfActive(); @@ -201,7 +206,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } private void reset() throws IOException { - RandomAccessFile randomAccessFile = this.randomAccessFile; + @Nullable RandomAccessFile randomAccessFile = this.randomAccessFile; if (randomAccessFile == null) { return; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java new file mode 100644 index 0000000000..6f0a87e97b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java @@ -0,0 +1,58 @@ +/* + * 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.audio; + +import static org.mockito.Mockito.verify; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import com.google.android.exoplayer2.audio.TeeAudioProcessor.AudioBufferSink; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link TeeAudioProcessorTest}. */ +@RunWith(AndroidJUnit4.class) +public final class TeeAudioProcessorTest { + + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private TeeAudioProcessor teeAudioProcessor; + + @Mock private AudioBufferSink mockAudioBufferSink; + + @Before + public void setUp() { + teeAudioProcessor = new TeeAudioProcessor(mockAudioBufferSink); + } + + @Test + public void initialFlush_flushesSink() throws Exception { + teeAudioProcessor.configure(AUDIO_FORMAT); + teeAudioProcessor.flush(); + + verify(mockAudioBufferSink) + .flush(AUDIO_FORMAT.sampleRate, AUDIO_FORMAT.channelCount, AUDIO_FORMAT.encoding); + } +} From aea9f8e550f701478c5f0121fc6f51bb409de557 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 17 Apr 2020 10:44:42 +0100 Subject: [PATCH 07/80] Merge pull request #7245 from Clement-Jean:silence-media-source-factory PiperOrigin-RevId: 307010600 --- RELEASENOTES.md | 2 + .../exoplayer2/source/SilenceMediaSource.java | 49 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3ae572c5b2..3b5be74ebb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### Next release ### +* Add `SilenceMediaSource.Factory` to support tags + ([PR #7245](https://github.com/google/ExoPlayer/pull/7245)). * Avoid throwing an exception while parsing fragmented MP4 default sample values where the most-significant bit is set ([#7207](https://github.com/google/ExoPlayer/issues/7207)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index abaf33633e..773eba732b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -33,6 +33,42 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Media source with a single period consisting of silent raw audio of a given duration. */ public final class SilenceMediaSource extends BaseMediaSource { + /** Factory for {@link SilenceMediaSource SilenceMediaSources}. */ + public static final class Factory { + + private long durationUs; + @Nullable private Object tag; + + /** + * Sets the duration of the silent audio. + * + * @param durationUs The duration of silent audio to output, in microseconds. + * @return This factory, for convenience. + */ + public Factory setDurationUs(long durationUs) { + this.durationUs = durationUs; + return this; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + */ + public Factory setTag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + /** Creates a new {@link SilenceMediaSource}. */ + public SilenceMediaSource createMediaSource() { + return new SilenceMediaSource(durationUs, tag); + } + } + private static final int SAMPLE_RATE_HZ = 44100; @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT; private static final int CHANNEL_COUNT = 2; @@ -54,6 +90,7 @@ public final class SilenceMediaSource extends BaseMediaSource { new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024]; private final long durationUs; + @Nullable private final Object tag; /** * Creates a new media source providing silent audio of the given duration. @@ -61,15 +98,25 @@ public final class SilenceMediaSource extends BaseMediaSource { * @param durationUs The duration of silent audio to output, in microseconds. */ public SilenceMediaSource(long durationUs) { + this(durationUs, /* tag= */ null); + } + + private SilenceMediaSource(long durationUs, @Nullable Object tag) { Assertions.checkArgument(durationUs >= 0); this.durationUs = durationUs; + this.tag = tag; } @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( new SinglePeriodTimeline( - durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false)); + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag)); } @Override From ad36f649654db90bded9f8d150a128f69653b5b4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Apr 2020 13:28:27 +0100 Subject: [PATCH 08/80] Merge pull request #7210 from nebyan:CacheKeyFactoryNotUsed PiperOrigin-RevId: 307045655 --- .../exoplayer2/upstream/cache/CacheUtil.java | 18 ++++++++---------- .../upstream/cache/CacheDataSourceTest.java | 4 ---- .../upstream/cache/CacheUtilTest.java | 16 ++++------------ 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index ce16ea2439..9f1fc54462 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -109,7 +109,6 @@ public final class CacheUtil { * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. - * @param cacheKeyFactory An optional factory for cache keys. * @param upstream A {@link DataSource} for reading data not in the cache. * @param progressListener A listener to receive progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. @@ -120,7 +119,6 @@ public final class CacheUtil { public static void cache( DataSpec dataSpec, Cache cache, - @Nullable CacheKeyFactory cacheKeyFactory, DataSource upstream, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled) @@ -128,7 +126,7 @@ public final class CacheUtil { cache( dataSpec, cache, - cacheKeyFactory, + /* cacheKeyFactory= */ null, new CacheDataSource(cache, upstream), new byte[DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, @@ -139,14 +137,14 @@ public final class CacheUtil { } /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops - * early if end of input is reached and {@code enableEOFException} is false. + * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early + * if end of input is reached and {@code enableEOFException} is false. * - *

If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending - * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager. - * Please note that it's the responsibility of the calling code to call {@link - * PriorityTaskManager#add} to register with the manager before calling this method, and to call - * {@link PriorityTaskManager#remove} afterwards to unregister. + *

If a {@link PriorityTaskManager} is provided, it's used to pause and resume caching + * depending on {@code priority} and the priority of other tasks registered to the + * PriorityTaskManager. Please note that it's the responsibility of the calling code to call + * {@link PriorityTaskManager#add} to register with the manager before calling this method, and to + * call {@link PriorityTaskManager#remove} afterwards to unregister. * *

This method may be slow and shouldn't normally be called on the main thread. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 27438fcac3..8862a65db2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -365,7 +365,6 @@ public final class CacheDataSourceTest { CacheUtil.cache( unboundedDataSpec, cache, - /* cacheKeyFactory= */ null, upstream2, /* progressListener= */ null, /* isCanceled= */ null); @@ -414,7 +413,6 @@ public final class CacheDataSourceTest { CacheUtil.cache( unboundedDataSpec, cache, - /* cacheKeyFactory= */ null, upstream2, /* progressListener= */ null, /* isCanceled= */ null); @@ -438,7 +436,6 @@ public final class CacheDataSourceTest { CacheUtil.cache( dataSpec, cache, - /* cacheKeyFactory= */ null, upstream, /* progressListener= */ null, /* isCanceled= */ null); @@ -474,7 +471,6 @@ public final class CacheDataSourceTest { CacheUtil.cache( dataSpec, cache, - /* cacheKeyFactory= */ null, upstream, /* progressListener= */ null, /* isCanceled= */ null); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index 9a449b2ebd..69463bff54 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -207,7 +207,6 @@ public final class CacheUtilTest { CacheUtil.cache( new DataSpec(Uri.parse("test_data")), cache, - /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); @@ -224,8 +223,7 @@ public final class CacheUtilTest { Uri testUri = Uri.parse("test_data"); DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 20, 20); counters.reset(); @@ -233,7 +231,6 @@ public final class CacheUtilTest { CacheUtil.cache( new DataSpec(testUri), cache, - /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); @@ -251,8 +248,7 @@ public final class CacheUtilTest { DataSpec dataSpec = new DataSpec(Uri.parse("test_data")); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 100, 100); assertCachedData(cache, fakeDataSet); @@ -268,8 +264,7 @@ public final class CacheUtilTest { Uri testUri = Uri.parse("test_data"); DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 20, 20); counters.reset(); @@ -277,7 +272,6 @@ public final class CacheUtilTest { CacheUtil.cache( new DataSpec(testUri), cache, - /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); @@ -294,8 +288,7 @@ public final class CacheUtilTest { Uri testUri = Uri.parse("test_data"); DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); CachingCounters counters = new CachingCounters(); - CacheUtil.cache( - dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); + CacheUtil.cache(dataSpec, cache, dataSource, counters, /* isCanceled= */ null); counters.assertValues(0, 100, 1000); assertCachedData(cache, fakeDataSet); @@ -344,7 +337,6 @@ public final class CacheUtilTest { CacheUtil.cache( new DataSpec(Uri.parse("test_data")), cache, - /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null); From e6b5e6eb6ee5797c6a5b9b962c1516eb388a375e Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 19 Apr 2020 17:12:26 +0100 Subject: [PATCH 09/80] Merge trick play tracks into main track groups Issue: #6054 PiperOrigin-RevId: 307285068 --- RELEASENOTES.md | 5 + .../source/dash/DashMediaPeriod.java | 121 +++++++--- .../source/dash/DashMediaPeriodTest.java | 222 +++++++++++++++--- .../testutil/MediaPeriodAsserts.java | 19 +- 4 files changed, 292 insertions(+), 75 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b5be74ebb..625ed1fe77 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,11 @@ * Avoid throwing an exception while parsing fragmented MP4 default sample values where the most-significant bit is set ([#7207](https://github.com/google/ExoPlayer/issues/7207)). +* DASH: + * Merge trick play adaptation sets (i.e., adaptation sets marked with + `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as + the main adaptation sets to which they refer. Trick play tracks are + marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. ### 2.11.4 (2020-04-08) ### diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 88de84603e..fa8e5338fc 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash; import android.util.Pair; +import android.util.SparseArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -516,50 +517,94 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos); } + /** + * Groups adaptation sets. Two adaptations sets belong to the same group if either: + * + *

    + *
  • One is a trick-play adaptation set and uses a {@code + * http://dashif.org/guidelines/trickmode} essential or supplemental property to indicate + * that the other is the main adaptation set to which it corresponds. + *
  • The two adaptation sets are marked as safe for switching using {@code + * urn:mpeg:dash:adaptation-set-switching:2016} supplemental properties. + *
+ * + * @param adaptationSets The adaptation sets to merge. + * @return An array of groups, where each group is an array of adaptation set indices. + */ private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) { int adaptationSetCount = adaptationSets.size(); - SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount); + SparseIntArray adaptationSetIdToIndex = new SparseIntArray(adaptationSetCount); + List> adaptationSetGroupedIndices = new ArrayList<>(adaptationSetCount); + SparseArray> adaptationSetIndexToGroupedIndices = + new SparseArray<>(adaptationSetCount); + + // Initially make each adaptation set belong to its own group. Also build the + // adaptationSetIdToIndex map. for (int i = 0; i < adaptationSetCount; i++) { - idToIndexMap.put(adaptationSets.get(i).id, i); + adaptationSetIdToIndex.put(adaptationSets.get(i).id, i); + List initialGroup = new ArrayList<>(); + initialGroup.add(i); + adaptationSetGroupedIndices.add(initialGroup); + adaptationSetIndexToGroupedIndices.put(i, initialGroup); } - int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][]; - boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount]; - - int groupCount = 0; + // Merge adaptation set groups. for (int i = 0; i < adaptationSetCount; i++) { - if (adaptationSetUsedFlags[i]) { - // This adaptation set has already been included in a group. - continue; + int mergedGroupIndex = i; + AdaptationSet adaptationSet = adaptationSets.get(i); + + // Trick-play adaptation sets are merged with their corresponding main adaptation sets. + @Nullable + Descriptor trickPlayProperty = findTrickPlayProperty(adaptationSet.essentialProperties); + if (trickPlayProperty == null) { + // Trick-play can also be specified using a supplemental property. + trickPlayProperty = findTrickPlayProperty(adaptationSet.supplementalProperties); } - adaptationSetUsedFlags[i] = true; - Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty( - adaptationSets.get(i).supplementalProperties); - if (adaptationSetSwitchingProperty == null) { - groupedAdaptationSetIndices[groupCount++] = new int[] {i}; - } else { - String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); - int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; - adaptationSetIndices[0] = i; - int outputIndex = 1; - for (String adaptationSetId : extraAdaptationSetIds) { - int extraIndex = - idToIndexMap.get(Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1); - if (extraIndex != -1) { - adaptationSetUsedFlags[extraIndex] = true; - adaptationSetIndices[outputIndex] = extraIndex; - outputIndex++; + if (trickPlayProperty != null) { + int mainAdaptationSetId = Integer.parseInt(trickPlayProperty.value); + int mainAdaptationSetIndex = + adaptationSetIdToIndex.get(mainAdaptationSetId, /* valueIfKeyNotFound= */ -1); + if (mainAdaptationSetIndex != -1) { + mergedGroupIndex = mainAdaptationSetIndex; + } + } + + // Adaptation sets that are safe for switching are merged, using the smallest index for the + // merged group. + if (mergedGroupIndex == i) { + @Nullable + Descriptor adaptationSetSwitchingProperty = + findAdaptationSetSwitchingProperty(adaptationSet.supplementalProperties); + if (adaptationSetSwitchingProperty != null) { + String[] otherAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); + for (String adaptationSetId : otherAdaptationSetIds) { + int otherAdaptationSetId = + adaptationSetIdToIndex.get( + Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1); + if (otherAdaptationSetId != -1) { + mergedGroupIndex = Math.min(mergedGroupIndex, otherAdaptationSetId); + } } } - if (outputIndex < adaptationSetIndices.length) { - adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex); - } - groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; + } + + // Merge the groups if necessary. + if (mergedGroupIndex != i) { + List thisGroup = adaptationSetIndexToGroupedIndices.get(i); + List mergedGroup = adaptationSetIndexToGroupedIndices.get(mergedGroupIndex); + mergedGroup.addAll(thisGroup); + adaptationSetIndexToGroupedIndices.put(i, mergedGroup); + adaptationSetGroupedIndices.remove(thisGroup); } } - return groupCount < adaptationSetCount - ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices; + int[][] groupedAdaptationSetIndices = new int[adaptationSetGroupedIndices.size()][]; + for (int i = 0; i < groupedAdaptationSetIndices.length; i++) { + groupedAdaptationSetIndices[i] = Util.toArray(adaptationSetGroupedIndices.get(i)); + // Restore the original adaptation set order within each group. + Arrays.sort(groupedAdaptationSetIndices[i]); + } + return groupedAdaptationSetIndices; } /** @@ -739,9 +784,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) { + return findDescriptor(descriptors, "urn:mpeg:dash:adaptation-set-switching:2016"); + } + + @Nullable + private static Descriptor findTrickPlayProperty(List descriptors) { + return findDescriptor(descriptors, "http://dashif.org/guidelines/trickmode"); + } + + @Nullable + private static Descriptor findDescriptor(List descriptors, String schemeIdUri) { for (int i = 0; i < descriptors.size(); i++) { Descriptor descriptor = descriptors.get(i); - if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) { + if (schemeIdUri.equals(descriptor.schemeIdUri)) { return descriptor; } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java index 53a9d854e2..9e74ddde45 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; @@ -35,7 +37,6 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; -import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -43,6 +44,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; @@ -53,7 +55,7 @@ import org.robolectric.annotation.LooperMode; public final class DashMediaPeriodTest { @Test - public void getSteamKeys_isCompatibleWithDashManifestFilter() { + public void getStreamKeys_isCompatibleWithDashManifestFilter() { // Test manifest which covers various edge cases: // - Multiple periods. // - Single and multiple representations per adaptation set. @@ -61,83 +63,220 @@ public final class DashMediaPeriodTest { // - Embedded track groups. // All cases are deliberately combined in one test to catch potential indexing problems which // only occur in combination. - DashManifest testManifest = + DashManifest manifest = createDashManifest( createPeriod( createAdaptationSet( /* id= */ 0, - /* trackType= */ C.TRACK_TYPE_VIDEO, + C.TRACK_TYPE_VIDEO, /* descriptor= */ null, createVideoRepresentation(/* bitrate= */ 1000000))), createPeriod( createAdaptationSet( /* id= */ 100, - /* trackType= */ C.TRACK_TYPE_VIDEO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 103, 104), + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 103, 104), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 200000), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 400000), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 600000)), createAdaptationSet( /* id= */ 101, - /* trackType= */ C.TRACK_TYPE_AUDIO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 102), + C.TRACK_TYPE_AUDIO, + createSwitchDescriptor(/* ids...= */ 102), createAudioRepresentation(/* bitrate= */ 48000), createAudioRepresentation(/* bitrate= */ 96000)), createAdaptationSet( /* id= */ 102, - /* trackType= */ C.TRACK_TYPE_AUDIO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 101), + C.TRACK_TYPE_AUDIO, + createSwitchDescriptor(/* ids...= */ 101), createAudioRepresentation(/* bitrate= */ 256000)), createAdaptationSet( /* id= */ 103, - /* trackType= */ C.TRACK_TYPE_VIDEO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 104), + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 100, 104), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 800000), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 1000000)), createAdaptationSet( /* id= */ 104, - /* trackType= */ C.TRACK_TYPE_VIDEO, - /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 103), + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 100, 103), createVideoRepresentationWithInbandEventStream(/* bitrate= */ 2000000)), createAdaptationSet( /* id= */ 105, - /* trackType= */ C.TRACK_TYPE_TEXT, + C.TRACK_TYPE_TEXT, /* descriptor= */ null, createTextRepresentation(/* language= */ "eng")), createAdaptationSet( /* id= */ 105, - /* trackType= */ C.TRACK_TYPE_TEXT, + C.TRACK_TYPE_TEXT, /* descriptor= */ null, createTextRepresentation(/* language= */ "ger")))); - FilterableManifestMediaPeriodFactory mediaPeriodFactory = - (manifest, periodIndex) -> - new DashMediaPeriod( - /* id= */ periodIndex, - manifest, - periodIndex, - mock(DashChunkSource.Factory.class), - mock(TransferListener.class), - DrmSessionManager.getDummyDrmSessionManager(), - mock(LoadErrorHandlingPolicy.class), - new EventDispatcher() - .withParameters( - /* windowIndex= */ 0, - /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()), - /* mediaTimeOffsetMs= */ 0), - /* elapsedRealtimeOffsetMs= */ 0, - mock(LoaderErrorThrower.class), - mock(Allocator.class), - mock(CompositeSequenceableLoaderFactory.class), - mock(PlayerEmsgCallback.class)); // Ignore embedded metadata as we don't want to select primary group just to get embedded track. MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( - mediaPeriodFactory, - testManifest, + DashMediaPeriodTest::createDashMediaPeriod, + manifest, /* periodIndex= */ 1, /* ignoredMimeType= */ "application/x-emsg"); } + @Test + public void adaptationSetSwitchingProperty_mergesTrackGroups() { + DashManifest manifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 1, 2), + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + createAdaptationSet( + /* id= */ 3, + C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 300)), + createAdaptationSet( + /* id= */ 2, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 0, 1), + createVideoRepresentation(/* bitrate= */ 200), + createVideoRepresentation(/* bitrate= */ 201)), + createAdaptationSet( + /* id= */ 1, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 0, 2), + createVideoRepresentation(/* bitrate= */ 100)))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect the three adaptation sets with the switch descriptor to be merged, retaining the + // representations in their original order. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format, + adaptationSets.get(3).representations.get(0).format), + new TrackGroup(adaptationSets.get(1).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + @Test + public void trickPlayProperty_mergesTrackGroups() { + DashManifest manifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 1), + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + createAdaptationSet( + /* id= */ 1, + C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 100)), + createAdaptationSet( + /* id= */ 2, + C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 200), + createVideoRepresentation(/* bitrate= */ 201)), + createAdaptationSet( + /* id= */ 3, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 2), + createVideoRepresentation(/* bitrate= */ 300)))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect the trick play adaptation sets to be merged with the ones to which they refer, + // retaining representations in their original order. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(1).representations.get(0).format), + new TrackGroup( + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format, + adaptationSets.get(3).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + @Test + public void adaptationSetSwitchingProperty_andTrickPlayProperty_mergesTrackGroups() { + DashManifest manifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 1), + createVideoRepresentation(/* bitrate= */ 0), + createVideoRepresentation(/* bitrate= */ 1)), + createAdaptationSet( + /* id= */ 1, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 2), + createVideoRepresentation(/* bitrate= */ 100)), + createAdaptationSet( + /* id= */ 2, + C.TRACK_TYPE_VIDEO, + createSwitchDescriptor(/* ids...= */ 1), + createVideoRepresentation(/* bitrate= */ 200), + createVideoRepresentation(/* bitrate= */ 201)), + createAdaptationSet( + /* id= */ 3, + C.TRACK_TYPE_VIDEO, + createTrickPlayDescriptor(/* mainAdaptationSetId= */ 2), + createVideoRepresentation(/* bitrate= */ 300)))); + DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + + // We expect all adaptation sets to be merged into one group, retaining representations in their + // original order. + TrackGroupArray expectedTrackGroups = + new TrackGroupArray( + new TrackGroup( + adaptationSets.get(0).representations.get(0).format, + adaptationSets.get(0).representations.get(1).format, + adaptationSets.get(1).representations.get(0).format, + adaptationSets.get(2).representations.get(0).format, + adaptationSets.get(2).representations.get(1).format, + adaptationSets.get(3).representations.get(0).format)); + + MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups); + } + + private static DashMediaPeriod createDashMediaPeriod(DashManifest manifest, int periodIndex) { + return new DashMediaPeriod( + /* id= */ periodIndex, + manifest, + periodIndex, + mock(DashChunkSource.Factory.class), + mock(TransferListener.class), + DrmSessionManager.getDummyDrmSessionManager(), + mock(LoadErrorHandlingPolicy.class), + new EventDispatcher() + .withParameters( + /* windowIndex= */ 0, + /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()), + /* mediaTimeOffsetMs= */ 0), + /* elapsedRealtimeOffsetMs= */ 0, + mock(LoaderErrorThrower.class), + mock(Allocator.class), + mock(CompositeSequenceableLoaderFactory.class), + mock(PlayerEmsgCallback.class)); + } + private static DashManifest createDashManifest(Period... periods) { return new DashManifest( /* availabilityStartTimeMs= */ 0, @@ -245,6 +384,13 @@ public final class DashMediaPeriodTest { /* id= */ null); } + private static Descriptor createTrickPlayDescriptor(int mainAdaptationSetId) { + return new Descriptor( + /* schemeIdUri= */ "http://dashif.org/guidelines/trickmode", + /* value= */ Integer.toString(mainAdaptationSetId), + /* id= */ null); + } + private static Descriptor getInbandEventDescriptor() { return new Descriptor( /* schemeIdUri= */ "inBandSchemeIdUri", /* value= */ "inBandValue", /* id= */ "inBandId"); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 42fc40e72d..82f56b262e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -54,6 +54,17 @@ public final class MediaPeriodAsserts { private MediaPeriodAsserts() {} + /** + * Prepares the {@link MediaPeriod} and asserts that it provides the specified track groups. + * + * @param mediaPeriod The {@link MediaPeriod} to test. + * @param expectedGroups The expected track groups. + */ + public static void assertTrackGroups(MediaPeriod mediaPeriod, TrackGroupArray expectedGroups) { + TrackGroupArray actualGroups = prepareAndGetTrackGroups(mediaPeriod); + assertThat(actualGroups).isEqualTo(expectedGroups); + } + /** * Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with * a {@link FilterableManifest} using these stream keys. @@ -85,7 +96,7 @@ public final class MediaPeriodAsserts { int periodIndex, @Nullable String ignoredMimeType) { MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest, periodIndex); - TrackGroupArray trackGroupArray = getTrackGroups(mediaPeriod); + TrackGroupArray trackGroupArray = prepareAndGetTrackGroups(mediaPeriod); // Create test vector of query test selections: // - One selection with one track per group, two tracks or all tracks. @@ -146,7 +157,7 @@ public final class MediaPeriodAsserts { // The filtered manifest should only have one period left. MediaPeriod filteredMediaPeriod = mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0); - TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod); + TrackGroupArray filteredTrackGroupArray = prepareAndGetTrackGroups(filteredMediaPeriod); for (TrackSelection trackSelection : testSelection) { if (ignoredMimeType != null && ignoredMimeType.equals(trackSelection.getFormat(0).sampleMimeType)) { @@ -186,8 +197,8 @@ public final class MediaPeriodAsserts { return true; } - private static TrackGroupArray getTrackGroups(MediaPeriod mediaPeriod) { - AtomicReference trackGroupArray = new AtomicReference<>(null); + private static TrackGroupArray prepareAndGetTrackGroups(MediaPeriod mediaPeriod) { + AtomicReference trackGroupArray = new AtomicReference<>(); DummyMainThread dummyMainThread = new DummyMainThread(); ConditionVariable preparedCondition = new ConditionVariable(); dummyMainThread.runOnMainThread( From cf52742ad9ce27e721240177ad139feff6683826 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Apr 2020 08:15:18 +0100 Subject: [PATCH 10/80] Fix gapless playback Audio processors are now flushed twice after reconfiguration. The second flush call cleared the pending trim start bytes so transitions between tracks were no longer gapless. Fix this by removing logic to clear pending trim bytes on flush. As a result we may trim data incorrectly if there is a flush before any data has been handled for seeking to a non-zero position, but this edge case will happen rarely and the effect shouldn't be noticeable. PiperOrigin-RevId: 307344357 --- .../audio/TrimmingAudioProcessor.java | 18 ++-- .../audio/TrimmingAudioProcessorTest.java | 101 ++++++++++++++++++ 2 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index 8d84325d93..f630c267e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -155,18 +155,20 @@ import java.nio.ByteBuffer; @Override protected void onFlush() { if (reconfigurationPending) { - // This is the initial flush after reconfiguration. Prepare to trim bytes from the start/end. + // Flushing activates the new configuration, so prepare to trim bytes from the start/end. reconfigurationPending = false; endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; - } else { - // This is a flush during playback (after the initial flush). We assume this was caused by a - // seek to a non-zero position and clear pending start bytes. This assumption may be wrong (we - // may be seeking to zero), but playing data that should have been trimmed shouldn't be - // noticeable after a seek. Ideally we would check the timestamp of the first input buffer - // queued after flushing to decide whether to trim (see also [Internal: b/77292509]). - pendingTrimStartBytes = 0; } + + // TODO(internal b/77292509): Flushing occurs to activate a configuration (handled above) but + // also when seeking within a stream. This implementation currently doesn't handle seek to start + // (where we need to trim at the start again), nor seeks to non-zero positions before start + // trimming has occurred (where we should set pendingTrimStartBytes to zero). These cases can be + // fixed by trimming in queueInput based on timestamp, once that information is available. + + // Any data in the end buffer should no longer be output if we are playing from a different + // position, so discard it and refill the buffer using new input. endBufferSize = 0; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java new file mode 100644 index 0000000000..19a1ad19c3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java @@ -0,0 +1,101 @@ +/* + * 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.audio; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; +import java.nio.ByteBuffer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link TrimmingAudioProcessor}. */ +@RunWith(AndroidJUnit4.class) +public final class TrimmingAudioProcessorTest { + + private static final AudioFormat AUDIO_FORMAT = + new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT); + private static final int TRACK_ONE_UNTRIMMED_FRAME_COUNT = 1024; + private static final int TRACK_ONE_TRIM_START_FRAME_COUNT = 64; + private static final int TRACK_ONE_TRIM_END_FRAME_COUNT = 32; + private static final int TRACK_TWO_TRIM_START_FRAME_COUNT = 128; + private static final int TRACK_TWO_TRIM_END_FRAME_COUNT = 16; + + private static final int TRACK_ONE_BUFFER_SIZE_BYTES = + AUDIO_FORMAT.bytesPerFrame * TRACK_ONE_UNTRIMMED_FRAME_COUNT; + private static final int TRACK_ONE_TRIMMED_BUFFER_SIZE_BYTES = + TRACK_ONE_BUFFER_SIZE_BYTES + - AUDIO_FORMAT.bytesPerFrame + * (TRACK_ONE_TRIM_START_FRAME_COUNT + TRACK_ONE_TRIM_END_FRAME_COUNT); + + private TrimmingAudioProcessor trimmingAudioProcessor; + + @Before + public void setUp() { + trimmingAudioProcessor = new TrimmingAudioProcessor(); + } + + @After + public void tearDown() { + trimmingAudioProcessor.reset(); + } + + @Test + public void flushTwice_trimsStartAndEnd() throws Exception { + trimmingAudioProcessor.setTrimFrameCount( + TRACK_ONE_TRIM_START_FRAME_COUNT, TRACK_ONE_TRIM_END_FRAME_COUNT); + trimmingAudioProcessor.configure(AUDIO_FORMAT); + trimmingAudioProcessor.flush(); + trimmingAudioProcessor.flush(); + + int outputSizeBytes = feedAndDrainAudioProcessorToEndOfTrackOne(); + + assertThat(trimmingAudioProcessor.getTrimmedFrameCount()) + .isEqualTo(TRACK_ONE_TRIM_START_FRAME_COUNT + TRACK_ONE_TRIM_END_FRAME_COUNT); + assertThat(outputSizeBytes).isEqualTo(TRACK_ONE_TRIMMED_BUFFER_SIZE_BYTES); + } + + /** + * Feeds and drains the audio processor up to the end of track one, returning the total output + * size in bytes. + */ + private int feedAndDrainAudioProcessorToEndOfTrackOne() throws Exception { + // Feed and drain the processor, simulating a gapless transition to another track. + ByteBuffer inputBuffer = ByteBuffer.allocate(TRACK_ONE_BUFFER_SIZE_BYTES); + int outputSize = 0; + while (!trimmingAudioProcessor.isEnded()) { + if (inputBuffer.hasRemaining()) { + trimmingAudioProcessor.queueInput(inputBuffer); + if (!inputBuffer.hasRemaining()) { + // Reconfigure for a next track then begin draining. + trimmingAudioProcessor.setTrimFrameCount( + TRACK_TWO_TRIM_START_FRAME_COUNT, TRACK_TWO_TRIM_END_FRAME_COUNT); + trimmingAudioProcessor.configure(AUDIO_FORMAT); + trimmingAudioProcessor.queueEndOfStream(); + } + } + ByteBuffer outputBuffer = trimmingAudioProcessor.getOutput(); + outputSize += outputBuffer.remaining(); + outputBuffer.clear(); + } + trimmingAudioProcessor.reset(); + return outputSize; + } +} From e7e74afbff7eb35ef31d37c23c41c86ad4e31467 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Apr 2020 11:15:53 +0100 Subject: [PATCH 11/80] Fix AdsMediaSource child sources not being released Also add unit tests for AdsMediaSource. PiperOrigin-RevId: 307365492 --- RELEASENOTES.md | 1 + .../google/android/exoplayer2/Timeline.java | 117 ++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 186 ++++++++------- .../source/ads/AdsMediaSourceTest.java | 216 ++++++++++++++++++ 4 files changed, 435 insertions(+), 85 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 625ed1fe77..37f7d884f8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,7 @@ * Avoid throwing an exception while parsing fragmented MP4 default sample values where the most-significant bit is set ([#7207](https://github.com/google/ExoPlayer/issues/7207)). +* Fix `AdsMediaSource` child `MediaSource`s not being released. * DASH: * Merge trick play adaptation sets (i.e., adaptation sets marked with `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 7423320d8b..93a87da0dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -19,6 +19,7 @@ import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; /** * A flexible representation of the structure of media. A timeline is able to represent the @@ -278,6 +279,48 @@ public abstract class Timeline { return positionInFirstPeriodUs; } + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + Window that = (Window) obj; + return Util.areEqual(uid, that.uid) + && Util.areEqual(tag, that.tag) + && Util.areEqual(manifest, that.manifest) + && presentationStartTimeMs == that.presentationStartTimeMs + && windowStartTimeMs == that.windowStartTimeMs + && isSeekable == that.isSeekable + && isDynamic == that.isDynamic + && isLive == that.isLive + && defaultPositionUs == that.defaultPositionUs + && durationUs == that.durationUs + && firstPeriodIndex == that.firstPeriodIndex + && lastPeriodIndex == that.lastPeriodIndex + && positionInFirstPeriodUs == that.positionInFirstPeriodUs; + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + uid.hashCode(); + result = 31 * result + (tag == null ? 0 : tag.hashCode()); + result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); + result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); + result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); + result = 31 * result + (isSeekable ? 1 : 0); + result = 31 * result + (isDynamic ? 1 : 0); + result = 31 * result + (isLive ? 1 : 0); + result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + firstPeriodIndex; + result = 31 * result + lastPeriodIndex; + result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); + return result; + } } /** @@ -534,6 +577,34 @@ public abstract class Timeline { return adPlaybackState.adResumePositionUs; } + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + Period that = (Period) obj; + return Util.areEqual(id, that.id) + && Util.areEqual(uid, that.uid) + && windowIndex == that.windowIndex + && durationUs == that.durationUs + && positionInWindowUs == that.positionInWindowUs + && Util.areEqual(adPlaybackState, that.adPlaybackState); + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + (id == null ? 0 : id.hashCode()); + result = 31 * result + (uid == null ? 0 : uid.hashCode()); + result = 31 * result + windowIndex; + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); + result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode()); + return result; + } } /** An empty timeline. */ @@ -834,4 +905,50 @@ public abstract class Timeline { * @return The unique id of the period. */ public abstract Object getUidOfPeriod(int periodIndex); + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Timeline)) { + return false; + } + Timeline other = (Timeline) obj; + if (other.getWindowCount() != getWindowCount() || other.getPeriodCount() != getPeriodCount()) { + return false; + } + Timeline.Window window = new Timeline.Window(); + Timeline.Period period = new Timeline.Period(); + Timeline.Window otherWindow = new Timeline.Window(); + Timeline.Period otherPeriod = new Timeline.Period(); + for (int i = 0; i < getWindowCount(); i++) { + if (!getWindow(i, window).equals(other.getWindow(i, otherWindow))) { + return false; + } + } + for (int i = 0; i < getPeriodCount(); i++) { + if (!getPeriod(i, period, /* setIds= */ true) + .equals(other.getPeriod(i, otherPeriod, /* setIds= */ true))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + Window window = new Window(); + Period period = new Period(); + int result = 7; + result = 31 * result + getWindowCount(); + for (int i = 0; i < getWindowCount(); i++) { + result = 31 * result + getWindow(i, window).hashCode(); + } + result = 31 * result + getPeriodCount(); + for (int i = 0; i < getPeriodCount(); i++) { + result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode(); + } + return result; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 5e22de4320..34f0d496a2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -44,10 +44,9 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source @@ -128,15 +127,13 @@ public final class AdsMediaSource extends CompositeMediaSource { private final AdsLoader adsLoader; private final AdsLoader.AdViewProvider adViewProvider; private final Handler mainHandler; - private final Map> maskingMediaPeriodByAdMediaSource; private final Timeline.Period period; // Accessed on the player thread. @Nullable private ComponentListener componentListener; @Nullable private Timeline contentTimeline; @Nullable private AdPlaybackState adPlaybackState; - private @NullableType MediaSource[][] adGroupMediaSources; - private @NullableType Timeline[][] adGroupTimelines; + private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -178,10 +175,8 @@ public final class AdsMediaSource extends CompositeMediaSource { this.adsLoader = adsLoader; this.adViewProvider = adViewProvider; mainHandler = new Handler(Looper.getMainLooper()); - maskingMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); - adGroupMediaSources = new MediaSource[0][]; - adGroupTimelines = new Timeline[0][]; + adMediaSourceHolders = new AdMediaSourceHolder[0][]; adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @@ -208,36 +203,21 @@ public final class AdsMediaSource extends CompositeMediaSource { int adIndexInAdGroup = id.adIndexInAdGroup; Uri adUri = Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]); - if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + if (adMediaSourceHolders[adGroupIndex].length <= adIndexInAdGroup) { int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); + adMediaSourceHolders[adGroupIndex] = + Arrays.copyOf(adMediaSourceHolders[adGroupIndex], adCount); } - MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - if (mediaSource == null) { - mediaSource = adMediaSourceFactory.createMediaSource(adUri); - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource; - maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>()); - prepareChildSource(id, mediaSource); + @Nullable + AdMediaSourceHolder adMediaSourceHolder = + adMediaSourceHolders[adGroupIndex][adIndexInAdGroup]; + if (adMediaSourceHolder == null) { + MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); + adMediaSourceHolder = new AdMediaSourceHolder(adMediaSource); + adMediaSourceHolders[adGroupIndex][adIndexInAdGroup] = adMediaSourceHolder; + prepareChildSource(id, adMediaSource); } - MaskingMediaPeriod maskingMediaPeriod = - new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); - maskingMediaPeriod.setPrepareErrorListener( - new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); - List mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource); - if (mediaPeriods == null) { - Object periodUid = - Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup]) - .getUidOfPeriod(/* periodIndex= */ 0); - MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); - maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); - } else { - // Keep track of the masking media period so it can be populated with the real media period - // when the source's info becomes available. - mediaPeriods.add(maskingMediaPeriod); - } - return maskingMediaPeriod; + return adMediaSourceHolder.createMediaPeriod(adUri, id, allocator, startPositionUs); } else { MaskingMediaPeriod mediaPeriod = new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs); @@ -249,12 +229,18 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod; - List mediaPeriods = - maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource); - if (mediaPeriods != null) { - mediaPeriods.remove(maskingMediaPeriod); + MediaPeriodId id = maskingMediaPeriod.id; + if (id.isAd()) { + AdMediaSourceHolder adMediaSourceHolder = + Assertions.checkNotNull(adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup]); + adMediaSourceHolder.releaseMediaPeriod(maskingMediaPeriod); + if (adMediaSourceHolder.isInactive()) { + releaseChildSource(id); + adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup] = null; + } + } else { + maskingMediaPeriod.releasePeriod(); } - maskingMediaPeriod.releasePeriod(); } @Override @@ -262,11 +248,9 @@ public final class AdsMediaSource extends CompositeMediaSource { super.releaseSourceInternal(); Assertions.checkNotNull(componentListener).release(); componentListener = null; - maskingMediaPeriodByAdMediaSource.clear(); contentTimeline = null; adPlaybackState = null; - adGroupMediaSources = new MediaSource[0][]; - adGroupTimelines = new Timeline[0][]; + adMediaSourceHolders = new AdMediaSourceHolder[0][]; mainHandler.post(adsLoader::stop); } @@ -276,10 +260,13 @@ public final class AdsMediaSource extends CompositeMediaSource { if (mediaPeriodId.isAd()) { int adGroupIndex = mediaPeriodId.adGroupIndex; int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup; - onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline); + Assertions.checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup]) + .handleSourceInfoRefresh(timeline); } else { - onContentSourceInfoRefreshed(timeline); + Assertions.checkArgument(timeline.getPeriodCount() == 1); + contentTimeline = timeline; } + maybeUpdateSourceInfo(); } @Override @@ -294,42 +281,17 @@ public final class AdsMediaSource extends CompositeMediaSource { private void onAdPlaybackState(AdPlaybackState adPlaybackState) { if (this.adPlaybackState == null) { - adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupMediaSources, new MediaSource[0]); - adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupTimelines, new Timeline[0]); + adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][]; + Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]); } this.adPlaybackState = adPlaybackState; maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - contentTimeline = timeline; - maybeUpdateSourceInfo(); - } - - private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, - int adIndexInAdGroup, Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline; - List mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource); - if (mediaPeriods != null) { - Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); - for (int i = 0; i < mediaPeriods.size(); i++) { - MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i); - MediaPeriodId adSourceMediaPeriodId = - new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); - mediaPeriod.createPeriod(adSourceMediaPeriodId); - } - } - maybeUpdateSourceInfo(); - } - private void maybeUpdateSourceInfo() { - Timeline contentTimeline = this.contentTimeline; + @Nullable Timeline contentTimeline = this.contentTimeline; if (adPlaybackState != null && contentTimeline != null) { - adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); + adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs()); Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline @@ -338,19 +300,16 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - private static long[][] getAdDurations( - @NullableType Timeline[][] adTimelines, Timeline.Period period) { - long[][] adDurations = new long[adTimelines.length][]; - for (int i = 0; i < adTimelines.length; i++) { - adDurations[i] = new long[adTimelines[i].length]; - for (int j = 0; j < adTimelines[i].length; j++) { - adDurations[i][j] = - adTimelines[i][j] == null - ? C.TIME_UNSET - : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + private long[][] getAdDurationsUs() { + long[][] adDurationsUs = new long[adMediaSourceHolders.length][]; + for (int i = 0; i < adMediaSourceHolders.length; i++) { + adDurationsUs[i] = new long[adMediaSourceHolders[i].length]; + for (int j = 0; j < adMediaSourceHolders[i].length; j++) { + @Nullable AdMediaSourceHolder holder = adMediaSourceHolders[i][j]; + adDurationsUs[i][j] = holder == null ? C.TIME_UNSET : holder.getDurationUs(); } } - return adDurations; + return adDurationsUs; } /** Listener for component events. All methods are called on the main thread. */ @@ -436,4 +395,61 @@ public final class AdsMediaSource extends CompositeMediaSource { () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception)); } } + + private final class AdMediaSourceHolder { + + private final MediaSource adMediaSource; + private final List activeMediaPeriods; + + @MonotonicNonNull private Timeline timeline; + + public AdMediaSourceHolder(MediaSource adMediaSource) { + this.adMediaSource = adMediaSource; + activeMediaPeriods = new ArrayList<>(); + } + + public MediaPeriod createMediaPeriod( + Uri adUri, MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod maskingMediaPeriod = + new MaskingMediaPeriod(adMediaSource, id, allocator, startPositionUs); + maskingMediaPeriod.setPrepareErrorListener( + new AdPrepareErrorListener(adUri, id.adGroupIndex, id.adIndexInAdGroup)); + activeMediaPeriods.add(maskingMediaPeriod); + if (timeline != null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); + maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); + } + return maskingMediaPeriod; + } + + public void handleSourceInfoRefresh(Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + if (this.timeline == null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + for (int i = 0; i < activeMediaPeriods.size(); i++) { + MaskingMediaPeriod mediaPeriod = activeMediaPeriods.get(i); + MediaPeriodId adSourceMediaPeriodId = + new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); + mediaPeriod.createPeriod(adSourceMediaPeriodId); + } + } + this.timeline = timeline; + } + + public long getDurationUs() { + return timeline == null + ? C.TIME_UNSET + : timeline.getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + } + + public void releaseMediaPeriod(MaskingMediaPeriod maskingMediaPeriod) { + activeMediaPeriods.remove(maskingMediaPeriod); + maskingMediaPeriod.releasePeriod(); + } + + public boolean isInactive() { + return activeMediaPeriods.isEmpty(); + } + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java new file mode 100644 index 0000000000..77eb628f28 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java @@ -0,0 +1,216 @@ +/* + * 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.source.ads; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; + +import android.net.Uri; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; +import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.LooperMode; + +/** Unit tests for {@link AdsMediaSource}. */ +@RunWith(AndroidJUnit4.class) +@LooperMode(PAUSED) +public final class AdsMediaSourceTest { + + private static final long PREROLL_AD_DURATION_US = 10 * C.MICROS_PER_SECOND; + private static final Timeline PREROLL_AD_TIMELINE = + new SinglePeriodTimeline( + PREROLL_AD_DURATION_US, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false); + private static final Object PREROLL_AD_PERIOD_UID = + PREROLL_AD_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0); + + private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND; + private static final Timeline CONTENT_TIMELINE = + new SinglePeriodTimeline( + CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); + private static final Object CONTENT_PERIOD_UID = + CONTENT_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0); + + private static final AdPlaybackState AD_PLAYBACK_STATE = + new AdPlaybackState(/* adGroupTimesUs...= */ 0) + .withContentDurationUs(CONTENT_DURATION_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withAdResumePositionUs(/* adResumePositionUs= */ 0); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private FakeMediaSource contentMediaSource; + private FakeMediaSource prerollAdMediaSource; + @Mock private MediaSourceCaller mockMediaSourceCaller; + private AdsMediaSource adsMediaSource; + + @Before + public void setUp() { + // Set up content and ad media sources, passing a null timeline so tests can simulate setting it + // later. + contentMediaSource = new FakeMediaSource(/* timeline= */ null); + prerollAdMediaSource = new FakeMediaSource(/* timeline= */ null); + MediaSourceFactory adMediaSourceFactory = mock(MediaSourceFactory.class); + when(adMediaSourceFactory.createMediaSource(any(Uri.class))).thenReturn(prerollAdMediaSource); + + // Prepare the AdsMediaSource and capture its ads loader listener. + AdsLoader mockAdsLoader = mock(AdsLoader.class); + AdViewProvider mockAdViewProvider = mock(AdViewProvider.class); + ArgumentCaptor eventListenerArgumentCaptor = + ArgumentCaptor.forClass(AdsLoader.EventListener.class); + adsMediaSource = + new AdsMediaSource( + contentMediaSource, adMediaSourceFactory, mockAdsLoader, mockAdViewProvider); + adsMediaSource.prepareSource(mockMediaSourceCaller, /* mediaTransferListener= */ null); + shadowOf(Looper.getMainLooper()).idle(); + verify(mockAdsLoader).start(eventListenerArgumentCaptor.capture(), eq(mockAdViewProvider)); + + // Simulate loading a preroll ad. + AdsLoader.EventListener adsLoaderEventListener = eventListenerArgumentCaptor.getValue(); + adsLoaderEventListener.onAdPlaybackState(AD_PLAYBACK_STATE); + shadowOf(Looper.getMainLooper()).idle(); + } + + @Test + public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfo() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE, null); + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(prerollAdMediaSource.isPrepared()).isTrue(); + verify(mockMediaSourceCaller) + .onSourceInfoRefreshed( + adsMediaSource, new SinglePeriodAdTimeline(CONTENT_TIMELINE, AD_PLAYBACK_STATE)); + } + + @Test + public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfoWithAdMediaSourceInfo() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE, null); + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE, null); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mockMediaSourceCaller) + .onSourceInfoRefreshed( + adsMediaSource, + new SinglePeriodAdTimeline( + CONTENT_TIMELINE, + AD_PLAYBACK_STATE.withAdDurationsUs(new long[][] {{PREROLL_AD_DURATION_US}}))); + } + + @Test + public void createPeriod_createsChildPrerollAdMediaPeriod() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE, null); + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE, null); + shadowOf(Looper.getMainLooper()).idle(); + + prerollAdMediaSource.assertMediaPeriodCreated( + new MediaPeriodId(PREROLL_AD_PERIOD_UID, /* windowSequenceNumber= */ 0)); + } + + @Test + public void createPeriod_createsChildContentMediaPeriod() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE, null); + shadowOf(Looper.getMainLooper()).idle(); + adsMediaSource.createPeriod( + new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + + contentMediaSource.assertMediaPeriodCreated( + new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0)); + } + + @Test + public void releasePeriod_releasesChildMediaPeriodsAndSources() { + contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE, null); + MediaPeriod prerollAdMediaPeriod = + adsMediaSource.createPeriod( + new MediaPeriodId( + CONTENT_PERIOD_UID, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE, null); + shadowOf(Looper.getMainLooper()).idle(); + MediaPeriod contentMediaPeriod = + adsMediaSource.createPeriod( + new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0), + mock(Allocator.class), + /* startPositionUs= */ 0); + adsMediaSource.releasePeriod(prerollAdMediaPeriod); + + prerollAdMediaSource.assertReleased(); + + adsMediaSource.releasePeriod(contentMediaPeriod); + adsMediaSource.releaseSource(mockMediaSourceCaller); + shadowOf(Looper.getMainLooper()).idle(); + prerollAdMediaSource.assertReleased(); + contentMediaSource.assertReleased(); + } +} From 190d81f0f6994a691847ff5a209a4aa213366669 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Apr 2020 12:54:52 +0100 Subject: [PATCH 12/80] Noop naming generalization for H265Reader This change generalizes the concept of "reading parameter sets" to "reading prefix NAL units", ahead of a change that will treat AUD and suffix SEI NAL units in the same way. The change also introduces some static isXxxNalUnit methods for clarity. Issue: #7113 PiperOrigin-RevId: 307376967 --- .../exoplayer2/extractor/ts/H265Reader.java | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index b361e4972c..97175a392f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -59,7 +59,7 @@ public final class H265Reader implements ElementaryStreamReader { private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer prefixSei; - private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? + private final NalUnitTargetBuffer suffixSei; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -397,17 +397,17 @@ public final class H265Reader implements ElementaryStreamReader { private final TrackOutput output; // Per NAL unit state. A sample consists of one or more NAL units. - private long nalUnitStartPosition; + private long nalUnitPosition; private boolean nalUnitHasKeyframeData; private int nalUnitBytesRead; private long nalUnitTimeUs; private boolean lookingForFirstSliceFlag; private boolean isFirstSlice; - private boolean isFirstParameterSet; + private boolean isFirstPrefixNalUnit; // Per sample state that gets reset at the start of each sample. private boolean readingSample; - private boolean writingParameterSets; + private boolean readingPrefix; private long samplePosition; private long sampleTimeUs; private boolean sampleIsKeyframe; @@ -419,31 +419,29 @@ public final class H265Reader implements ElementaryStreamReader { public void reset() { lookingForFirstSliceFlag = false; isFirstSlice = false; - isFirstParameterSet = false; + isFirstPrefixNalUnit = false; readingSample = false; - writingParameterSets = false; + readingPrefix = false; } public void startNalUnit( long position, int offset, int nalUnitType, long pesTimeUs, boolean hasOutputFormat) { isFirstSlice = false; - isFirstParameterSet = false; + isFirstPrefixNalUnit = false; nalUnitTimeUs = pesTimeUs; nalUnitBytesRead = 0; - nalUnitStartPosition = position; + nalUnitPosition = position; - if (nalUnitType >= VPS_NUT) { - if (!writingParameterSets && readingSample) { - // This is a non-VCL NAL unit, so flush the previous sample. + if (!isVclBodyNalUnit(nalUnitType)) { + if (readingSample && !readingPrefix) { if (hasOutputFormat) { outputSample(offset); } readingSample = false; } - if (nalUnitType <= PPS_NUT) { - // This sample will have parameter sets at the start. - isFirstParameterSet = !writingParameterSets; - writingParameterSets = true; + if (isPrefixNalUnit(nalUnitType)) { + isFirstPrefixNalUnit = !readingPrefix; + readingPrefix = true; } } @@ -465,30 +463,40 @@ public final class H265Reader implements ElementaryStreamReader { } public void endNalUnit(long position, int offset, boolean hasOutputFormat) { - if (writingParameterSets && isFirstSlice) { + if (readingPrefix && isFirstSlice) { // This sample has parameter sets. Reset the key-frame flag based on the first slice. sampleIsKeyframe = nalUnitHasKeyframeData; - writingParameterSets = false; - } else if (isFirstParameterSet || isFirstSlice) { + readingPrefix = false; + } else if (isFirstPrefixNalUnit || isFirstSlice) { // This NAL unit is at the start of a new sample (access unit). if (hasOutputFormat && readingSample) { // Output the sample ending before this NAL unit. - int nalUnitLength = (int) (position - nalUnitStartPosition); + int nalUnitLength = (int) (position - nalUnitPosition); outputSample(offset + nalUnitLength); } - samplePosition = nalUnitStartPosition; + samplePosition = nalUnitPosition; sampleTimeUs = nalUnitTimeUs; - readingSample = true; sampleIsKeyframe = nalUnitHasKeyframeData; + readingSample = true; } } private void outputSample(int offset) { @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (nalUnitStartPosition - samplePosition); + int size = (int) (nalUnitPosition - samplePosition); output.sampleMetadata(sampleTimeUs, flags, size, offset, null); } - } + /** Returns whether a NAL unit type is one that occurs before any VCL NAL units in a sample. */ + private static boolean isPrefixNalUnit(int nalUnitType) { + // TODO: Include AUD_NUT and PREFIX_SEI_NUT + return VPS_NUT <= nalUnitType && nalUnitType <= PPS_NUT; + } + /** Returns whether a NAL unit type is one that occurs in the VLC body of a sample. */ + private static boolean isVclBodyNalUnit(int nalUnitType) { + // TODO: Include SUFFIX_SEI_NUT + return nalUnitType < VPS_NUT; + } + } } From a697905cfb95a97a1ff467d94ec6879d1ed89bab Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Apr 2020 13:25:43 +0100 Subject: [PATCH 13/80] Fix H265Reader to correctly output SEI and AUD NAL units Issue: #7113 PiperOrigin-RevId: 307380133 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/extractor/ts/H265Reader.java | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 37f7d884f8..fef940b552 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as the main adaptation sets to which they refer. Trick play tracks are marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. +* MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 + samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). ### 2.11.4 (2020-04-08) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 97175a392f..b4007ea4a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -41,6 +41,7 @@ public final class H265Reader implements ElementaryStreamReader { private static final int VPS_NUT = 32; private static final int SPS_NUT = 33; private static final int PPS_NUT = 34; + private static final int AUD_NUT = 35; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; @@ -445,7 +446,7 @@ public final class H265Reader implements ElementaryStreamReader { } } - // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp. + // Look for the first slice flag if this NAL unit contains a slice_segment_layer_rbsp. nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT); lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R; } @@ -489,14 +490,12 @@ public final class H265Reader implements ElementaryStreamReader { /** Returns whether a NAL unit type is one that occurs before any VCL NAL units in a sample. */ private static boolean isPrefixNalUnit(int nalUnitType) { - // TODO: Include AUD_NUT and PREFIX_SEI_NUT - return VPS_NUT <= nalUnitType && nalUnitType <= PPS_NUT; + return (VPS_NUT <= nalUnitType && nalUnitType <= AUD_NUT) || nalUnitType == PREFIX_SEI_NUT; } /** Returns whether a NAL unit type is one that occurs in the VLC body of a sample. */ private static boolean isVclBodyNalUnit(int nalUnitType) { - // TODO: Include SUFFIX_SEI_NUT - return nalUnitType < VPS_NUT; + return nalUnitType < VPS_NUT || nalUnitType == SUFFIX_SEI_NUT; } } } From d9703358acf47db9180d9b4bc5d225801d24b959 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 20 Apr 2020 17:11:13 +0100 Subject: [PATCH 14/80] Use anti-aliasing and bitmap filtering for bitmap subtitles issue:#6950 PiperOrigin-RevId: 307411067 --- RELEASENOTES.md | 4 +++ .../exoplayer2/ui/SubtitlePainter.java | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fef940b552..742b2828e7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD # Release notes # ### Next release ### @@ -15,6 +16,9 @@ marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. * MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). +* Text + * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles + ([#6950](https://github.com/google/ExoPlayer/pull/6950)). ### 2.11.4 (2020-04-08) ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 76768804df..714d40ff9a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -64,7 +64,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final float spacingAdd; private final TextPaint textPaint; - private final Paint paint; + private final Paint windowPaint; + private final Paint bitmapPaint; // Previous input variables. @Nullable private CharSequence cueText; @@ -122,9 +123,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; textPaint.setAntiAlias(true); textPaint.setSubpixelText(true); - paint = new Paint(); - paint.setAntiAlias(true); - paint.setStyle(Style.FILL); + windowPaint = new Paint(); + windowPaint.setAntiAlias(true); + windowPaint.setStyle(Style.FILL); + + bitmapPaint = new Paint(); + bitmapPaint.setAntiAlias(true); + bitmapPaint.setFilterBitmap(true); } /** @@ -415,9 +420,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; canvas.translate(textLeft, textTop); if (Color.alpha(windowColor) > 0) { - paint.setColor(windowColor); - canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(), - paint); + windowPaint.setColor(windowColor); + canvas.drawRect( + -textPaddingX, + 0, + textLayout.getWidth() + textPaddingX, + textLayout.getHeight(), + windowPaint); } if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { @@ -451,7 +460,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull({"cueBitmap", "bitmapRect"}) private void drawBitmapLayout(Canvas canvas) { - canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, /* paint= */ null); + canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, bitmapPaint); } /** From b954a5aa5f38fe69abbb58313125dafc54267842 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Apr 2020 18:22:35 +0100 Subject: [PATCH 15/80] Fix timestamp rounding error in fMP4 extractor. The sample timestamps are currently rounded to milliseconds, only to be multiplied by 1000 later. This causes rounding errors where the sample timestamps don't match the timestamps in the seek table (which are already in microseconds). issue:#7086 PiperOrigin-RevId: 307630559 --- .../extractor/mp4/FragmentedMp4Extractor.java | 25 ++- .../extractor/mp4/TrackFragment.java | 26 +-- .../mp4/sample_ac4_fragmented.mp4.0.dump | 2 +- .../mp4/sample_ac4_fragmented.mp4.1.dump | 2 +- .../mp4/sample_ac4_fragmented.mp4.2.dump | 2 +- .../mp4/sample_ac4_protected.mp4.0.dump | 2 +- .../mp4/sample_ac4_protected.mp4.1.dump | 2 +- .../mp4/sample_ac4_protected.mp4.2.dump | 2 +- .../assets/mp4/sample_fragmented.mp4.0.dump | 150 +++++++++--------- .../mp4/sample_fragmented_seekable.mp4.0.dump | 150 +++++++++--------- .../mp4/sample_fragmented_seekable.mp4.1.dump | 122 +++++++------- .../mp4/sample_fragmented_seekable.mp4.2.dump | 92 +++++------ .../mp4/sample_fragmented_seekable.mp4.3.dump | 62 ++++---- .../mp4/sample_fragmented_sei.mp4.0.dump | 150 +++++++++--------- 14 files changed, 395 insertions(+), 394 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 42aeab64b3..c0d1581c39 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -962,20 +962,20 @@ public class FragmentedMp4Extractor implements Extractor { // Offset to the entire video timeline. In the presence of B-frames this is usually used to // ensure that the first frame's presentation timestamp is zero. - long edtsOffset = 0; + long edtsOffsetUs = 0; // Currently we only support a single edit that moves the entire media timeline (indicated by // duration == 0). Other uses of edit lists are uncommon and unsupported. if (track.editListDurations != null && track.editListDurations.length == 1 && track.editListDurations[0] == 0) { - edtsOffset = + edtsOffsetUs = Util.scaleLargeTimestamp( - track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); + track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; - int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; - long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + int[] sampleCompositionTimeOffsetUsTable = fragment.sampleCompositionTimeOffsetUsTable; + long[] sampleDecodingTimeUsTable = fragment.sampleDecodingTimeUsTable; boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO @@ -999,13 +999,13 @@ public class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = - (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale); + sampleCompositionTimeOffsetUsTable[i] = + (int) ((sampleOffset * C.MICROS_PER_SECOND) / timescale); } else { - sampleCompositionTimeOffsetTable[i] = 0; + sampleCompositionTimeOffsetUsTable[i] = 0; } - sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; + sampleDecodingTimeUsTable[i] = + Util.scaleLargeTimestamp(cumulativeTime, C.MICROS_PER_SECOND, timescale) - edtsOffsetUs; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); @@ -1291,7 +1291,7 @@ public class FragmentedMp4Extractor implements Extractor { Track track = currentTrackBundle.track; TrackOutput output = currentTrackBundle.output; int sampleIndex = currentTrackBundle.currentSampleIndex; - long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + long sampleTimeUs = fragment.getSamplePresentationTimeUs(sampleIndex); if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -1535,10 +1535,9 @@ public class FragmentedMp4Extractor implements Extractor { * @param timeUs The seek time, in microseconds. */ public void seek(long timeUs) { - long timeMs = C.usToMs(timeUs); int searchIndex = currentSampleIndex; while (searchIndex < fragment.sampleCount - && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + && fragment.getSamplePresentationTimeUs(searchIndex) < timeUs) { if (fragment.sampleIsSyncFrameTable[searchIndex]) { firstSampleToOutputIndex = searchIndex; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java index 51ec2bf282..0272e8e338 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -60,14 +60,10 @@ import java.io.IOException; * The size of each sample in the fragment. */ public int[] sampleSizeTable; - /** - * The composition time offset of each sample in the fragment. - */ - public int[] sampleCompositionTimeOffsetTable; - /** - * The decoding time of each sample in the fragment. - */ - public long[] sampleDecodingTimeTable; + /** The composition time offset of each sample in the fragment, in microseconds. */ + public int[] sampleCompositionTimeOffsetUsTable; + /** The decoding time of each sample in the fragment, in microseconds. */ + public long[] sampleDecodingTimeUsTable; /** * Indicates which samples are sync frames. */ @@ -139,8 +135,8 @@ import java.io.IOException; // likely. The choice of 25% is relatively arbitrary. int tableSize = (sampleCount * 125) / 100; sampleSizeTable = new int[tableSize]; - sampleCompositionTimeOffsetTable = new int[tableSize]; - sampleDecodingTimeTable = new long[tableSize]; + sampleCompositionTimeOffsetUsTable = new int[tableSize]; + sampleDecodingTimeUsTable = new long[tableSize]; sampleIsSyncFrameTable = new boolean[tableSize]; sampleHasSubsampleEncryptionTable = new boolean[tableSize]; } @@ -186,8 +182,14 @@ import java.io.IOException; sampleEncryptionDataNeedsFill = false; } - public long getSamplePresentationTime(int index) { - return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; + /** + * Returns the sample presentation timestamp in microseconds. + * + * @param index The sample index. + * @return The presentation timestamps of this sample in microseconds. + */ + public long getSamplePresentationTimeUs(int index) { + return sampleDecodingTimeUsTable[index] + sampleCompositionTimeOffsetUsTable[index]; } /** Returns whether the sample at the given index has a subsample encryption table. */ diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump index 505c85e51f..b2412d09ff 100644 --- a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump @@ -81,7 +81,7 @@ track 0: flags = 1 data = length 520, hash FEE56928 sample 13: - time = 520000 + time = 519999 flags = 1 data = length 599, hash 41F496C5 sample 14: diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump index 8bee343bd9..41844c32a3 100644 --- a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump @@ -57,7 +57,7 @@ track 0: flags = 1 data = length 520, hash FEE56928 sample 7: - time = 520000 + time = 519999 flags = 1 data = length 599, hash 41F496C5 sample 8: diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump index ee1cf91a57..0f00ba9c5b 100644 --- a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump @@ -33,7 +33,7 @@ track 0: flags = 1 data = length 520, hash FEE56928 sample 1: - time = 520000 + time = 519999 flags = 1 data = length 599, hash 41F496C5 sample 2: diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump index 02db599cd7..c0e7d2a38d 100644 --- a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump @@ -107,7 +107,7 @@ track 0: crypto mode = 1 encryption key = length 16, hash 9FDDEA52 sample 13: - time = 520000 + time = 519999 flags = 1073741825 data = length 616, hash 3F657E23 crypto mode = 1 diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump index 8b45dd0a50..7886fc21ac 100644 --- a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump @@ -71,7 +71,7 @@ track 0: crypto mode = 1 encryption key = length 16, hash 9FDDEA52 sample 7: - time = 520000 + time = 519999 flags = 1073741825 data = length 616, hash 3F657E23 crypto mode = 1 diff --git a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump index a6be34dec7..e726932cb0 100644 --- a/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump +++ b/library/core/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump @@ -35,7 +35,7 @@ track 0: crypto mode = 1 encryption key = length 16, hash 9FDDEA52 sample 1: - time = 520000 + time = 519999 flags = 1073741825 data = length 616, hash 3F657E23 crypto mode = 1 diff --git a/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump index 65f59d78b5..d2b197286b 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented.mp4.0.dump @@ -31,123 +31,123 @@ track 0: total output bytes = 85933 sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: @@ -181,183 +181,183 @@ track 1: flags = 1 data = length 18, hash 96519432 sample 1: - time = 23000 + time = 23219 flags = 1 data = length 4, hash EE9DF sample 2: - time = 46000 + time = 46439 flags = 1 data = length 4, hash EEDBF sample 3: - time = 69000 + time = 69659 flags = 1 data = length 157, hash E2F078F4 sample 4: - time = 92000 + time = 92879 flags = 1 data = length 371, hash B9471F94 sample 5: - time = 116000 + time = 116099 flags = 1 data = length 373, hash 2AB265CB sample 6: - time = 139000 + time = 139319 flags = 1 data = length 402, hash 1295477C sample 7: - time = 162000 + time = 162539 flags = 1 data = length 455, hash 2D8146C8 sample 8: - time = 185000 + time = 185759 flags = 1 data = length 434, hash F2C5D287 sample 9: - time = 208000 + time = 208979 flags = 1 data = length 450, hash 84143FCD sample 10: - time = 232000 + time = 232199 flags = 1 data = length 429, hash EF769D50 sample 11: - time = 255000 + time = 255419 flags = 1 data = length 450, hash EC3DE692 sample 12: - time = 278000 + time = 278639 flags = 1 data = length 447, hash 3E519E13 sample 13: - time = 301000 + time = 301859 flags = 1 data = length 457, hash 1E4F23A0 sample 14: - time = 325000 + time = 325079 flags = 1 data = length 447, hash A439EA97 sample 15: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 16: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 17: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 18: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 19: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 20: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 21: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 22: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 23: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 24: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 25: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 26: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 27: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 28: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 29: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 30: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 31: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 32: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 33: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 34: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 35: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 36: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 37: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 38: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 39: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 40: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 41: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 42: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 43: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 44: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 45: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump index 27838bd2a8..8df0f881aa 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump @@ -31,123 +31,123 @@ track 0: total output bytes = 85933 sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: @@ -181,183 +181,183 @@ track 1: flags = 1 data = length 18, hash 96519432 sample 1: - time = 23000 + time = 23219 flags = 1 data = length 4, hash EE9DF sample 2: - time = 46000 + time = 46439 flags = 1 data = length 4, hash EEDBF sample 3: - time = 69000 + time = 69659 flags = 1 data = length 157, hash E2F078F4 sample 4: - time = 92000 + time = 92879 flags = 1 data = length 371, hash B9471F94 sample 5: - time = 116000 + time = 116099 flags = 1 data = length 373, hash 2AB265CB sample 6: - time = 139000 + time = 139319 flags = 1 data = length 402, hash 1295477C sample 7: - time = 162000 + time = 162539 flags = 1 data = length 455, hash 2D8146C8 sample 8: - time = 185000 + time = 185759 flags = 1 data = length 434, hash F2C5D287 sample 9: - time = 208000 + time = 208979 flags = 1 data = length 450, hash 84143FCD sample 10: - time = 232000 + time = 232199 flags = 1 data = length 429, hash EF769D50 sample 11: - time = 255000 + time = 255419 flags = 1 data = length 450, hash EC3DE692 sample 12: - time = 278000 + time = 278639 flags = 1 data = length 447, hash 3E519E13 sample 13: - time = 301000 + time = 301859 flags = 1 data = length 457, hash 1E4F23A0 sample 14: - time = 325000 + time = 325079 flags = 1 data = length 447, hash A439EA97 sample 15: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 16: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 17: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 18: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 19: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 20: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 21: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 22: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 23: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 24: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 25: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 26: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 27: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 28: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 29: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 30: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 31: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 32: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 33: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 34: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 35: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 36: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 37: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 38: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 39: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 40: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 41: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 42: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 43: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 44: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 45: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump index ea6deafcad..2e80647199 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump @@ -31,123 +31,123 @@ track 0: total output bytes = 85933 sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: @@ -177,127 +177,127 @@ track 1: total output bytes = 13359 sample count = 31 sample 0: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 1: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 2: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 3: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 4: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 5: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 6: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 7: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 8: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 9: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 10: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 11: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 12: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 13: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 14: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 15: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 16: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 17: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 18: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 19: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 20: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 21: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 22: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 23: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 24: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 25: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 26: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 27: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 28: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 29: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 30: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump index d14025e0b1..1715795320 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump @@ -31,123 +31,123 @@ track 0: total output bytes = 85933 sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: @@ -177,67 +177,67 @@ track 1: total output bytes = 6804 sample count = 16 sample 0: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 1: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 2: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 3: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 4: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 5: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 6: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 7: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 8: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 9: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 10: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 11: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 12: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 13: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 14: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 15: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump index d08a1e93ad..fcd968440f 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump @@ -31,123 +31,123 @@ track 0: total output bytes = 85933 sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: @@ -177,7 +177,7 @@ track 1: total output bytes = 10 sample count = 1 sample 0: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump index d596a77f78..3967f39251 100644 --- a/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -31,123 +31,123 @@ track 0: total output bytes = 85933 sample count = 30 sample 0: - time = 66000 + time = 66733 flags = 1 data = length 38070, hash B58E1AEE sample 1: - time = 199000 + time = 200199 flags = 0 data = length 8340, hash 8AC449FF sample 2: - time = 132000 + time = 133466 flags = 0 data = length 1295, hash C0DA5090 sample 3: - time = 100000 + time = 100100 flags = 0 data = length 469, hash D6E0A200 sample 4: - time = 166000 + time = 166832 flags = 0 data = length 564, hash E5F56C5B sample 5: - time = 332000 + time = 333666 flags = 0 data = length 6075, hash 8756E49E sample 6: - time = 266000 + time = 266933 flags = 0 data = length 847, hash DCC2B618 sample 7: - time = 233000 + time = 233566 flags = 0 data = length 455, hash B9CCE047 sample 8: - time = 299000 + time = 300299 flags = 0 data = length 467, hash 69806D94 sample 9: - time = 466000 + time = 467133 flags = 0 data = length 4549, hash 3944F501 sample 10: - time = 399000 + time = 400399 flags = 0 data = length 1087, hash 491BF106 sample 11: - time = 367000 + time = 367033 flags = 0 data = length 380, hash 5FED016A sample 12: - time = 433000 + time = 433766 flags = 0 data = length 455, hash 8A0610 sample 13: - time = 599000 + time = 600599 flags = 0 data = length 5190, hash B9031D8 sample 14: - time = 533000 + time = 533866 flags = 0 data = length 1071, hash 684E7DC8 sample 15: - time = 500000 + time = 500500 flags = 0 data = length 653, hash 8494F326 sample 16: - time = 566000 + time = 567232 flags = 0 data = length 485, hash 2CCC85F4 sample 17: - time = 733000 + time = 734066 flags = 0 data = length 4884, hash D16B6A96 sample 18: - time = 666000 + time = 667333 flags = 0 data = length 997, hash 164FF210 sample 19: - time = 633000 + time = 633966 flags = 0 data = length 640, hash F664125B sample 20: - time = 700000 + time = 700699 flags = 0 data = length 491, hash B5930C7C sample 21: - time = 866000 + time = 867533 flags = 0 data = length 2989, hash 92CF4FCF sample 22: - time = 800000 + time = 800799 flags = 0 data = length 838, hash 294A3451 sample 23: - time = 767000 + time = 767433 flags = 0 data = length 544, hash FCCE2DE6 sample 24: - time = 833000 + time = 834166 flags = 0 data = length 329, hash A654FFA1 sample 25: - time = 1000000 + time = 1000999 flags = 0 data = length 1517, hash 5F7EBF8B sample 26: - time = 933000 + time = 934266 flags = 0 data = length 803, hash 7A5C4C1D sample 27: - time = 900000 + time = 900900 flags = 0 data = length 415, hash B31BBC3B sample 28: - time = 967000 + time = 967632 flags = 0 data = length 415, hash 850DFEA3 sample 29: - time = 1033000 + time = 1034366 flags = 0 data = length 619, hash AB5E56CA track 1: @@ -181,183 +181,183 @@ track 1: flags = 1 data = length 18, hash 96519432 sample 1: - time = 23000 + time = 23219 flags = 1 data = length 4, hash EE9DF sample 2: - time = 46000 + time = 46439 flags = 1 data = length 4, hash EEDBF sample 3: - time = 69000 + time = 69659 flags = 1 data = length 157, hash E2F078F4 sample 4: - time = 92000 + time = 92879 flags = 1 data = length 371, hash B9471F94 sample 5: - time = 116000 + time = 116099 flags = 1 data = length 373, hash 2AB265CB sample 6: - time = 139000 + time = 139319 flags = 1 data = length 402, hash 1295477C sample 7: - time = 162000 + time = 162539 flags = 1 data = length 455, hash 2D8146C8 sample 8: - time = 185000 + time = 185759 flags = 1 data = length 434, hash F2C5D287 sample 9: - time = 208000 + time = 208979 flags = 1 data = length 450, hash 84143FCD sample 10: - time = 232000 + time = 232199 flags = 1 data = length 429, hash EF769D50 sample 11: - time = 255000 + time = 255419 flags = 1 data = length 450, hash EC3DE692 sample 12: - time = 278000 + time = 278639 flags = 1 data = length 447, hash 3E519E13 sample 13: - time = 301000 + time = 301859 flags = 1 data = length 457, hash 1E4F23A0 sample 14: - time = 325000 + time = 325079 flags = 1 data = length 447, hash A439EA97 sample 15: - time = 348000 + time = 348299 flags = 1 data = length 456, hash 1E9034C6 sample 16: - time = 371000 + time = 371519 flags = 1 data = length 398, hash 99DB7345 sample 17: - time = 394000 + time = 394739 flags = 1 data = length 474, hash 3F05F10A sample 18: - time = 417000 + time = 417959 flags = 1 data = length 416, hash C105EE09 sample 19: - time = 441000 + time = 441179 flags = 1 data = length 454, hash 5FDBE458 sample 20: - time = 464000 + time = 464399 flags = 1 data = length 438, hash 41A93AC3 sample 21: - time = 487000 + time = 487619 flags = 1 data = length 443, hash 10FDA652 sample 22: - time = 510000 + time = 510839 flags = 1 data = length 412, hash 1F791E25 sample 23: - time = 534000 + time = 534058 flags = 1 data = length 482, hash A6D983D sample 24: - time = 557000 + time = 557278 flags = 1 data = length 386, hash BED7392F sample 25: - time = 580000 + time = 580498 flags = 1 data = length 463, hash 5309F8C9 sample 26: - time = 603000 + time = 603718 flags = 1 data = length 394, hash 21C7321F sample 27: - time = 626000 + time = 626938 flags = 1 data = length 489, hash 71B4730D sample 28: - time = 650000 + time = 650158 flags = 1 data = length 403, hash D9C6DE89 sample 29: - time = 673000 + time = 673378 flags = 1 data = length 447, hash 9B14B73B sample 30: - time = 696000 + time = 696598 flags = 1 data = length 439, hash 4760D35B sample 31: - time = 719000 + time = 719818 flags = 1 data = length 463, hash 1601F88D sample 32: - time = 743000 + time = 743038 flags = 1 data = length 423, hash D4AE6773 sample 33: - time = 766000 + time = 766258 flags = 1 data = length 497, hash A3C674D3 sample 34: - time = 789000 + time = 789478 flags = 1 data = length 419, hash D3734A1F sample 35: - time = 812000 + time = 812698 flags = 1 data = length 474, hash DFB41F9 sample 36: - time = 835000 + time = 835918 flags = 1 data = length 413, hash 53E7CB9F sample 37: - time = 859000 + time = 859138 flags = 1 data = length 445, hash D15B0E39 sample 38: - time = 882000 + time = 882358 flags = 1 data = length 453, hash 77ED81E4 sample 39: - time = 905000 + time = 905578 flags = 1 data = length 545, hash 3321AEB9 sample 40: - time = 928000 + time = 928798 flags = 1 data = length 317, hash F557D0E sample 41: - time = 952000 + time = 952018 flags = 1 data = length 537, hash ED58CF7B sample 42: - time = 975000 + time = 975238 flags = 1 data = length 458, hash 51CDAA10 sample 43: - time = 998000 + time = 998458 flags = 1 data = length 465, hash CBA1EFD7 sample 44: - time = 1021000 + time = 1021678 flags = 1 data = length 446, hash D6735B8A sample 45: - time = 1044000 + time = 1044897 flags = 1 data = length 10, hash A453EEBE track 3: From 7ee08f09d28d4d3e13cdaf13689f463dd9d1bd5c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 23 Apr 2020 14:57:51 +0100 Subject: [PATCH 16/80] Fix AdsMediaSource parameter when reporting load error PiperOrigin-RevId: 308041841 --- .../google/android/exoplayer2/source/ads/AdsMediaSource.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 34f0d496a2..4ecef1bd5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -358,7 +359,7 @@ public final class AdsMediaSource extends CompositeMediaSource { dataSpec.uri, /* responseHeaders= */ Collections.emptyMap(), C.DATA_TYPE_AD, - C.TRACK_TYPE_UNKNOWN, + /* elapsedRealtimeMs= */ SystemClock.elapsedRealtime(), /* loadDurationMs= */ 0, /* bytesLoaded= */ 0, error, From 49e5e66033c497fdd26c756d97c94b595ec35eb6 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 24 Apr 2020 13:32:05 +0100 Subject: [PATCH 17/80] Fix NPE when reading from a SampleQueue from a loading thread Issue: #7273 PiperOrigin-RevId: 308238035 --- RELEASENOTES.md | 3 ++- .../source/ProgressiveMediaPeriod.java | 3 ++- .../exoplayer2/source/SampleQueue.java | 8 +++--- .../source/chunk/ChunkSampleStream.java | 11 ++++++-- .../exoplayer2/source/SampleQueueTest.java | 27 +++++++++++++++---- .../source/dash/PlayerEmsgHandler.java | 5 +++- .../source/hls/HlsSampleStreamWrapper.java | 10 +++++-- 7 files changed, 52 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 742b2828e7..89d630527a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD # Release notes # ### Next release ### @@ -14,6 +13,8 @@ `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as the main adaptation sets to which they refer. Trick play tracks are marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. + * Fix assertion failure in `SampleQueue` when playing DASH streams with + EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). * MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). * Text diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 966a58bf5f..efdfdf15a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -679,7 +679,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return sampleQueues[i]; } } - SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager); + SampleQueue trackOutput = new SampleQueue( + allocator, /* playbackLooper= */ handler.getLooper(), drmSessionManager); trackOutput.setUpstreamFormatChangeListener(this); @NullableType TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index b5cfe6ed72..c63b755f4a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -55,6 +55,7 @@ public class SampleQueue implements TrackOutput { private final SampleExtrasHolder extrasHolder; private final DrmSessionManager drmSessionManager; private UpstreamFormatChangedListener upstreamFormatChangeListener; + private final Looper playbackLooper; @Nullable private Format downstreamFormat; @Nullable private DrmSession currentDrmSession; @@ -91,11 +92,13 @@ public class SampleQueue implements TrackOutput { * Creates a sample queue. * * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + * @param playbackLooper The looper associated with the media playback thread. * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} * from. The created instance does not take ownership of this {@link DrmSessionManager}. */ - public SampleQueue(Allocator allocator, DrmSessionManager drmSessionManager) { + public SampleQueue(Allocator allocator, Looper playbackLooper, DrmSessionManager drmSessionManager) { sampleDataQueue = new SampleDataQueue(allocator); + this.playbackLooper = playbackLooper; this.drmSessionManager = drmSessionManager; extrasHolder = new SampleExtrasHolder(); capacity = SAMPLE_CAPACITY_INCREMENT; @@ -789,8 +792,7 @@ public class SampleQueue implements TrackOutput { } // Ensure we acquire the new session before releasing the previous one in case the same session // is being used for both DrmInitData. - DrmSession previousSession = currentDrmSession; - Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); + @Nullable DrmSession previousSession = currentDrmSession; currentDrmSession = newDrmInitData != null ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index db555b136f..e2278d7f95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -130,13 +131,19 @@ public class ChunkSampleStream implements SampleStream, S int[] trackTypes = new int[1 + embeddedTrackCount]; SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new SampleQueue(allocator, drmSessionManager); + primarySampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + drmSessionManager); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { SampleQueue sampleQueue = - new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()); + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + DrmSessionManager.getDummyDrmSessionManager()); embeddedSampleQueues[i] = sampleQueue; sampleQueues[i + 1] = sampleQueue; trackTypes[i + 1] = embeddedTrackTypes[i]; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index a34488d2e7..a35e8c4d52 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -26,6 +26,7 @@ import static java.util.Arrays.copyOfRange; import static org.junit.Assert.assertArrayEquals; import static org.mockito.Mockito.when; +import android.os.Looper; import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -40,6 +41,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import java.util.Arrays; @@ -143,7 +145,10 @@ public final class SampleQueueTest { mockDrmSession = (DrmSession) Mockito.mock(DrmSession.class); when(mockDrmSessionManager.acquireSession(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(mockDrmSession); - sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + sampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager); formatHolder = new FormatHolder(); inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } @@ -360,7 +365,10 @@ public final class SampleQueueTest { public void testIsReadyReturnsTrueForClearSampleAndPlayClearSamplesWithoutKeysIsTrue() { when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true); // We recreate the queue to ensure the mock DRM session manager flags are taken into account. - sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + sampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager); writeTestDataWithEncryptedSections(); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue(); } @@ -542,7 +550,10 @@ public final class SampleQueueTest { public void testAllowPlayClearSamplesWithoutKeysReadsClearSamples() { when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true); // We recreate the queue to ensure the mock DRM session manager flags are taken into account. - sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + sampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); @@ -931,7 +942,10 @@ public final class SampleQueueTest { public void testAdjustUpstreamFormat() { String label = "label"; sampleQueue = - new SampleQueue(allocator, mockDrmSessionManager) { + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager) { @Override public Format getAdjustedUpstreamFormat(Format format) { return super.getAdjustedUpstreamFormat(format.copyWithLabel(label)); @@ -947,7 +961,10 @@ public final class SampleQueueTest { public void testInvalidateUpstreamFormatAdjustment() { AtomicReference label = new AtomicReference<>("label1"); sampleQueue = - new SampleQueue(allocator, mockDrmSessionManager) { + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + mockDrmSessionManager) { @Override public Format getAdjustedUpstreamFormat(Format format) { return super.getAdjustedUpstreamFormat(format.copyWithLabel(label.get())); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 3b52e070a6..187baad76b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -284,7 +284,10 @@ public final class PlayerEmsgHandler implements Handler.Callback { private final MetadataInputBuffer buffer; /* package */ PlayerTrackEmsgHandler(Allocator allocator) { - this.sampleQueue = new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()); + this.sampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + DrmSessionManager.getDummyDrmSessionManager()); formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 03a67a1407..c7116ba878 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.util.SparseIntArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -907,7 +908,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; FormatAdjustingSampleQueue trackOutput = - new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData); + new FormatAdjustingSampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + drmSessionManager, + overridingDrmInitData); if (isAudioVideo) { trackOutput.setDrmInitData(drmInitData); } @@ -1331,9 +1336,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public FormatAdjustingSampleQueue( Allocator allocator, + Looper playbackLooper, DrmSessionManager drmSessionManager, Map overridingDrmInitData) { - super(allocator, drmSessionManager); + super(allocator, playbackLooper, drmSessionManager); this.overridingDrmInitData = overridingDrmInitData; } From f052e89a8ff9edeeac816200d27c24e6a87b9100 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Apr 2020 10:27:31 +0100 Subject: [PATCH 18/80] Pass app context to the IMA SDK Notes: this doesn't fix the current issue where the component containing the ad overlay view leaks, but is good practice anyway. PiperOrigin-RevId: 308582036 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 98dbef7c6c..a37294365c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -477,7 +477,9 @@ public final class ImaAdsLoader adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); + adsLoader = + imaFactory.createAdsLoader( + context.getApplicationContext(), imaSdkSettings, adDisplayContainer); adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; From 8760424d76bcb94efa0d7e39197c3758a161e7ea Mon Sep 17 00:00:00 2001 From: vigneshv Date: Mon, 27 Apr 2020 21:15:49 +0100 Subject: [PATCH 19/80] av1_extension: Add a heuristic to determine default thread count Android scheduler has performance issues when a device has a combiation of big/medium/little cores. Add a heuristic to set the default number of threads used for deocding to the number of "performance" (i.e. big) cores. PiperOrigin-RevId: 308683989 --- RELEASENOTES.md | 2 + .../exoplayer2/ext/av1/Gav1Decoder.java | 23 ++- .../ext/av1/Libgav1VideoRenderer.java | 15 +- extensions/av1/src/main/jni/CMakeLists.txt | 4 +- extensions/av1/src/main/jni/cpu_info.cc | 153 ++++++++++++++++++ extensions/av1/src/main/jni/cpu_info.h | 13 ++ extensions/av1/src/main/jni/gav1_jni.cc | 5 + 7 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 extensions/av1/src/main/jni/cpu_info.cc create mode 100644 extensions/av1/src/main/jni/cpu_info.h diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 89d630527a..95e2a0581f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ * Text * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles ([#6950](https://github.com/google/ExoPlayer/pull/6950)). +* AV1 extension: Add a heuristic to determine the default number of threads + used for AV1 playback using the extension. ### 2.11.4 (2020-04-08) ### diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java index 687ac47f2a..cdff8581f1 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.av1; +import static java.lang.Runtime.getRuntime; + import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -44,7 +46,9 @@ import java.nio.ByteBuffer; * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. * @param initialInputBufferSize The initial size of each input buffer, in bytes. - * @param threads Number of threads libgav1 will use to decode. + * @param threads Number of threads libgav1 will use to decode. If {@link + * Libgav1VideoRenderer#THREAD_COUNT_AUTODETECT} is passed, then this class will auto detect + * the number of threads to be used. * @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder. */ public Gav1Decoder( @@ -56,6 +60,16 @@ import java.nio.ByteBuffer; if (!Gav1Library.isAvailable()) { throw new Gav1DecoderException("Failed to load decoder native library."); } + + if (threads == Libgav1VideoRenderer.THREAD_COUNT_AUTODETECT) { + // Try to get the optimal number of threads from the AV1 heuristic. + threads = gav1GetThreads(); + if (threads <= 0) { + // If that is not available, default to the number of available processors. + threads = getRuntime().availableProcessors(); + } + } + gav1DecoderContext = gav1Init(threads); if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) { throw new Gav1DecoderException( @@ -231,4 +245,11 @@ import java.nio.ByteBuffer; * @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured. */ private native int gav1CheckError(long context); + + /** + * Returns the optimal number of threads to be used for AV1 decoding. + * + * @return Optimal number of threads if there was no error, 0 if an error occurred. + */ + private native int gav1GetThreads(); } diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java index 3d10c2579b..122a94b7b1 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.ext.av1; -import static java.lang.Runtime.getRuntime; - import android.os.Handler; import android.view.Surface; import androidx.annotation.Nullable; @@ -55,6 +53,13 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; */ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { + /** + * Attempts to use as many threads as performance processors available on the device. If the + * number of performance processors cannot be detected, the number of available processors is + * used. + */ + public static final int THREAD_COUNT_AUTODETECT = 0; + private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4; private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4; /* Default size based on 720p resolution video compressed by a factor of two. */ @@ -94,7 +99,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { eventHandler, eventListener, maxDroppedFramesToNotify, - /* threads= */ getRuntime().availableProcessors(), + THREAD_COUNT_AUTODETECT, DEFAULT_NUM_OF_INPUT_BUFFERS, DEFAULT_NUM_OF_OUTPUT_BUFFERS); } @@ -109,7 +114,9 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - * @param threads Number of threads libgav1 will use to decode. + * @param threads Number of threads libgav1 will use to decode. If + * {@link #THREAD_COUNT_AUTODETECT} is passed, then the number of threads to use is + * auto-detected based on CPU capabilities. * @param numInputBuffers Number of input buffers. * @param numOutputBuffers Number of output buffers. */ diff --git a/extensions/av1/src/main/jni/CMakeLists.txt b/extensions/av1/src/main/jni/CMakeLists.txt index c7989d4ef2..075773a70e 100644 --- a/extensions/av1/src/main/jni/CMakeLists.txt +++ b/extensions/av1/src/main/jni/CMakeLists.txt @@ -44,7 +44,9 @@ add_subdirectory("${libgav1_root}" # Build libgav1JNI. add_library(gav1JNI SHARED - gav1_jni.cc) + gav1_jni.cc + cpu_info.cc + cpu_info.h) # Locate NDK log library. find_library(android_log_lib log) diff --git a/extensions/av1/src/main/jni/cpu_info.cc b/extensions/av1/src/main/jni/cpu_info.cc new file mode 100644 index 0000000000..8f4a405f4f --- /dev/null +++ b/extensions/av1/src/main/jni/cpu_info.cc @@ -0,0 +1,153 @@ +#include "cpu_info.h" // NOLINT + +#include + +#include +#include +#include +#include +#include + +namespace gav1_jni { +namespace { + +// Note: The code in this file needs to use the 'long' type because it is the +// return type of the Standard C Library function strtol(). The linter warnings +// are suppressed with NOLINT comments since they are integers at runtime. + +// Returns the number of online processor cores. +int GetNumberOfProcessorsOnline() { + // See https://developer.android.com/ndk/guides/cpu-features. + long num_cpus = sysconf(_SC_NPROCESSORS_ONLN); // NOLINT + if (num_cpus < 0) { + return 0; + } + // It is safe to cast num_cpus to int. sysconf(_SC_NPROCESSORS_ONLN) returns + // the return value of get_nprocs(), which is an int. + return static_cast(num_cpus); +} + +} // namespace + +// These CPUs support heterogeneous multiprocessing. +#if defined(__arm__) || defined(__aarch64__) + +// A helper function used by GetNumberOfPerformanceCoresOnline(). +// +// Returns the cpuinfo_max_freq value (in kHz) of the given CPU. Returns 0 on +// failure. +long GetCpuinfoMaxFreq(int cpu_index) { // NOLINT + char buffer[128]; + const int rv = snprintf( + buffer, sizeof(buffer), + "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu_index); + if (rv < 0 || rv >= sizeof(buffer)) { + return 0; + } + FILE* file = fopen(buffer, "r"); + if (file == nullptr) { + return 0; + } + char* const str = fgets(buffer, sizeof(buffer), file); + fclose(file); + if (str == nullptr) { + return 0; + } + const long freq = strtol(str, nullptr, 10); // NOLINT + if (freq <= 0 || freq == LONG_MAX) { + return 0; + } + return freq; +} + +// Returns the number of performance CPU cores that are online. The number of +// efficiency CPU cores is subtracted from the total number of CPU cores. Uses +// cpuinfo_max_freq to determine whether a CPU is a performance core or an +// efficiency core. +// +// This function is not perfect. For example, the Snapdragon 632 SoC used in +// Motorola Moto G7 has performance and efficiency cores with the same +// cpuinfo_max_freq but different cpuinfo_min_freq. This function fails to +// differentiate the two kinds of cores and reports all the cores as +// performance cores. +int GetNumberOfPerformanceCoresOnline() { + // Get the online CPU list. Some examples of the online CPU list are: + // "0-7" + // "0" + // "0-1,2,3,4-7" + FILE* file = fopen("/sys/devices/system/cpu/online", "r"); + if (file == nullptr) { + return 0; + } + char online[512]; + char* const str = fgets(online, sizeof(online), file); + fclose(file); + file = nullptr; + if (str == nullptr) { + return 0; + } + + // Count the number of the slowest CPUs. Some SoCs such as Snapdragon 855 + // have performance cores with different max frequencies, so only the slowest + // CPUs are efficiency cores. If we count the number of the fastest CPUs, we + // will fail to count the second fastest performance cores. + long slowest_cpu_freq = LONG_MAX; // NOLINT + int num_slowest_cpus = 0; + int num_cpus = 0; + const char* cp = online; + int range_begin = -1; + while (true) { + char* str_end; + const int cpu = static_cast(strtol(cp, &str_end, 10)); // NOLINT + if (str_end == cp) { + break; + } + cp = str_end; + if (*cp == '-') { + range_begin = cpu; + } else { + if (range_begin == -1) { + range_begin = cpu; + } + + num_cpus += cpu - range_begin + 1; + for (int i = range_begin; i <= cpu; ++i) { + const long freq = GetCpuinfoMaxFreq(i); // NOLINT + if (freq <= 0) { + return 0; + } + if (freq < slowest_cpu_freq) { + slowest_cpu_freq = freq; + num_slowest_cpus = 0; + } + if (freq == slowest_cpu_freq) { + ++num_slowest_cpus; + } + } + + range_begin = -1; + } + if (*cp == '\0') { + break; + } + ++cp; + } + + // If there are faster CPU cores than the slowest CPU cores, exclude the + // slowest CPU cores. + if (num_slowest_cpus < num_cpus) { + num_cpus -= num_slowest_cpus; + } + return num_cpus; +} + +#else + +// Assume symmetric multiprocessing. +int GetNumberOfPerformanceCoresOnline() { + return GetNumberOfProcessorsOnline(); +} + +#endif + +} // namespace gav1_jni diff --git a/extensions/av1/src/main/jni/cpu_info.h b/extensions/av1/src/main/jni/cpu_info.h new file mode 100644 index 0000000000..77f869a93e --- /dev/null +++ b/extensions/av1/src/main/jni/cpu_info.h @@ -0,0 +1,13 @@ +#ifndef EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ +#define EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ + +namespace gav1_jni { + +// Returns the number of performance cores that are available for AV1 decoding. +// This is a heuristic that works on most common android devices. Returns 0 on +// error or if the number of performance cores cannot be determined. +int GetNumberOfPerformanceCoresOnline(); + +} // namespace gav1_jni + +#endif // EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_ diff --git a/extensions/av1/src/main/jni/gav1_jni.cc b/extensions/av1/src/main/jni/gav1_jni.cc index e0cef86d22..714ab499b1 100644 --- a/extensions/av1/src/main/jni/gav1_jni.cc +++ b/extensions/av1/src/main/jni/gav1_jni.cc @@ -32,6 +32,7 @@ #include // NOLINT #include +#include "cpu_info.h" // NOLINT #include "gav1/decoder.h" #define LOG_TAG "gav1_jni" @@ -774,5 +775,9 @@ DECODER_FUNC(jint, gav1CheckError, jlong jContext) { return kStatusOk; } +DECODER_FUNC(jint, gav1GetThreads) { + return gav1_jni::GetNumberOfPerformanceCoresOnline(); +} + // TODO(b/139902005): Add functions for getting libgav1 version and build // configuration once libgav1 ABI provides this information. From 3ac4c1a6e57766dc74d6f3e266eb008605a6331c Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 15 Apr 2020 09:52:51 +0100 Subject: [PATCH 20/80] Add Clock#currentTimeMillis() PiperOrigin-RevId: 306602043 --- .../google/android/exoplayer2/util/Clock.java | 7 +++ .../android/exoplayer2/util/SystemClock.java | 5 +++ .../exoplayer2/testutil/FakeClock.java | 45 ++++++++++++++----- .../exoplayer2/testutil/FakeClockTest.java | 19 ++++++++ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 7a87d7d9a3..ffb8236bd1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -30,6 +30,13 @@ public interface Clock { */ Clock DEFAULT = new SystemClock(); + /** + * Returns the current time in milliseconds since the Unix Epoch. + * + * @see System#currentTimeMillis() + */ + long currentTimeMillis(); + /** @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index be526595c6..a094e810bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -25,6 +25,11 @@ import androidx.annotation.Nullable; */ /* package */ final class SystemClock implements Clock { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + @Override public long elapsedRealtime() { return android.os.SystemClock.elapsedRealtime(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index a591546613..dcf454449c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import androidx.annotation.GuardedBy; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import java.util.ArrayList; @@ -28,16 +29,31 @@ public class FakeClock implements Clock { private final List wakeUpTimes; private final List handlerMessages; + private final long bootTimeMs; - private long currentTimeMs; + @GuardedBy("this") + private long timeSinceBootMs; /** - * Create {@link FakeClock} with an arbitrary initial timestamp. + * Creates a fake clock assuming the system was booted exactly at time {@code 0} (the Unix Epoch) + * and {@code initialTimeMs} milliseconds have passed since system boot. * - * @param initialTimeMs Initial timestamp in milliseconds. + * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. */ public FakeClock(long initialTimeMs) { - this.currentTimeMs = initialTimeMs; + this(/* bootTimeMs= */ 0, initialTimeMs); + } + + /** + * Creates a fake clock specifying when the system was booted and how much time has passed since + * then. + * + * @param bootTimeMs The time the system was booted since the Unix Epoch, in milliseconds. + * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. + */ + public FakeClock(long bootTimeMs, long initialTimeMs) { + this.bootTimeMs = bootTimeMs; + this.timeSinceBootMs = initialTimeMs; this.wakeUpTimes = new ArrayList<>(); this.handlerMessages = new ArrayList<>(); } @@ -48,23 +64,28 @@ public class FakeClock implements Clock { * @param timeDiffMs The amount of time to add to the timestamp in milliseconds. */ public synchronized void advanceTime(long timeDiffMs) { - currentTimeMs += timeDiffMs; + timeSinceBootMs += timeDiffMs; for (Long wakeUpTime : wakeUpTimes) { - if (wakeUpTime <= currentTimeMs) { + if (wakeUpTime <= timeSinceBootMs) { notifyAll(); break; } } for (int i = handlerMessages.size() - 1; i >= 0; i--) { - if (handlerMessages.get(i).maybeSendToTarget(currentTimeMs)) { + if (handlerMessages.get(i).maybeSendToTarget(timeSinceBootMs)) { handlerMessages.remove(i); } } } + @Override + public synchronized long currentTimeMillis() { + return bootTimeMs + timeSinceBootMs; + } + @Override public synchronized long elapsedRealtime() { - return currentTimeMs; + return timeSinceBootMs; } @Override @@ -77,9 +98,9 @@ public class FakeClock implements Clock { if (sleepTimeMs <= 0) { return; } - Long wakeUpTimeMs = currentTimeMs + sleepTimeMs; + Long wakeUpTimeMs = timeSinceBootMs + sleepTimeMs; wakeUpTimes.add(wakeUpTimeMs); - while (currentTimeMs < wakeUpTimeMs) { + while (timeSinceBootMs < wakeUpTimeMs) { try { wait(); } catch (InterruptedException e) { @@ -97,7 +118,7 @@ public class FakeClock implements Clock { /** Adds a handler post to list of pending messages. */ protected synchronized boolean addHandlerMessageAtTime( HandlerWrapper handler, Runnable runnable, long timeMs) { - if (timeMs <= currentTimeMs) { + if (timeMs <= timeSinceBootMs) { return handler.post(runnable); } handlerMessages.add(new HandlerMessageData(timeMs, handler, runnable)); @@ -107,7 +128,7 @@ public class FakeClock implements Clock { /** Adds an empty handler message to list of pending messages. */ protected synchronized boolean addHandlerMessageAtTime( HandlerWrapper handler, int message, long timeMs) { - if (timeMs <= currentTimeMs) { + if (timeMs <= timeSinceBootMs) { return handler.sendEmptyMessage(message); } handlerMessages.add(new HandlerMessageData(timeMs, handler, message)); diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index c82980d7a4..55e0d29f01 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -35,6 +35,25 @@ public final class FakeClockTest { private static final long TIMEOUT_MS = 10000; + @Test + public void currentTimeMillis_withoutBootTime() { + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 10); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(10); + } + + @Test + public void currentTimeMillis_withBootTime() { + FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 150, /* initialTimeMs= */ 200); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(350); + } + + @Test + public void currentTimeMillis_advanceTime_currentTimeHasAdvanced() { + FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50); + fakeClock.advanceTime(/* timeDiffMs */ 250); + assertThat(fakeClock.currentTimeMillis()).isEqualTo(400); + } + @Test public void testAdvanceTime() { FakeClock fakeClock = new FakeClock(2000); From 9213ffafa8bb72abed9ff8887132a610639a2303 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Apr 2020 14:35:49 +0100 Subject: [PATCH 21/80] ConditionVariable: Improve documentation and allow clock injection - Improve documentation explaining the benefits of ExoPlayer's ConditionVariable over the one that the platform provides - Allow Clock injection - Create TestUtil method for obtaining a ConditionVariable whose block(long) method times out correctly when used in a Robolectric test - Add basic unit tests for ConditionVariable PiperOrigin-RevId: 308812698 --- .../exoplayer2/util/ConditionVariable.java | 34 ++++- .../android/exoplayer2/util/SystemClock.java | 7 +- .../util/ConditionVariableTest.java | 118 ++++++++++++++++++ .../android/exoplayer2/testutil/TestUtil.java | 21 ++++ .../exoplayer2/testutil/TestUtilTest.java | 46 +++++++ 5 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java create mode 100644 testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index c035c62a7e..1b5cd47401 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -16,13 +16,39 @@ package com.google.android.exoplayer2.util; /** - * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return - * whether they resulted in a change of state. + * An interruptible condition variable. This class provides a number of benefits over {@link + * android.os.ConditionVariable}: + * + *
    + *
  • Consistent use of ({@link Clock#elapsedRealtime()} for timing {@link #block(long)} timeout + * intervals. {@link android.os.ConditionVariable} used {@link System#currentTimeMillis()} + * prior to Android 10, which is not a correct clock to use for interval timing because it's + * not guaranteed to be monotonic. + *
  • Support for injecting a custom {@link Clock}. + *
  • The ability to query the variable's current state, by calling {@link #isOpen()}. + *
  • {@link #open()} and {@link #close()} return whether they changed the variable's state. + *
*/ public final class ConditionVariable { + private final Clock clock; private boolean isOpen; + /** Creates an instance using {@link Clock#DEFAULT}. */ + public ConditionVariable() { + this(Clock.DEFAULT); + } + + /** + * Creates an instance. + * + * @param clock The {@link Clock} whose {@link Clock#elapsedRealtime()} method is used to + * determine when {@link #block(long)} should time out. + */ + public ConditionVariable(Clock clock) { + this.clock = clock; + } + /** * Opens the condition and releases all threads that are blocked. * @@ -67,11 +93,11 @@ public final class ConditionVariable { * @throws InterruptedException If the thread is interrupted. */ public synchronized boolean block(long timeout) throws InterruptedException { - long now = android.os.SystemClock.elapsedRealtime(); + long now = clock.elapsedRealtime(); long end = now + timeout; while (!isOpen && now < end) { wait(end - now); - now = android.os.SystemClock.elapsedRealtime(); + now = clock.elapsedRealtime(); } return isOpen; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index a094e810bf..89e1c60d7a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -21,9 +21,12 @@ import android.os.Looper; import androidx.annotation.Nullable; /** - * The standard implementation of {@link Clock}. + * The standard implementation of {@link Clock}, an instance of which is available via {@link + * SystemClock#DEFAULT}. */ -/* package */ final class SystemClock implements Clock { +public class SystemClock implements Clock { + + protected SystemClock() {} @Override public long currentTimeMillis() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java new file mode 100644 index 0000000000..1e47aa680d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ConditionVariableTest}. */ +@RunWith(AndroidJUnit4.class) +public class ConditionVariableTest { + + @Test + public void initialState_isClosed() { + ConditionVariable conditionVariable = buildTestConditionVariable(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithTimeout_timesOut() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + assertThat(conditionVariable.block(1)).isFalse(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void blockWithTimeout_blocksForAtLeastTimeout() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + long startTimeMs = System.currentTimeMillis(); + assertThat(conditionVariable.block(/* timeout= */ 500)).isFalse(); + long endTimeMs = System.currentTimeMillis(); + assertThat(endTimeMs - startTimeMs).isAtLeast(500L); + } + + @Test + public void blockWithoutTimeout_blocks() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + blockingThread.interrupt(); + blockingThread.join(); + assertThat(blockWasInterrupted.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void open_unblocksBlock() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + conditionVariable.open(); + blockingThread.join(); + assertThat(blockReturned.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isTrue(); + } + + private static ConditionVariable buildTestConditionVariable() { + return new ConditionVariable( + new SystemClock() { + @Override + public long elapsedRealtime() { + // elapsedRealtime() does not advance during Robolectric test execution, so use + // currentTimeMillis() instead. This is technically unsafe because this clock is not + // guaranteed to be monotonic, but in practice it will work provided the clock of the + // host machine does not change during test execution. + return Clock.DEFAULT.currentTimeMillis(); + } + }); + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index c47b438100..b0beb1ba13 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -36,6 +36,9 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.ConditionVariable; +import com.google.android.exoplayer2.util.SystemClock; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.io.InputStream; @@ -441,4 +444,22 @@ public class TestUtil { } return new DefaultExtractorInput(dataSource, position, length); } + + /** + * Creates a {@link ConditionVariable} whose {@link ConditionVariable#block(long)} method times + * out according to wallclock time when used in Robolectric tests. + */ + public static ConditionVariable createRobolectricConditionVariable() { + return new ConditionVariable( + new SystemClock() { + @Override + public long elapsedRealtime() { + // elapsedRealtime() does not advance during Robolectric test execution, so use + // currentTimeMillis() instead. This is technically unsafe because this clock is not + // guaranteed to be monotonic, but in practice it will work provided the clock of the + // host machine does not change during test execution. + return Clock.DEFAULT.currentTimeMillis(); + } + }); + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java new file mode 100644 index 0000000000..a80d474f9b --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.ConditionVariable; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TestUtil}. */ +@RunWith(AndroidJUnit4.class) +public class TestUtilTest { + + @Test + public void createRobolectricConditionVariable_blockWithTimeout_timesOut() + throws InterruptedException { + ConditionVariable conditionVariable = TestUtil.createRobolectricConditionVariable(); + assertThat(conditionVariable.block(/* timeout= */ 1)).isFalse(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + + @Test + public void createRobolectricConditionVariable_blockWithTimeout_blocksForAtLeastTimeout() + throws InterruptedException { + ConditionVariable conditionVariable = TestUtil.createRobolectricConditionVariable(); + long startTimeMs = System.currentTimeMillis(); + assertThat(conditionVariable.block(/* timeout= */ 500)).isFalse(); + long endTimeMs = System.currentTimeMillis(); + assertThat(endTimeMs - startTimeMs).isAtLeast(500); + } +} From 1772b0d9177880f458b48998df2047132dddd055 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Apr 2020 14:59:23 +0100 Subject: [PATCH 22/80] ConditionVariable: Fix block(long) to correctly handle large timeouts PiperOrigin-RevId: 308815613 --- .../exoplayer2/util/ConditionVariable.java | 25 +++++++++++----- .../util/ConditionVariableTest.java | 29 ++++++++++++++++++- .../exoplayer2/testutil/TestUtilTest.java | 4 +-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 1b5cd47401..69782ab1e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -86,18 +86,27 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. + * Blocks until the condition is opened or until {@code timeoutMs} have passed. * - * @param timeout The maximum time to wait in milliseconds. + * @param timeoutMs The maximum time to wait in milliseconds. If {@code timeoutMs <= 0} then the + * call will return immediately without blocking. * @return True if the condition was opened, false if the call returns because of the timeout. * @throws InterruptedException If the thread is interrupted. */ - public synchronized boolean block(long timeout) throws InterruptedException { - long now = clock.elapsedRealtime(); - long end = now + timeout; - while (!isOpen && now < end) { - wait(end - now); - now = clock.elapsedRealtime(); + public synchronized boolean block(long timeoutMs) throws InterruptedException { + if (timeoutMs <= 0) { + return isOpen; + } + long nowMs = clock.elapsedRealtime(); + long endMs = nowMs + timeoutMs; + if (endMs < nowMs) { + // timeoutMs is large enough for (nowMs + timeoutMs) to rollover. Block indefinitely. + block(); + } else { + while (!isOpen && nowMs < endMs) { + wait(endMs - nowMs); + nowMs = clock.elapsedRealtime(); + } } return isOpen; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java index 1e47aa680d..79eac5d1ad 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java @@ -43,7 +43,7 @@ public class ConditionVariableTest { public void blockWithTimeout_blocksForAtLeastTimeout() throws InterruptedException { ConditionVariable conditionVariable = buildTestConditionVariable(); long startTimeMs = System.currentTimeMillis(); - assertThat(conditionVariable.block(/* timeout= */ 500)).isFalse(); + assertThat(conditionVariable.block(/* timeoutMs= */ 500)).isFalse(); long endTimeMs = System.currentTimeMillis(); assertThat(endTimeMs - startTimeMs).isAtLeast(500L); } @@ -75,6 +75,33 @@ public class ConditionVariableTest { assertThat(conditionVariable.isOpen()).isFalse(); } + @Test + public void blockWithMaxTimeout_blocks() throws InterruptedException { + ConditionVariable conditionVariable = buildTestConditionVariable(); + + AtomicBoolean blockReturned = new AtomicBoolean(); + AtomicBoolean blockWasInterrupted = new AtomicBoolean(); + Thread blockingThread = + new Thread( + () -> { + try { + conditionVariable.block(/* timeoutMs= */ Long.MAX_VALUE); + blockReturned.set(true); + } catch (InterruptedException e) { + blockWasInterrupted.set(true); + } + }); + + blockingThread.start(); + Thread.sleep(500); + assertThat(blockReturned.get()).isFalse(); + + blockingThread.interrupt(); + blockingThread.join(); + assertThat(blockWasInterrupted.get()).isTrue(); + assertThat(conditionVariable.isOpen()).isFalse(); + } + @Test public void open_unblocksBlock() throws InterruptedException { ConditionVariable conditionVariable = buildTestConditionVariable(); diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java index a80d474f9b..0a999c4161 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/TestUtilTest.java @@ -30,7 +30,7 @@ public class TestUtilTest { public void createRobolectricConditionVariable_blockWithTimeout_timesOut() throws InterruptedException { ConditionVariable conditionVariable = TestUtil.createRobolectricConditionVariable(); - assertThat(conditionVariable.block(/* timeout= */ 1)).isFalse(); + assertThat(conditionVariable.block(/* timeoutMs= */ 1)).isFalse(); assertThat(conditionVariable.isOpen()).isFalse(); } @@ -39,7 +39,7 @@ public class TestUtilTest { throws InterruptedException { ConditionVariable conditionVariable = TestUtil.createRobolectricConditionVariable(); long startTimeMs = System.currentTimeMillis(); - assertThat(conditionVariable.block(/* timeout= */ 500)).isFalse(); + assertThat(conditionVariable.block(/* timeoutMs= */ 500)).isFalse(); long endTimeMs = System.currentTimeMillis(); assertThat(endTimeMs - startTimeMs).isAtLeast(500); } From dfc3c507a0fd7a9dfbc0f240c89466d0117f9b95 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 27 May 2020 20:58:42 +0100 Subject: [PATCH 23/80] Don't allow bad MediaSource release implementation to crash player. This also allows subsequent MediaSource instance in the list to still be released successfully. Issue: #7168 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 6fd23a93c5..108d94abb4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -954,7 +954,12 @@ import java.util.concurrent.atomic.AtomicBoolean; startPositionUs); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(/* caller= */ this); + try { + mediaSource.releaseSource(/* caller= */ this); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Failed to release child source.", e); + } mediaSource = null; } } From 7414a86fe047a634c17a425fb86d00fa381becdb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 27 May 2020 21:03:00 +0100 Subject: [PATCH 24/80] Let MediaSourceFactory setDrmSessionManager accept null Issue: #7168 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 5 ++++- .../android/exoplayer2/source/ProgressiveMediaSource.java | 5 ++++- .../android/exoplayer2/source/dash/DashMediaSource.java | 5 ++++- .../google/android/exoplayer2/source/hls/HlsMediaSource.java | 5 ++++- .../exoplayer2/source/smoothstreaming/SsMediaSource.java | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 5a577449fa..0454472abf 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -628,7 +628,10 @@ public class PlayerActivity extends AppCompatActivity @Override public MediaSourceFactory setDrmSessionManager(DrmSessionManager drmSessionManager) { - this.drmSessionManager = drmSessionManager; + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 512fbce4a2..b48e7835ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -174,7 +174,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource @Override public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); return this; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index dcd4b15cae..39cc03dd12 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -307,7 +307,10 @@ public final class DashMediaSource extends BaseMediaSource { @Override public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); return this; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 16dedb6c21..cc2fe618fe 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -323,7 +323,10 @@ public final class HlsMediaSource extends BaseMediaSource @Override public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); return this; } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 8cc848dfa4..89dd8039ef 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -273,7 +273,10 @@ public final class SsMediaSource extends BaseMediaSource @Override public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { Assertions.checkState(!isCreateCalled); - this.drmSessionManager = drmSessionManager; + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); return this; } From c20b85ac60701a34081d8d7488ae434b9792aa9b Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Apr 2020 16:40:06 +0100 Subject: [PATCH 25/80] Update Gradle plugins. PiperOrigin-RevId: 309231983 --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a4823b94ee..d520925fb0 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.novoda:bintray-release:0.9.1' - classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' } } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7fefd1c665..dc65d6734f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip From 2fa2fb73b771512e6c15bec3bfcf1c98b654ed9f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 1 May 2020 13:17:27 +0100 Subject: [PATCH 26/80] Catch correct exception from Context.startService Issue: #7306 PiperOrigin-RevId: 309392633 --- RELEASENOTES.md | 3 ++- .../com/google/android/exoplayer2/offline/DownloadService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 95e2a0581f..ba6d416506 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,11 +17,12 @@ EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). * MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). -* Text +* Text * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles ([#6950](https://github.com/google/ExoPlayer/pull/6950)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. +* DownloadService: Fix "Not allowed to start service" `IllegalStateException`. ### 2.11.4 (2020-04-08) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 819478b80e..f78e9bb545 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -1022,7 +1022,7 @@ public abstract class DownloadService extends Service { try { Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); context.startService(intent); - } catch (IllegalArgumentException e) { + } catch (IllegalStateException e) { // The process is classed as idle by the platform. Starting a background service is not // allowed in this state. Log.w(TAG, "Failed to restart DownloadService (process is idle)."); From 01ff17f3e7da922cc746203f5b793a82fd7bb407 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 1 May 2020 19:50:46 +0100 Subject: [PATCH 27/80] Merge pull request #7304 from AChep:patch-1 PiperOrigin-RevId: 309395364 --- .../com/google/android/exoplayer2/ui/DefaultTimeBar.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 89bcaf84bc..8c20d441b2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -195,7 +195,6 @@ public class DefaultTimeBar extends View implements TimeBar { private final Formatter formatter; private final Runnable stopScrubbingRunnable; private final CopyOnWriteArraySet listeners; - private final int[] locationOnScreen; private final Point touchPosition; private final float density; @@ -249,7 +248,6 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberPaint = new Paint(); scrubberPaint.setAntiAlias(true); listeners = new CopyOnWriteArraySet<>(); - locationOnScreen = new int[2]; touchPosition = new Point(); // Calculate the dimensions and paints for drawn elements. @@ -755,10 +753,7 @@ public class DefaultTimeBar extends View implements TimeBar { } private Point resolveRelativeTouchPosition(MotionEvent motionEvent) { - getLocationOnScreen(locationOnScreen); - touchPosition.set( - ((int) motionEvent.getRawX()) - locationOnScreen[0], - ((int) motionEvent.getRawY()) - locationOnScreen[1]); + touchPosition.set((int) motionEvent.getX(), (int) motionEvent.getY()); return touchPosition; } From d1ff9846704c5d756aa347bdf310f98c50d9ba51 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 May 2020 11:14:52 +0100 Subject: [PATCH 28/80] CronetDataSource: Use standard InterruptedIOException PiperOrigin-RevId: 309710359 --- .../ext/cronet/CronetDataSource.java | 15 +++-------- .../ext/cronet/CronetDataSourceTest.java | 25 ++++++++++--------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 1903e33995..a70de17939 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Predicate; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -83,14 +84,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } - /** Thrown on catching an InterruptedException. */ - public static final class InterruptedIOException extends IOException { - - public InterruptedIOException(InterruptedException e) { - super(e); - } - } - static { ExoPlayerLibraryInfo.registerModule("goog.exo.cronet"); } @@ -440,7 +433,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID); + throw new OpenException(new InterruptedIOException(), dataSpec, Status.INVALID); } // Check for a valid response code. @@ -705,7 +698,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) { throw new IOException("HTTP request with non-empty body must set Content-Type"); } - + // Set the Range header. if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) { StringBuilder rangeValue = new StringBuilder(); @@ -769,7 +762,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } Thread.currentThread().interrupt(); throw new HttpDataSourceException( - new InterruptedIOException(e), + new InterruptedIOException(), castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ); } catch (SocketTimeoutException e) { diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 47f6fa7d2f..228a51f7f4 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -282,7 +283,7 @@ public final class CronetDataSourceTest { fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { // Check for connection not automatically closed. - assertThat(e.getCause() instanceof UnknownHostException).isFalse(); + assertThat(e).hasCauseThat().isNotInstanceOf(UnknownHostException.class); verify(mockUrlRequest, never()).cancel(); verify(mockTransferListener, never()) .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); @@ -320,7 +321,7 @@ public final class CronetDataSourceTest { fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { // Check for connection not automatically closed. - assertThat(e.getCause() instanceof UnknownHostException).isTrue(); + assertThat(e).hasCauseThat().isInstanceOf(UnknownHostException.class); verify(mockUrlRequest, never()).cancel(); verify(mockTransferListener, never()) .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true); @@ -336,7 +337,7 @@ public final class CronetDataSourceTest { dataSourceUnderTest.open(testDataSpec); fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { - assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue(); + assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class); // Check for connection not automatically closed. verify(mockUrlRequest, never()).cancel(); verify(mockTransferListener, never()) @@ -359,7 +360,7 @@ public final class CronetDataSourceTest { dataSourceUnderTest.open(testDataSpec); fail("HttpDataSource.HttpDataSourceException expected"); } catch (HttpDataSourceException e) { - assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue(); + assertThat(e).isInstanceOf(HttpDataSource.InvalidContentTypeException.class); // Check for connection not automatically closed. verify(mockUrlRequest, never()).cancel(); assertThat(testedContentTypes).hasSize(1); @@ -890,8 +891,8 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e instanceof CronetDataSource.OpenException).isTrue(); - assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); + assertThat(e).isInstanceOf(CronetDataSource.OpenException.class); + assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class); assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) .isEqualTo(TEST_CONNECTION_STATUS); timedOutLatch.countDown(); @@ -928,8 +929,8 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e instanceof CronetDataSource.OpenException).isTrue(); - assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + assertThat(e).isInstanceOf(CronetDataSource.OpenException.class); + assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class); assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) .isEqualTo(TEST_INVALID_CONNECTION_STATUS); timedOutLatch.countDown(); @@ -999,8 +1000,8 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e instanceof CronetDataSource.OpenException).isTrue(); - assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); + assertThat(e).isInstanceOf(CronetDataSource.OpenException.class); + assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class); openExceptions.getAndIncrement(); timedOutLatch.countDown(); } @@ -1224,7 +1225,7 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class); timedOutLatch.countDown(); } } @@ -1255,7 +1256,7 @@ public final class CronetDataSourceTest { fail(); } catch (HttpDataSourceException e) { // Expected. - assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); + assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class); timedOutLatch.countDown(); } } From d159f622c2deb98ea78b4256d9053683641cd868 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 4 May 2020 12:48:44 +0100 Subject: [PATCH 29/80] Update initial bitrate estimates. PiperOrigin-RevId: 309720018 --- .../upstream/DefaultBandwidthMeter.java | 365 +++++++++--------- 1 file changed, 183 insertions(+), 182 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 2fbed96a85..44ade5ea4f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -56,19 +56,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default initial Wifi bitrate estimate in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = - new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000}; + new long[] {5_800_000, 3_500_000, 1_900_000, 1_000_000, 520_000}; /** Default initial 2G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = - new long[] {200_000, 148_000, 132_000, 115_000, 95_000}; + new long[] {204_000, 154_000, 139_000, 122_000, 102_000}; /** Default initial 3G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = - new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000}; + new long[] {2_200_000, 1_150_000, 810_000, 640_000, 450_000}; /** Default initial 4G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = - new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000}; + new long[] {4_900_000, 2_300_000, 1_500_000, 970_000, 540_000}; /** * Default initial bitrate estimate used when the device is offline or the network type cannot be @@ -488,244 +488,245 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private static Map createInitialBitrateCountryGroupAssignment() { HashMap countryGroupAssignment = new HashMap<>(); - countryGroupAssignment.put("AD", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("AE", new int[] {1, 4, 4, 4}); + countryGroupAssignment.put("AD", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("AE", new int[] {2, 4, 4, 4}); countryGroupAssignment.put("AF", new int[] {4, 4, 3, 3}); - countryGroupAssignment.put("AG", new int[] {3, 1, 0, 1}); - countryGroupAssignment.put("AI", new int[] {1, 0, 0, 3}); + countryGroupAssignment.put("AG", new int[] {4, 2, 2, 3}); + countryGroupAssignment.put("AI", new int[] {0, 3, 2, 4}); countryGroupAssignment.put("AL", new int[] {1, 2, 0, 1}); - countryGroupAssignment.put("AM", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("AO", new int[] {3, 4, 2, 0}); - countryGroupAssignment.put("AR", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("AS", new int[] {3, 0, 4, 2}); + countryGroupAssignment.put("AM", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("AO", new int[] {3, 4, 3, 1}); + countryGroupAssignment.put("AQ", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("AR", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("AS", new int[] {2, 2, 4, 2}); countryGroupAssignment.put("AT", new int[] {0, 3, 0, 0}); - countryGroupAssignment.put("AU", new int[] {0, 3, 0, 1}); - countryGroupAssignment.put("AW", new int[] {1, 1, 0, 3}); - countryGroupAssignment.put("AX", new int[] {0, 3, 0, 2}); + countryGroupAssignment.put("AU", new int[] {0, 2, 0, 1}); + countryGroupAssignment.put("AW", new int[] {1, 1, 2, 4}); + countryGroupAssignment.put("AX", new int[] {0, 1, 0, 0}); countryGroupAssignment.put("AZ", new int[] {3, 3, 3, 3}); countryGroupAssignment.put("BA", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("BB", new int[] {0, 2, 0, 0}); - countryGroupAssignment.put("BD", new int[] {2, 1, 3, 3}); - countryGroupAssignment.put("BE", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("BB", new int[] {0, 3, 0, 0}); + countryGroupAssignment.put("BD", new int[] {2, 0, 4, 3}); + countryGroupAssignment.put("BE", new int[] {0, 1, 2, 3}); countryGroupAssignment.put("BF", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("BG", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("BH", new int[] {2, 1, 3, 4}); + countryGroupAssignment.put("BH", new int[] {1, 0, 3, 4}); countryGroupAssignment.put("BI", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("BJ", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("BL", new int[] {1, 0, 2, 2}); - countryGroupAssignment.put("BM", new int[] {1, 2, 0, 0}); - countryGroupAssignment.put("BN", new int[] {4, 1, 3, 2}); - countryGroupAssignment.put("BO", new int[] {1, 2, 3, 2}); - countryGroupAssignment.put("BQ", new int[] {1, 1, 2, 4}); - countryGroupAssignment.put("BR", new int[] {2, 3, 3, 2}); - countryGroupAssignment.put("BS", new int[] {2, 1, 1, 4}); + countryGroupAssignment.put("BJ", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("BL", new int[] {1, 0, 4, 3}); + countryGroupAssignment.put("BM", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("BN", new int[] {4, 0, 2, 4}); + countryGroupAssignment.put("BO", new int[] {1, 3, 3, 3}); + countryGroupAssignment.put("BQ", new int[] {1, 0, 1, 0}); + countryGroupAssignment.put("BR", new int[] {2, 4, 3, 1}); + countryGroupAssignment.put("BS", new int[] {3, 1, 1, 3}); countryGroupAssignment.put("BT", new int[] {3, 0, 3, 1}); - countryGroupAssignment.put("BW", new int[] {4, 4, 1, 2}); - countryGroupAssignment.put("BY", new int[] {0, 1, 1, 2}); - countryGroupAssignment.put("BZ", new int[] {2, 2, 2, 1}); - countryGroupAssignment.put("CA", new int[] {0, 3, 1, 3}); - countryGroupAssignment.put("CD", new int[] {4, 4, 2, 2}); - countryGroupAssignment.put("CF", new int[] {4, 4, 3, 0}); - countryGroupAssignment.put("CG", new int[] {3, 4, 2, 4}); - countryGroupAssignment.put("CH", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("BW", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("BY", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("BZ", new int[] {1, 3, 2, 1}); + countryGroupAssignment.put("CA", new int[] {0, 3, 2, 2}); + countryGroupAssignment.put("CD", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("CF", new int[] {4, 3, 2, 2}); + countryGroupAssignment.put("CG", new int[] {3, 4, 1, 1}); + countryGroupAssignment.put("CH", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("CI", new int[] {3, 4, 3, 3}); - countryGroupAssignment.put("CK", new int[] {2, 4, 1, 0}); + countryGroupAssignment.put("CK", new int[] {2, 0, 1, 0}); countryGroupAssignment.put("CL", new int[] {1, 2, 2, 3}); - countryGroupAssignment.put("CM", new int[] {3, 4, 3, 1}); - countryGroupAssignment.put("CN", new int[] {2, 0, 2, 3}); - countryGroupAssignment.put("CO", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("CR", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("CU", new int[] {4, 4, 3, 1}); - countryGroupAssignment.put("CV", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("CM", new int[] {3, 4, 3, 2}); + countryGroupAssignment.put("CN", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("CO", new int[] {2, 3, 3, 2}); + countryGroupAssignment.put("CR", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("CU", new int[] {4, 4, 2, 1}); + countryGroupAssignment.put("CV", new int[] {2, 3, 3, 2}); countryGroupAssignment.put("CW", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("CY", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("CZ", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("DE", new int[] {0, 1, 1, 3}); - countryGroupAssignment.put("DJ", new int[] {4, 3, 4, 1}); - countryGroupAssignment.put("DK", new int[] {0, 0, 1, 1}); - countryGroupAssignment.put("DM", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("DE", new int[] {0, 1, 2, 3}); + countryGroupAssignment.put("DJ", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("DK", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("DM", new int[] {1, 1, 0, 2}); countryGroupAssignment.put("DO", new int[] {3, 3, 4, 4}); countryGroupAssignment.put("DZ", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("EC", new int[] {2, 3, 4, 3}); - countryGroupAssignment.put("EE", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("EG", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("EH", new int[] {2, 0, 3, 3}); - countryGroupAssignment.put("ER", new int[] {4, 2, 2, 0}); + countryGroupAssignment.put("EC", new int[] {2, 3, 4, 2}); + countryGroupAssignment.put("EE", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("EG", new int[] {3, 4, 2, 1}); + countryGroupAssignment.put("EH", new int[] {2, 0, 3, 1}); + countryGroupAssignment.put("ER", new int[] {4, 2, 4, 4}); countryGroupAssignment.put("ES", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("ET", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("ET", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("FI", new int[] {0, 0, 1, 0}); - countryGroupAssignment.put("FJ", new int[] {3, 0, 3, 3}); - countryGroupAssignment.put("FK", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("FM", new int[] {4, 0, 4, 0}); - countryGroupAssignment.put("FO", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("FR", new int[] {1, 0, 3, 1}); - countryGroupAssignment.put("GA", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("GB", new int[] {0, 1, 3, 3}); - countryGroupAssignment.put("GD", new int[] {2, 0, 4, 4}); - countryGroupAssignment.put("GE", new int[] {1, 1, 1, 4}); - countryGroupAssignment.put("GF", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("GG", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("GH", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("GI", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("GL", new int[] {2, 2, 0, 2}); - countryGroupAssignment.put("GM", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("FJ", new int[] {3, 0, 4, 4}); + countryGroupAssignment.put("FK", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("FM", new int[] {3, 2, 4, 1}); + countryGroupAssignment.put("FO", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("FR", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("GA", new int[] {3, 2, 2, 2}); + countryGroupAssignment.put("GB", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("GD", new int[] {1, 1, 3, 1}); + countryGroupAssignment.put("GE", new int[] {1, 0, 1, 4}); + countryGroupAssignment.put("GF", new int[] {2, 0, 1, 3}); + countryGroupAssignment.put("GG", new int[] {1, 0, 0, 0}); + countryGroupAssignment.put("GH", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("GI", new int[] {4, 4, 0, 0}); + countryGroupAssignment.put("GL", new int[] {2, 1, 1, 2}); + countryGroupAssignment.put("GM", new int[] {4, 3, 2, 4}); countryGroupAssignment.put("GN", new int[] {3, 4, 4, 2}); - countryGroupAssignment.put("GP", new int[] {2, 1, 1, 4}); - countryGroupAssignment.put("GQ", new int[] {4, 4, 3, 0}); - countryGroupAssignment.put("GR", new int[] {1, 1, 0, 2}); - countryGroupAssignment.put("GT", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("GU", new int[] {1, 2, 4, 4}); - countryGroupAssignment.put("GW", new int[] {4, 4, 4, 1}); + countryGroupAssignment.put("GP", new int[] {2, 1, 3, 4}); + countryGroupAssignment.put("GQ", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("GR", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("GT", new int[] {3, 2, 2, 2}); + countryGroupAssignment.put("GU", new int[] {1, 0, 2, 2}); + countryGroupAssignment.put("GW", new int[] {3, 4, 4, 3}); countryGroupAssignment.put("GY", new int[] {3, 2, 1, 1}); countryGroupAssignment.put("HK", new int[] {0, 2, 3, 4}); - countryGroupAssignment.put("HN", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("HN", new int[] {3, 1, 3, 3}); countryGroupAssignment.put("HR", new int[] {1, 1, 0, 1}); countryGroupAssignment.put("HT", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("HU", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("ID", new int[] {3, 2, 3, 4}); + countryGroupAssignment.put("ID", new int[] {2, 2, 2, 3}); countryGroupAssignment.put("IE", new int[] {1, 0, 1, 1}); - countryGroupAssignment.put("IL", new int[] {0, 0, 2, 3}); + countryGroupAssignment.put("IL", new int[] {1, 0, 2, 3}); countryGroupAssignment.put("IM", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("IN", new int[] {2, 2, 4, 4}); - countryGroupAssignment.put("IO", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("IN", new int[] {2, 2, 4, 3}); + countryGroupAssignment.put("IO", new int[] {4, 4, 2, 3}); countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 2}); - countryGroupAssignment.put("IR", new int[] {3, 0, 2, 2}); + countryGroupAssignment.put("IR", new int[] {3, 0, 2, 1}); countryGroupAssignment.put("IS", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("IT", new int[] {1, 0, 1, 2}); + countryGroupAssignment.put("IT", new int[] {1, 1, 1, 2}); countryGroupAssignment.put("JE", new int[] {1, 0, 0, 1}); - countryGroupAssignment.put("JM", new int[] {2, 3, 3, 1}); - countryGroupAssignment.put("JO", new int[] {1, 2, 1, 2}); - countryGroupAssignment.put("JP", new int[] {0, 2, 1, 1}); - countryGroupAssignment.put("KE", new int[] {3, 4, 4, 3}); - countryGroupAssignment.put("KG", new int[] {1, 1, 2, 2}); - countryGroupAssignment.put("KH", new int[] {1, 0, 4, 4}); - countryGroupAssignment.put("KI", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("KM", new int[] {4, 3, 2, 3}); - countryGroupAssignment.put("KN", new int[] {1, 0, 1, 3}); - countryGroupAssignment.put("KP", new int[] {4, 2, 4, 2}); - countryGroupAssignment.put("KR", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("KW", new int[] {2, 3, 1, 1}); - countryGroupAssignment.put("KY", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("JM", new int[] {3, 3, 3, 4}); + countryGroupAssignment.put("JO", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("JP", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("KE", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("KG", new int[] {2, 0, 2, 2}); + countryGroupAssignment.put("KH", new int[] {1, 0, 4, 3}); + countryGroupAssignment.put("KI", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("KM", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("KN", new int[] {1, 0, 2, 4}); + countryGroupAssignment.put("KP", new int[] {4, 2, 0, 2}); + countryGroupAssignment.put("KR", new int[] {0, 1, 0, 1}); + countryGroupAssignment.put("KW", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("KY", new int[] {3, 1, 2, 3}); + countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 2}); countryGroupAssignment.put("LA", new int[] {2, 2, 1, 1}); countryGroupAssignment.put("LB", new int[] {3, 2, 0, 0}); countryGroupAssignment.put("LC", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("LI", new int[] {0, 0, 2, 4}); - countryGroupAssignment.put("LK", new int[] {2, 1, 2, 3}); - countryGroupAssignment.put("LR", new int[] {3, 4, 3, 1}); - countryGroupAssignment.put("LS", new int[] {3, 3, 2, 0}); + countryGroupAssignment.put("LI", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("LK", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("LR", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("LS", new int[] {3, 3, 2, 2}); countryGroupAssignment.put("LT", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("LU", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("LV", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("LY", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("MA", new int[] {2, 1, 2, 1}); - countryGroupAssignment.put("MC", new int[] {0, 0, 0, 1}); + countryGroupAssignment.put("LY", new int[] {3, 3, 4, 3}); + countryGroupAssignment.put("MA", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("MC", new int[] {0, 4, 0, 0}); countryGroupAssignment.put("MD", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("ME", new int[] {1, 2, 1, 2}); - countryGroupAssignment.put("MF", new int[] {1, 1, 1, 1}); - countryGroupAssignment.put("MG", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("ME", new int[] {1, 3, 1, 2}); + countryGroupAssignment.put("MF", new int[] {2, 3, 1, 1}); + countryGroupAssignment.put("MG", new int[] {3, 4, 2, 3}); countryGroupAssignment.put("MH", new int[] {4, 0, 2, 4}); countryGroupAssignment.put("MK", new int[] {1, 0, 0, 0}); countryGroupAssignment.put("ML", new int[] {4, 4, 2, 0}); - countryGroupAssignment.put("MM", new int[] {3, 3, 1, 2}); - countryGroupAssignment.put("MN", new int[] {2, 3, 2, 3}); + countryGroupAssignment.put("MM", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("MN", new int[] {2, 3, 1, 1}); countryGroupAssignment.put("MO", new int[] {0, 0, 4, 4}); - countryGroupAssignment.put("MP", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 4}); - countryGroupAssignment.put("MR", new int[] {4, 2, 4, 2}); - countryGroupAssignment.put("MS", new int[] {1, 2, 3, 3}); - countryGroupAssignment.put("MT", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("MU", new int[] {2, 2, 3, 4}); - countryGroupAssignment.put("MV", new int[] {4, 3, 0, 2}); - countryGroupAssignment.put("MW", new int[] {3, 2, 1, 0}); - countryGroupAssignment.put("MX", new int[] {2, 4, 4, 3}); - countryGroupAssignment.put("MY", new int[] {2, 2, 3, 3}); - countryGroupAssignment.put("MZ", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("NA", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("NC", new int[] {2, 0, 3, 3}); - countryGroupAssignment.put("NE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("NF", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("NG", new int[] {3, 4, 3, 1}); - countryGroupAssignment.put("NI", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("NL", new int[] {0, 2, 3, 3}); - countryGroupAssignment.put("NO", new int[] {0, 1, 1, 0}); + countryGroupAssignment.put("MP", new int[] {0, 2, 1, 2}); + countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 3}); + countryGroupAssignment.put("MR", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("MS", new int[] {1, 4, 3, 4}); + countryGroupAssignment.put("MT", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("MU", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("MV", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("MW", new int[] {3, 1, 1, 1}); + countryGroupAssignment.put("MX", new int[] {2, 4, 3, 3}); + countryGroupAssignment.put("MY", new int[] {2, 1, 3, 3}); + countryGroupAssignment.put("MZ", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("NA", new int[] {4, 3, 3, 3}); + countryGroupAssignment.put("NC", new int[] {2, 0, 4, 4}); + countryGroupAssignment.put("NE", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("NF", new int[] {1, 2, 2, 0}); + countryGroupAssignment.put("NG", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("NI", new int[] {3, 2, 4, 3}); + countryGroupAssignment.put("NL", new int[] {0, 2, 3, 2}); + countryGroupAssignment.put("NO", new int[] {0, 2, 1, 0}); countryGroupAssignment.put("NP", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("NR", new int[] {4, 0, 3, 1}); + countryGroupAssignment.put("NR", new int[] {4, 0, 3, 2}); countryGroupAssignment.put("NZ", new int[] {0, 0, 1, 2}); - countryGroupAssignment.put("OM", new int[] {3, 2, 1, 3}); - countryGroupAssignment.put("PA", new int[] {1, 3, 3, 4}); - countryGroupAssignment.put("PE", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("PF", new int[] {2, 2, 0, 1}); - countryGroupAssignment.put("PG", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("OM", new int[] {2, 3, 0, 2}); + countryGroupAssignment.put("PA", new int[] {1, 3, 3, 3}); + countryGroupAssignment.put("PE", new int[] {2, 4, 4, 4}); + countryGroupAssignment.put("PF", new int[] {2, 1, 1, 1}); + countryGroupAssignment.put("PG", new int[] {4, 3, 3, 2}); countryGroupAssignment.put("PH", new int[] {3, 0, 3, 4}); - countryGroupAssignment.put("PK", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("PL", new int[] {1, 0, 1, 3}); + countryGroupAssignment.put("PK", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("PL", new int[] {1, 0, 1, 2}); countryGroupAssignment.put("PM", new int[] {0, 2, 2, 0}); - countryGroupAssignment.put("PR", new int[] {1, 2, 3, 3}); - countryGroupAssignment.put("PS", new int[] {3, 3, 2, 4}); + countryGroupAssignment.put("PR", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("PS", new int[] {3, 3, 1, 4}); countryGroupAssignment.put("PT", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("PW", new int[] {2, 1, 2, 0}); - countryGroupAssignment.put("PY", new int[] {2, 0, 2, 3}); - countryGroupAssignment.put("QA", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("PW", new int[] {1, 1, 3, 0}); + countryGroupAssignment.put("PY", new int[] {2, 0, 3, 3}); + countryGroupAssignment.put("QA", new int[] {2, 3, 1, 1}); countryGroupAssignment.put("RE", new int[] {1, 0, 2, 2}); countryGroupAssignment.put("RO", new int[] {0, 1, 1, 2}); countryGroupAssignment.put("RS", new int[] {1, 2, 0, 0}); - countryGroupAssignment.put("RU", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("RW", new int[] {4, 4, 2, 4}); + countryGroupAssignment.put("RU", new int[] {0, 1, 0, 1}); + countryGroupAssignment.put("RW", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("SA", new int[] {2, 2, 2, 1}); - countryGroupAssignment.put("SB", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("SB", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("SC", new int[] {4, 2, 0, 1}); - countryGroupAssignment.put("SD", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("SD", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("SE", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("SG", new int[] {0, 2, 3, 3}); - countryGroupAssignment.put("SH", new int[] {4, 4, 2, 3}); - countryGroupAssignment.put("SI", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("SJ", new int[] {2, 0, 2, 4}); + countryGroupAssignment.put("SG", new int[] {1, 0, 3, 3}); + countryGroupAssignment.put("SH", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("SI", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SJ", new int[] {2, 2, 2, 4}); countryGroupAssignment.put("SK", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("SL", new int[] {4, 3, 3, 3}); - countryGroupAssignment.put("SM", new int[] {0, 0, 2, 4}); - countryGroupAssignment.put("SN", new int[] {3, 4, 4, 2}); - countryGroupAssignment.put("SO", new int[] {3, 4, 4, 3}); - countryGroupAssignment.put("SR", new int[] {2, 2, 1, 0}); - countryGroupAssignment.put("SS", new int[] {4, 3, 4, 3}); - countryGroupAssignment.put("ST", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("SV", new int[] {2, 3, 3, 4}); + countryGroupAssignment.put("SL", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("SM", new int[] {0, 0, 1, 2}); + countryGroupAssignment.put("SN", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("SO", new int[] {3, 4, 3, 4}); + countryGroupAssignment.put("SR", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("SS", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("ST", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("SV", new int[] {2, 2, 4, 4}); countryGroupAssignment.put("SX", new int[] {2, 4, 1, 0}); - countryGroupAssignment.put("SY", new int[] {4, 3, 2, 1}); + countryGroupAssignment.put("SY", new int[] {4, 3, 1, 1}); countryGroupAssignment.put("SZ", new int[] {4, 4, 3, 4}); - countryGroupAssignment.put("TC", new int[] {1, 2, 1, 1}); - countryGroupAssignment.put("TD", new int[] {4, 4, 4, 2}); - countryGroupAssignment.put("TG", new int[] {3, 3, 1, 0}); - countryGroupAssignment.put("TH", new int[] {1, 3, 4, 4}); + countryGroupAssignment.put("TC", new int[] {1, 2, 1, 0}); + countryGroupAssignment.put("TD", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("TG", new int[] {3, 2, 1, 0}); + countryGroupAssignment.put("TH", new int[] {1, 3, 3, 3}); countryGroupAssignment.put("TJ", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("TL", new int[] {4, 2, 4, 4}); - countryGroupAssignment.put("TM", new int[] {4, 1, 2, 2}); - countryGroupAssignment.put("TN", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("TO", new int[] {3, 3, 3, 1}); - countryGroupAssignment.put("TR", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("TT", new int[] {1, 3, 1, 2}); - countryGroupAssignment.put("TV", new int[] {4, 2, 2, 4}); + countryGroupAssignment.put("TM", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("TN", new int[] {2, 1, 1, 1}); + countryGroupAssignment.put("TO", new int[] {4, 3, 4, 4}); + countryGroupAssignment.put("TR", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("TT", new int[] {1, 3, 2, 4}); + countryGroupAssignment.put("TV", new int[] {4, 2, 3, 4}); countryGroupAssignment.put("TW", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("TZ", new int[] {3, 3, 4, 3}); - countryGroupAssignment.put("UA", new int[] {0, 2, 1, 2}); - countryGroupAssignment.put("UG", new int[] {4, 3, 3, 2}); - countryGroupAssignment.put("US", new int[] {1, 1, 3, 3}); - countryGroupAssignment.put("UY", new int[] {2, 2, 1, 1}); - countryGroupAssignment.put("UZ", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("VA", new int[] {1, 2, 4, 2}); - countryGroupAssignment.put("VC", new int[] {2, 0, 2, 4}); - countryGroupAssignment.put("VE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("VG", new int[] {3, 0, 1, 3}); - countryGroupAssignment.put("VI", new int[] {1, 1, 4, 4}); - countryGroupAssignment.put("VN", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("VU", new int[] {4, 1, 3, 1}); - countryGroupAssignment.put("WS", new int[] {3, 3, 3, 2}); + countryGroupAssignment.put("TZ", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("UA", new int[] {0, 3, 1, 1}); + countryGroupAssignment.put("UG", new int[] {3, 2, 2, 3}); + countryGroupAssignment.put("US", new int[] {0, 1, 2, 2}); + countryGroupAssignment.put("UY", new int[] {2, 1, 2, 2}); + countryGroupAssignment.put("UZ", new int[] {2, 2, 3, 2}); + countryGroupAssignment.put("VA", new int[] {0, 2, 2, 2}); + countryGroupAssignment.put("VC", new int[] {2, 3, 0, 2}); + countryGroupAssignment.put("VE", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("VG", new int[] {3, 1, 2, 4}); + countryGroupAssignment.put("VI", new int[] {1, 4, 4, 3}); + countryGroupAssignment.put("VN", new int[] {0, 1, 3, 4}); + countryGroupAssignment.put("VU", new int[] {4, 0, 3, 3}); + countryGroupAssignment.put("WS", new int[] {3, 2, 4, 3}); countryGroupAssignment.put("XK", new int[] {1, 2, 1, 0}); countryGroupAssignment.put("YE", new int[] {4, 4, 4, 3}); countryGroupAssignment.put("YT", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("ZA", new int[] {2, 4, 2, 2}); - countryGroupAssignment.put("ZM", new int[] {3, 2, 2, 1}); - countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 1}); + countryGroupAssignment.put("ZA", new int[] {2, 3, 2, 2}); + countryGroupAssignment.put("ZM", new int[] {3, 2, 3, 3}); + countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 3}); return Collections.unmodifiableMap(countryGroupAssignment); } } From 2112b722b58682a43932fa9bed39dec6a40ec375 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 May 2020 14:54:54 +0100 Subject: [PATCH 30/80] Add missing @Player.State in action schedule PiperOrigin-RevId: 309735092 --- .../java/com/google/android/exoplayer2/testutil/Action.java | 4 ++-- .../google/android/exoplayer2/testutil/ActionSchedule.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index b65accdf3f..2d6beff416 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -685,13 +685,13 @@ public abstract class Action { */ public static final class WaitForPlaybackState extends Action { - private final int targetPlaybackState; + @Player.State private final int targetPlaybackState; /** * @param tag A tag to use for logging. * @param targetPlaybackState The playback state to wait for. */ - public WaitForPlaybackState(String tag, int targetPlaybackState) { + public WaitForPlaybackState(String tag, @Player.State int targetPlaybackState) { super(tag, "WaitForPlaybackState"); this.targetPlaybackState = targetPlaybackState; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index f6ab4b9924..4800df662c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -433,7 +433,7 @@ public final class ActionSchedule { * @param targetPlaybackState The target playback state. * @return The builder, for convenience. */ - public Builder waitForPlaybackState(int targetPlaybackState) { + public Builder waitForPlaybackState(@Player.State int targetPlaybackState) { return apply(new WaitForPlaybackState(tag, targetPlaybackState)); } From 8304cf34807671e26a0456a537398258655e6f2e Mon Sep 17 00:00:00 2001 From: samrobinson Date: Tue, 5 May 2020 13:04:58 +0100 Subject: [PATCH 31/80] Change SilenceSkippingAudioProcessor to not rely on the frame MSB. PiperOrigin-RevId: 309925306 --- .../audio/SilenceSkippingAudioProcessor.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index 2a98d2fb25..454007194f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -17,11 +17,13 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit @@ -39,19 +41,9 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. */ private static final long PADDING_SILENCE_US = 20_000; - /** - * The absolute level below which an individual PCM sample is classified as silent. Note: the - * specified value will be rounded so that the threshold check only depends on the more - * significant byte, for efficiency. - */ + /** The absolute level below which an individual PCM sample is classified as silent. */ private static final short SILENCE_THRESHOLD_LEVEL = 1024; - /** - * Threshold for classifying an individual PCM sample as silent based on its more significant - * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding. - */ - private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8; - /** Trimming states. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -325,9 +317,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * classified as a noisy frame, or the limit of the buffer if no such frame exists. */ private int findNoisePosition(ByteBuffer buffer) { + Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. - for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) { - if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + for (int i = buffer.position(); i < buffer.limit(); i += 2) { + if (Math.abs(buffer.getShort(i)) > SILENCE_THRESHOLD_LEVEL) { // Round to the start of the frame. return bytesPerFrame * (i / bytesPerFrame); } @@ -340,9 +333,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * from the byte position to the limit are classified as silent. */ private int findNoiseLimit(ByteBuffer buffer) { + Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. - for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) { - if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + for (int i = buffer.limit() - 2; i >= buffer.position(); i -= 2) { + if (Math.abs(buffer.getShort(i)) > SILENCE_THRESHOLD_LEVEL) { // Return the start of the next frame. return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame; } From 4862c7e6a8d259238eec96c4e49513fc386dfdca Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 May 2020 10:25:48 +0100 Subject: [PATCH 32/80] Upgrade OkHttp to 3.12.11. PiperOrigin-RevId: 310114401 --- RELEASENOTES.md | 1 + extensions/okhttp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ba6d416506..2d9412aa3b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,7 @@ ([#6950](https://github.com/google/ExoPlayer/pull/6950)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.11. * DownloadService: Fix "Not allowed to start service" `IllegalStateException`. ### 2.11.4 (2020-04-08) ### diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 41eac7c661..e0d12b5c1c 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -41,7 +41,7 @@ dependencies { // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 // Since OkHttp is distributed as a jar rather than an aar, Gradle won't // stop us from making this mistake! - api 'com.squareup.okhttp3:okhttp:3.12.8' + api 'com.squareup.okhttp3:okhttp:3.12.11' } ext { From 5f6a489bd1e0134c9c3cc898cc8feb6a547000a9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 6 May 2020 20:57:06 +0100 Subject: [PATCH 33/80] Merge pull request #7324 from tpiDev:cronet/migrate-to-play-services-17-0-0 PiperOrigin-RevId: 310115628 --- RELEASENOTES.md | 4 ++ extensions/cronet/README.md | 44 ++++++++++++ extensions/cronet/build.gradle | 2 +- .../ext/cronet/CronetDataSourceTest.java | 67 ++++++++++++++++--- 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2d9412aa3b..9ffeda9365 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,10 @@ * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. * OkHttp extension: Upgrade OkHttp dependency to 3.12.11. +* Cronet extension: Default to using the Cronet implementation in Google Play + Services rather than Cronet Embedded. This allows Cronet to be used with a + negligible increase in application size, compared to approximately 8MB when + embedding the library. * DownloadService: Fix "Not allowed to start service" `IllegalStateException`. ### 2.11.4 (2020-04-08) ### diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index dc64b862b6..112ad26bba 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -20,6 +20,10 @@ Alternatively, you can clone the ExoPlayer repository and depend on the module locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. +Note that by default, the extension will use the Cronet implementation in +Google Play Services. If you prefer, it's also possible to embed the Cronet +implementation directly into your application. See below for more details. + [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md ## Using the extension ## @@ -47,6 +51,46 @@ new DefaultDataSourceFactory( ``` respectively. +## Choosing between Google Play Services Cronet and Cronet Embedded ## + +The underlying Cronet implementation is available both via a [Google Play +Services](https://developers.google.com/android/guides/overview) API, and as a +library that can be embedded directly into your application. When you depend on +`com.google.android.exoplayer:extension-cronet:2.X.X`, the library will _not_ be +embedded into your application by default. The extension will attempt to use the +Cronet implementation in Google Play Services. The benefits of this approach +are: + +* A negligible increase in the size of your application. +* The Cronet implementation is updated automatically by Google Play Services. + +If Google Play Services is not available on a device, `CronetDataSourceFactory` +will fall back to creating `DefaultHttpDataSource` instances, or +`HttpDataSource` instances created by a `fallbackFactory` that you can specify. + +It's also possible to embed the Cronet implementation directly into your +application. To do this, add an additional gradle dependency to the Cronet +Embedded library: + +```gradle +implementation 'com.google.android.exoplayer:extension-cronet:2.X.X' +implementation 'org.chromium.net:cronet-embedded:XX.XXXX.XXX' +``` + +where `XX.XXXX.XXX` is the version of the library that you wish to use. The +extension will automatically detect and use the library. Embedding will add +approximately 8MB to your application, however it may be suitable if: + +* Your application is likely to be used in markets where Google Play Services is + not widely available. +* You want to control the exact version of the Cronet implementation being used. + +If you do embed the library, you can specify which implementation should +be preferred if the Google Play Services implementation is also available. This +is controlled by a `preferGMSCoreCronet` parameter, which can be passed to the +`CronetEngineWrapper` constructor (GMS Core is another name for Google Play +Services). + ## Links ## * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*` diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index d5b7a99f96..1c80a21ecc 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:76.3809.111' + api "com.google.android.gms:play-services-cronet:17.0.0" implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 228a51f7f4..093a09499d 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -57,7 +58,6 @@ import org.chromium.net.CronetEngine; import org.chromium.net.NetworkException; import org.chromium.net.UrlRequest; import org.chromium.net.UrlResponseInfo; -import org.chromium.net.impl.UrlResponseInfoImpl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -140,15 +140,62 @@ public final class CronetDataSourceTest { private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) { ArrayList> responseHeaderList = new ArrayList<>(); - responseHeaderList.addAll(testResponseHeader.entrySet()); - return new UrlResponseInfoImpl( - Collections.singletonList(url), - statusCode, - null, // httpStatusText - responseHeaderList, - false, // wasCached - null, // negotiatedProtocol - null); // proxyServer + Map> responseHeaderMap = new HashMap<>(); + for (Map.Entry entry : testResponseHeader.entrySet()) { + responseHeaderList.add(entry); + responseHeaderMap.put(entry.getKey(), Collections.singletonList(entry.getValue())); + } + return new UrlResponseInfo() { + @Override + public String getUrl() { + return url; + } + + @Override + public List getUrlChain() { + return Collections.singletonList(url); + } + + @Override + public int getHttpStatusCode() { + return statusCode; + } + + @Override + public String getHttpStatusText() { + return null; + } + + @Override + public List> getAllHeadersAsList() { + return responseHeaderList; + } + + @Override + public Map> getAllHeaders() { + return responseHeaderMap; + } + + @Override + public boolean wasCached() { + return false; + } + + @Override + public String getNegotiatedProtocol() { + return null; + } + + @Override + public String getProxyServer() { + return null; + } + + @Override + public long getReceivedByteCount() { + return 0; + } + }; } @Test From ad1dffcae8e3ff73a32716a47d0b837c7d4ffe59 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Mon, 11 May 2020 15:24:15 +0100 Subject: [PATCH 34/80] Make the base values of SilenceSkippingAudioProcessor configurable. Issue:#6705 PiperOrigin-RevId: 310907118 --- RELEASENOTES.md | 2 + .../exoplayer2/audio/DefaultAudioSink.java | 17 +++++- .../audio/SilenceSkippingAudioProcessor.java | 56 ++++++++++++++----- .../SilenceSkippingAudioProcessorTest.java | 29 +++++++++- 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9ffeda9365..cc03d8621c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### Next release ### +* Enable the configuration of `SilenceSkippingAudioProcessor` + ([#6705](https://github.com/google/ExoPlayer/issues/6705)). * Add `SilenceMediaSource.Factory` to support tags ([PR #7245](https://github.com/google/ExoPlayer/pull/7245)). * Avoid throwing an exception while parsing fragmented MP4 default sample diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ba31c118e7..32a819bf81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -120,9 +120,20 @@ public final class DefaultAudioSink implements AudioSink { /** * Creates a new default chain of audio processors, with the user-defined {@code - * audioProcessors} applied before silence skipping and playback parameters. + * audioProcessors} applied before silence skipping and speed adjustment processors. */ public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { + this(audioProcessors, new SilenceSkippingAudioProcessor(), new SonicAudioProcessor()); + } + + /** + * Creates a new default chain of audio processors, with the user-defined {@code + * audioProcessors} applied before silence skipping and speed adjustment processors. + */ + public DefaultAudioProcessorChain( + AudioProcessor[] audioProcessors, + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor, + SonicAudioProcessor sonicAudioProcessor) { // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array // rather than using Arrays.copyOf. this.audioProcessors = new AudioProcessor[audioProcessors.length + 2]; @@ -132,8 +143,8 @@ public final class DefaultAudioSink implements AudioSink { /* dest= */ this.audioProcessors, /* destPos= */ 0, /* length= */ audioProcessors.length); - silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); - sonicAudioProcessor = new SonicAudioProcessor(); + this.silenceSkippingAudioProcessor = silenceSkippingAudioProcessor; + this.sonicAudioProcessor = sonicAudioProcessor; this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index 454007194f..7ddb491525 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -32,17 +32,20 @@ import java.nio.ByteOrder; public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { /** - * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify - * that part of audio as silent, in microseconds. + * The default value for {@link #SilenceSkippingAudioProcessor(long, long, short) + * minimumSilenceDurationUs}. */ - private static final long MINIMUM_SILENCE_DURATION_US = 150_000; + public static final long DEFAULT_MINIMUM_SILENCE_DURATION_US = 150_000; /** - * The duration of silence by which to extend non-silent sections, in microseconds. The value must - * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. + * The default value for {@link #SilenceSkippingAudioProcessor(long, long, short) + * paddingSilenceUs}. */ - private static final long PADDING_SILENCE_US = 20_000; - /** The absolute level below which an individual PCM sample is classified as silent. */ - private static final short SILENCE_THRESHOLD_LEVEL = 1024; + public static final long DEFAULT_PADDING_SILENCE_US = 20_000; + /** + * The default value for {@link #SilenceSkippingAudioProcessor(long, long, short) + * silenceThresholdLevel}. + */ + public static final short DEFAULT_SILENCE_THRESHOLD_LEVEL = 1024; /** Trimming states. */ @Documented @@ -60,8 +63,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { /** State when the input is silent. */ private static final int STATE_SILENT = 2; + private final long minimumSilenceDurationUs; + private final long paddingSilenceUs; + private final short silenceThresholdLevel; private int bytesPerFrame; - private boolean enabled; /** @@ -83,8 +88,31 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { private boolean hasOutputNoise; private long skippedFrames; - /** Creates a new silence trimming audio processor. */ + /** Creates a new silence skipping audio processor. */ public SilenceSkippingAudioProcessor() { + this( + DEFAULT_MINIMUM_SILENCE_DURATION_US, + DEFAULT_PADDING_SILENCE_US, + DEFAULT_SILENCE_THRESHOLD_LEVEL); + } + + /** + * Creates a new silence skipping audio processor. + * + * @param minimumSilenceDurationUs The minimum duration of audio that must be below {@code + * silenceThresholdLevel} to classify that part of audio as silent, in microseconds. + * @param paddingSilenceUs The duration of silence by which to extend non-silent sections, in + * microseconds. The value must not exceed {@code minimumSilenceDurationUs}. + * @param silenceThresholdLevel The absolute level below which an individual PCM sample is + * classified as silent. + */ + public SilenceSkippingAudioProcessor( + long minimumSilenceDurationUs, long paddingSilenceUs, short silenceThresholdLevel) { + Assertions.checkArgument(paddingSilenceUs <= minimumSilenceDurationUs); + this.minimumSilenceDurationUs = minimumSilenceDurationUs; + this.paddingSilenceUs = paddingSilenceUs; + this.silenceThresholdLevel = silenceThresholdLevel; + maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY; paddingBuffer = Util.EMPTY_BYTE_ARRAY; } @@ -158,11 +186,11 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { protected void onFlush() { if (enabled) { bytesPerFrame = inputAudioFormat.bytesPerFrame; - int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame; + int maybeSilenceBufferSize = durationUsToFrames(minimumSilenceDurationUs) * bytesPerFrame; if (maybeSilenceBuffer.length != maybeSilenceBufferSize) { maybeSilenceBuffer = new byte[maybeSilenceBufferSize]; } - paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame; + paddingSize = durationUsToFrames(paddingSilenceUs) * bytesPerFrame; if (paddingBuffer.length != paddingSize) { paddingBuffer = new byte[paddingSize]; } @@ -320,7 +348,7 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. for (int i = buffer.position(); i < buffer.limit(); i += 2) { - if (Math.abs(buffer.getShort(i)) > SILENCE_THRESHOLD_LEVEL) { + if (Math.abs(buffer.getShort(i)) > silenceThresholdLevel) { // Round to the start of the frame. return bytesPerFrame * (i / bytesPerFrame); } @@ -336,7 +364,7 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. for (int i = buffer.limit() - 2; i >= buffer.position(); i -= 2) { - if (Math.abs(buffer.getShort(i)) > SILENCE_THRESHOLD_LEVEL) { + if (Math.abs(buffer.getShort(i)) > silenceThresholdLevel) { // Return the start of the next frame. return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java index 6783c96055..4933460e01 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessorTest.java @@ -204,7 +204,34 @@ public final class SilenceSkippingAudioProcessorTest { } @Test - public void testSkipThenFlush_resetsSkippedFrameCount() throws Exception { + public void customPaddingValue_hasCorrectOutputAndSkippedFrameCounts() throws Exception { + // Given a signal that alternates between silence and noise. + InputBufferProvider inputBufferProvider = + getInputBufferProviderForAlternatingSilenceAndNoise( + TEST_SIGNAL_SILENCE_DURATION_MS, + TEST_SIGNAL_NOISE_DURATION_MS, + TEST_SIGNAL_FRAME_COUNT); + + // When processing the entire signal with a larger than normal padding silence. + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor = + new SilenceSkippingAudioProcessor( + SilenceSkippingAudioProcessor.DEFAULT_MINIMUM_SILENCE_DURATION_US, + /* paddingSilenceUs= */ 21_000, + SilenceSkippingAudioProcessor.DEFAULT_SILENCE_THRESHOLD_LEVEL); + silenceSkippingAudioProcessor.setEnabled(true); + silenceSkippingAudioProcessor.configure(AUDIO_FORMAT); + silenceSkippingAudioProcessor.flush(); + assertThat(silenceSkippingAudioProcessor.isActive()).isTrue(); + long totalOutputFrames = + process(silenceSkippingAudioProcessor, inputBufferProvider, /* inputBufferSize= */ 120); + + // The right number of frames are skipped/output. + assertThat(totalOutputFrames).isEqualTo(58379); + assertThat(silenceSkippingAudioProcessor.getSkippedFrames()).isEqualTo(41621); + } + + @Test + public void skipThenFlush_resetsSkippedFrameCount() throws Exception { // Given a signal that alternates between silence and noise. InputBufferProvider inputBufferProvider = getInputBufferProviderForAlternatingSilenceAndNoise( From f116f298125fa852c172a47b4cd29544f112d2a1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 May 2020 10:06:56 +0100 Subject: [PATCH 35/80] Prevent leaking of the Thread.interrupt flag to other LoadTasks PiperOrigin-RevId: 311290214 --- .../android/exoplayer2/upstream/Loader.java | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index a498f510dd..5b7846f5ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -32,6 +32,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; /** * Manages the background loading of {@link Loadable}s. @@ -56,6 +57,21 @@ public final class Loader implements LoaderErrorThrower { /** * Cancels the load. + * + *

Loadable implementations should ensure that a currently executing {@link #load()} call + * will exit reasonably quickly after this method is called. The {@link #load()} call may exit + * either by returning or by throwing an {@link IOException}. + * + *

If there is a currently executing {@link #load()} call, then the thread on which that call + * is being made will be interrupted immediately after the call to this method. Hence + * implementations do not need to (and should not attempt to) interrupt the loading thread + * themselves. + * + *

Although the loading thread will be interrupted, Loadable implementations should not use + * the interrupted status of the loading thread in {@link #load()} to determine whether the load + * has been canceled. This approach is not robust [Internal ref: b/79223737]. Instead, + * implementations should use their own flag to signal cancelation (for example, using {@link + * AtomicBoolean}). */ void cancelLoad(); @@ -309,10 +325,9 @@ public final class Loader implements LoaderErrorThrower { private static final String TAG = "LoadTask"; private static final int MSG_START = 0; - private static final int MSG_CANCEL = 1; - private static final int MSG_END_OF_SOURCE = 2; - private static final int MSG_IO_EXCEPTION = 3; - private static final int MSG_FATAL_ERROR = 4; + private static final int MSG_FINISH = 1; + private static final int MSG_IO_EXCEPTION = 2; + private static final int MSG_FATAL_ERROR = 3; public final int defaultMinRetryCount; @@ -323,8 +338,8 @@ public final class Loader implements LoaderErrorThrower { @Nullable private IOException currentError; private int errorCount; - @Nullable private volatile Thread executorThread; - private volatile boolean canceled; + @Nullable private Thread executorThread; + private boolean canceled; private volatile boolean released; public LoadTask(Looper looper, T loadable, Loader.Callback callback, @@ -356,16 +371,21 @@ public final class Loader implements LoaderErrorThrower { this.released = released; currentError = null; if (hasMessages(MSG_START)) { + // The task has not been given to the executor yet. + canceled = true; removeMessages(MSG_START); if (!released) { - sendEmptyMessage(MSG_CANCEL); + sendEmptyMessage(MSG_FINISH); } } else { - canceled = true; - loadable.cancelLoad(); - Thread executorThread = this.executorThread; - if (executorThread != null) { - executorThread.interrupt(); + // The task has been given to the executor. + synchronized (this) { + canceled = true; + loadable.cancelLoad(); + @Nullable Thread executorThread = this.executorThread; + if (executorThread != null) { + executorThread.interrupt(); + } } } if (released) { @@ -384,8 +404,12 @@ public final class Loader implements LoaderErrorThrower { @Override public void run() { try { - executorThread = Thread.currentThread(); - if (!canceled) { + boolean shouldLoad; + synchronized (this) { + shouldLoad = !canceled; + executorThread = Thread.currentThread(); + } + if (shouldLoad) { TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName()); try { loadable.load(); @@ -393,8 +417,13 @@ public final class Loader implements LoaderErrorThrower { TraceUtil.endSection(); } } + synchronized (this) { + executorThread = null; + // Clear the interrupted flag if set, to avoid it leaking into a subsequent task. + Thread.interrupted(); + } if (!released) { - sendEmptyMessage(MSG_END_OF_SOURCE); + sendEmptyMessage(MSG_FINISH); } } catch (IOException e) { if (!released) { @@ -404,7 +433,7 @@ public final class Loader implements LoaderErrorThrower { // The load was canceled. Assertions.checkState(canceled); if (!released) { - sendEmptyMessage(MSG_END_OF_SOURCE); + sendEmptyMessage(MSG_FINISH); } } catch (Exception e) { // This should never happen, but handle it anyway. @@ -453,10 +482,7 @@ public final class Loader implements LoaderErrorThrower { return; } switch (msg.what) { - case MSG_CANCEL: - callback.onLoadCanceled(loadable, nowMs, durationMs, false); - break; - case MSG_END_OF_SOURCE: + case MSG_FINISH: try { callback.onLoadCompleted(loadable, nowMs, durationMs); } catch (RuntimeException e) { From bc96d3a93c3c834337bc9165e98cca30c596e85c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 15 May 2020 10:58:46 +0100 Subject: [PATCH 36/80] Merge pull request #7367 from inv3rse:keep-paused-state-during-buffering PiperOrigin-RevId: 311623784 --- RELEASENOTES.md | 3 +++ .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cc03d8621c..4ac4648d0c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,9 @@ Services rather than Cronet Embedded. This allows Cronet to be used with a negligible increase in application size, compared to approximately 8MB when embedding the library. +* MediaSession extension: Set session playback state to BUFFERING only when + actually playing ([#7367](https://github.com/google/ExoPlayer/pull/7367), + [#7206](https://github.com/google/ExoPlayer/issues/7206)). * DownloadService: Fix "Not allowed to start service" `IllegalStateException`. ### 2.11.4 (2020-04-08) ### diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 41a2071827..0847686d21 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -987,7 +987,9 @@ public final class MediaSessionConnector { @Player.State int exoPlayerPlaybackState, boolean playWhenReady) { switch (exoPlayerPlaybackState) { case Player.STATE_BUFFERING: - return PlaybackStateCompat.STATE_BUFFERING; + return playWhenReady + ? PlaybackStateCompat.STATE_BUFFERING + : PlaybackStateCompat.STATE_PAUSED; case Player.STATE_READY: return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; case Player.STATE_ENDED: From 4736a102f875c4a8b7ac53e7c9d75fe85032d7e7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 May 2020 00:27:33 +0100 Subject: [PATCH 37/80] Attach ExoMediaCryptoType for progressive streams PiperOrigin-RevId: 311628160 --- .../android/exoplayer2/source/ProgressiveMediaPeriod.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index efdfdf15a8..277e17410d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -730,6 +730,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate); } } + if (trackFormat.drmInitData != null) { + trackFormat = + trackFormat.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(trackFormat.drmInitData)); + } trackArray[i] = new TrackGroup(trackFormat); } isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; From 8736324d0e3fc348ead3b03d8d2a23e515e1c66d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 May 2020 10:48:11 +0100 Subject: [PATCH 38/80] Clean up samples list - Add Widevine AV1 streams - Remove SD and HD only Widevine streams (we don't need so many!) - Simplify naming PiperOrigin-RevId: 311697741 --- demos/main/src/main/assets/media.exolist.json | 284 ++++++------------ 1 file changed, 96 insertions(+), 188 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 4375bdf3a7..ac5737d195 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -3,208 +3,147 @@ "name": "YouTube DASH", "samples": [ { - "name": "Google Glass (MP4,H264)", + "name": "Google Glass H264 (MP4)", "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", "extension": "mpd" }, { - "name": "Google Play (MP4,H264)", + "name": "Google Play H264 (MP4)", "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", "extension": "mpd" }, { - "name": "Google Glass (WebM,VP9)", + "name": "Google Glass VP9 (WebM)", "uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", "extension": "mpd" }, { - "name": "Google Play (WebM,VP9)", + "name": "Google Play VP9 (WebM)", "uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", "extension": "mpd" } ] }, { - "name": "Widevine DASH Policy Tests (GTS)", + "name": "Widevine GTS policy tests", "samples": [ { - "name": "WV: HDCP not specified", + "name": "SW secure crypto (L3)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=d286538032258a1c&provider=widevine_test" + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test" }, { - "name": "WV: HDCP not required", + "name": "SW secure decode", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=48fcc369939ac96c&provider=widevine_test" + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test" }, { - "name": "WV: HDCP required", + "name": "HW secure crypto", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=e06c39f1151da3df&provider=widevine_test" + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test" }, { - "name": "WV: Secure video path required (MP4,H264)", + "name": "HW secure decode", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test" }, { - "name": "WV: Secure video path required (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" - }, - { - "name": "WV: Secure video path required (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=0894c7c8719b28a0&provider=widevine_test" - }, - { - "name": "WV: HDCP + secure video path required", + "name": "HW secure all (L1)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=efd045b1eb61888a&provider=widevine_test" + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test" }, { - "name": "WV: 30s license duration (fails at ~30s)", + "name": "30s license (fails at ~30s)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=f9a34cab7b05881a&provider=widevine_test" + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test" + }, + { + "name": "HDCP not required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test" + }, + { + "name": "HDCP 1.0 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test" + }, + { + "name": "HDCP 2.0 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test" + }, + { + "name": "HDCP 2.1 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test" + }, + { + "name": "HDCP 2.2 required", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test" + }, + { + "name": "HDCP no digital output", + "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test" } ] }, { - "name": "Widevine HDCP Capabilities Tests", + "name": "Widevine DASH H264 (MP4)", "samples": [ { - "name": "WV: HDCP: None (not required)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_None&provider=widevine_test" - }, - { - "name": "WV: HDCP: 1.0 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V1&provider=widevine_test" - }, - { - "name": "WV: HDCP: 2.0 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2&provider=widevine_test" - }, - { - "name": "WV: HDCP: 2.1 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_1&provider=widevine_test" - }, - { - "name": "WV: HDCP: 2.2 required", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_V2_2&provider=widevine_test" - }, - { - "name": "WV: HDCP: No digital output", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=HDCP_NO_DIGTAL_OUTPUT&provider=widevine_test" - } - ] - }, - { - "name": "Widevine DASH: MP4,H264", - "samples": [ - { - "name": "WV: Clear SD & HD (MP4,H264)", + "name": "Clear", "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" }, { - "name": "WV: Clear SD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_sd.mpd" - }, - { - "name": "WV: Clear HD (MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_hd.mpd" - }, - { - "name": "WV: Clear UHD (MP4,H264)", + "name": "Clear UHD", "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd" }, { - "name": "WV: Secure SD & HD (cenc,MP4,H264)", + "name": "Secure (cenc)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD (cenc,MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure HD (cenc,MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure UHD (cenc,MP4,H264)", + "name": "Secure UHD (cenc)", "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD & HD (cbc1,MP4,H264)", + "name": "Secure (cbc1)", "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD (cbc1,MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure HD (cbc1,MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure UHD (cbc1,MP4,H264)", + "name": "Secure UHD (cbc1)", "uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD & HD (cbcs,MP4,H264)", + "name": "Secure (cbcs)", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD (cbcs,MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure HD (cbcs,MP4,H264)", - "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure UHD (cbcs,MP4,H264)", + "name": "Secure UHD (cbcs)", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" @@ -212,68 +151,36 @@ ] }, { - "name": "Widevine DASH: WebM,VP9", + "name": "Widevine DASH VP9 (WebM)", "samples": [ { - "name": "WV: Clear SD & HD (WebM,VP9)", + "name": "Clear", "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd" }, { - "name": "WV: Clear SD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_sd.mpd" - }, - { - "name": "WV: Clear HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_hd.mpd" - }, - { - "name": "WV: Clear UHD (WebM,VP9)", + "name": "Clear UHD", "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" }, { - "name": "WV: Secure Fullsample SD & HD (WebM,VP9)", + "name": "Secure (full-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure Fullsample SD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Fullsample HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Fullsample UHD (WebM,VP9)", + "name": "Secure UHD (full-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure Subsample SD & HD (WebM,VP9)", + "name": "Secure (sub-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure Subsample SD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Subsample HD (WebM,VP9)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure Subsample UHD (WebM,VP9)", + "name": "Secure UHD (sub-sample)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" @@ -281,50 +188,51 @@ ] }, { - "name": "Widevine DASH: MP4,H265", + "name": "Widevine DASH H265 (MP4)", "samples": [ { - "name": "WV: Clear SD & HD (MP4,H265)", + "name": "Clear", "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" }, { - "name": "WV: Clear SD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_sd.mpd" - }, - { - "name": "WV: Clear HD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_hd.mpd" - }, - { - "name": "WV: Clear UHD (MP4,H265)", + "name": "Clear UHD", "uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd" }, { - "name": "WV: Secure SD & HD (MP4,H265)", + "name": "Secure", "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_sd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure HD (MP4,H265)", - "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_hd.mpd", - "drm_scheme": "widevine", - "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" - }, - { - "name": "WV: Secure UHD (MP4,H265)", + "name": "Secure UHD", "uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" } ] }, + { + "name": "Widevine AV1 (WebM)", + "samples": [ + { + "name": "Clear", + "uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm" + }, + { + "name": "Secure L3", + "uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test" + }, + { + "name": "Secure L1", + "uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test" + } + ] + }, { "name": "SmoothStreaming", "samples": [ @@ -355,7 +263,7 @@ "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" }, { - "name": "Apple master playlist advanced (fMP4)", + "name": "Apple master playlist advanced (FMP4)", "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" }, { From 09025d3912cefbe13f8e2dbf7017a4a57a7c548e Mon Sep 17 00:00:00 2001 From: samrobinson Date: Mon, 18 May 2020 10:32:23 +0100 Subject: [PATCH 39/80] Allow MP3 files to play with size greater than 2GB. Issue:#7337 PiperOrigin-RevId: 312042768 --- RELEASENOTES.md | 2 ++ .../com/google/android/exoplayer2/extractor/mp3/XingSeeker.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4ac4648d0c..4126bdb5e0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,8 @@ marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. * Fix assertion failure in `SampleQueue` when playing DASH streams with EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). +* MP3: Allow MP3 files with XING headers that are larger than 2GB to be played + ([#7337](https://github.com/google/ExoPlayer/issues/7337)). * MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). * Text diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index db1a0199ac..c51b68a7c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -60,7 +60,7 @@ import com.google.android.exoplayer2.util.Util; return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long dataSize = frame.readUnsignedIntToInt(); + long dataSize = frame.readUnsignedInt(); long[] tableOfContents = new long[100]; for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); From 1d8dd763f0bd3f0f54707860129d125908ce35a6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 May 2020 14:19:56 +0100 Subject: [PATCH 40/80] Fix SimpleCache.getCachedLength rollover bug & improve test coverage PiperOrigin-RevId: 312266156 --- .../upstream/cache/CachedContent.java | 12 +- .../upstream/cache/SimpleCacheTest.java | 111 +++++++++++++----- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 7abb9b3896..b6a55c8da4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkState; + import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import java.io.File; import java.util.TreeSet; @@ -115,12 +117,18 @@ import java.util.TreeSet; * @return the length of the cached or not cached data block length. */ public long getCachedBytesLength(long position, long length) { + checkArgument(position >= 0); + checkArgument(length >= 0); SimpleCacheSpan span = getSpan(position); if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length); } long queryEndPosition = position + length; + if (queryEndPosition < 0) { + // The calculation rolled over (length is probably Long.MAX_VALUE). + queryEndPosition = Long.MAX_VALUE; + } long currentEndPosition = span.position + span.length; if (currentEndPosition < queryEndPosition) { for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) { @@ -151,7 +159,7 @@ import java.util.TreeSet; */ public SimpleCacheSpan setLastTouchTimestamp( SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) { - Assertions.checkState(cachedSpans.remove(cacheSpan)); + checkState(cachedSpans.remove(cacheSpan)); File file = cacheSpan.file; if (updateFile) { File directory = file.getParentFile(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 4d9a936c4e..8294dee383 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -284,38 +284,93 @@ public class SimpleCacheTest { } @Test - public void testGetCachedLength() throws Exception { + public void getCachedLength_noCachedContent_returnsNegativeMaxHoleLength() { SimpleCache simpleCache = getSimpleCache(); - CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); - // No cached bytes, returns -'length' - assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(-100); - - // Position value doesn't affect the return value - assertThat(simpleCache.getCachedLength(KEY_1, 20, 100)).isEqualTo(-100); - - addCache(simpleCache, KEY_1, 0, 15); - - // Returns the length of a single span - assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(15); - - // Value is capped by the 'length' - assertThat(simpleCache.getCachedLength(KEY_1, 0, 10)).isEqualTo(10); - - addCache(simpleCache, KEY_1, 15, 35); - - // Returns the length of two adjacent spans - assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50); - - addCache(simpleCache, KEY_1, 60, 10); - - // Not adjacent span doesn't affect return value - assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50); - - // Returns length of hole up to the next cached span - assertThat(simpleCache.getCachedLength(KEY_1, 55, 100)).isEqualTo(-5); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100)) + .isEqualTo(-100); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE)) + .isEqualTo(-Long.MAX_VALUE); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100)) + .isEqualTo(-100); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE)) + .isEqualTo(-Long.MAX_VALUE); + } + @Test + public void getCachedLength_returnsNegativeHoleLength() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + addCache(simpleCache, KEY_1, /* position= */ 50, /* length= */ 50); simpleCache.releaseHoleSpan(cacheSpan); + + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100)) + .isEqualTo(-50); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE)) + .isEqualTo(-50); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100)) + .isEqualTo(-30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE)) + .isEqualTo(-30); + } + + @Test + public void getCachedLength_returnsCachedLength() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 50); + simpleCache.releaseHoleSpan(cacheSpan); + + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100)) + .isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE)) + .isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100)) + .isEqualTo(30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE)) + .isEqualTo(30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 15)) + .isEqualTo(15); + } + + @Test + public void getCachedLength_withMultipleAdjacentSpans_returnsCachedLength() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 25); + addCache(simpleCache, KEY_1, /* position= */ 25, /* length= */ 25); + simpleCache.releaseHoleSpan(cacheSpan); + + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100)) + .isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE)) + .isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100)) + .isEqualTo(30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE)) + .isEqualTo(30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 15)) + .isEqualTo(15); + } + + @Test + public void getCachedLength_withMultipleNonAdjacentSpans_returnsCachedLength() throws Exception { + SimpleCache simpleCache = getSimpleCache(); + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, /* position= */ 0); + addCache(simpleCache, KEY_1, /* position= */ 0, /* length= */ 10); + addCache(simpleCache, KEY_1, /* position= */ 15, /* length= */ 35); + simpleCache.releaseHoleSpan(cacheSpan); + + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ 100)) + .isEqualTo(10); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 0, /* length= */ Long.MAX_VALUE)) + .isEqualTo(10); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 100)) + .isEqualTo(30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ Long.MAX_VALUE)) + .isEqualTo(30); + assertThat(simpleCache.getCachedLength(KEY_1, /* position= */ 20, /* length= */ 15)) + .isEqualTo(15); } /* Tests https://github.com/google/ExoPlayer/issues/3260 case. */ From 5927d0302bc7e583b9f99c44a72d7a382b49bcdc Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 May 2020 14:32:52 +0100 Subject: [PATCH 41/80] Adding instructions on how to build and run ExoPlayer demo apps Issue:#7338 PiperOrigin-RevId: 312470913 --- demos/README.md | 21 +++++++++++++++++++++ demos/cast/README.md | 3 +++ demos/gl/README.md | 3 +++ demos/main/README.md | 3 +++ demos/surface/README.md | 3 +++ 5 files changed, 33 insertions(+) diff --git a/demos/README.md b/demos/README.md index 7e62249db1..2360e01137 100644 --- a/demos/README.md +++ b/demos/README.md @@ -2,3 +2,24 @@ This directory contains applications that demonstrate how to use ExoPlayer. Browse the individual demos and their READMEs to learn more. + +## Running a demo ## + +### From Android Studio ### + +* File -> New -> Import Project -> Specify the root ExoPlayer folder. +* Choose the demo from the run configuration dropdown list. +* Click Run. + +### Using gradle from the command line: ### + +* Open a Terminal window at the root ExoPlayer folder. +* Run `./gradlew projects` to show all projects. Demo projects start with `demo`. +* Run `./gradlew ::tasks` to view the list of available tasks for +the demo project. Choose an install option from the `Install tasks` section. +* Run `./gradlew ::`. + +**Example**: + +`./gradlew :demo:installNoExtensionsDebug` installs the main ExoPlayer demo app + in debug mode with no extensions. diff --git a/demos/cast/README.md b/demos/cast/README.md index 2c68a5277a..fd682433f9 100644 --- a/demos/cast/README.md +++ b/demos/cast/README.md @@ -2,3 +2,6 @@ This folder contains a demo application that showcases ExoPlayer integration with Google Cast. + +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. diff --git a/demos/gl/README.md b/demos/gl/README.md index 12dabe902b..9bffc3edea 100644 --- a/demos/gl/README.md +++ b/demos/gl/README.md @@ -8,4 +8,7 @@ drawn using an Android canvas, and includes the current frame's presentation timestamp, to show how to get the timestamp of the frame currently in the off-screen surface texture. +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. + [GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView diff --git a/demos/main/README.md b/demos/main/README.md index bdb04e5ba8..00072c070b 100644 --- a/demos/main/README.md +++ b/demos/main/README.md @@ -3,3 +3,6 @@ This is the main ExoPlayer demo application. It uses ExoPlayer to play a number of test streams. It can be used as a starting point or reference project when developing other applications that make use of the ExoPlayer library. + +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. diff --git a/demos/surface/README.md b/demos/surface/README.md index 312259dbf6..3febb23feb 100644 --- a/demos/surface/README.md +++ b/demos/surface/README.md @@ -18,4 +18,7 @@ called, and because you can move output off-screen easily (`setOutputSurface` can't take a `null` surface, so the player has to use a `DummySurface`, which doesn't handle protected output on all devices). +Please see the [demos README](../README.md) for instructions on how to build and +run this demo. + [SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl From b05e9944ea95c2b1a341610568e5cfbe8df6f333 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 26 May 2020 08:52:37 +0100 Subject: [PATCH 42/80] Update TrackSelectionDialogBuilder to use androidx compat Dialog. This ensure style themes are correctly applied. issue:#7357 PiperOrigin-RevId: 313145345 --- RELEASENOTES.md | 5 ++++- library/ui/build.gradle | 1 + .../android/exoplayer2/ui/TrackSelectionDialogBuilder.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4126bdb5e0..16a0f33e60 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,7 +21,10 @@ ([#7337](https://github.com/google/ExoPlayer/issues/7337)). * MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). -* Text +* UI: + * Update `TrackSelectionDialogBuilder` to use androidx compat Dialog + ([#7357](https://github.com/google/ExoPlayer/issues/7357)). +* Text: * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles ([#6950](https://github.com/google/ExoPlayer/pull/6950)). * AV1 extension: Add a heuristic to determine the default number of threads diff --git a/library/ui/build.gradle b/library/ui/build.gradle index b6bf139963..8727ba416a 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation project(modulePrefix + 'library-core') api 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java index f8a016bc8b..5c91645a4c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java @@ -15,12 +15,12 @@ */ package com.google.android.exoplayer2.ui; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; From 7e02066f6e7934dc1c063775d35bb460e66e5a01 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 27 May 2020 19:01:12 +0100 Subject: [PATCH 43/80] Merge pull request #7422 from noamtamim:bandwidthmeter-5g PiperOrigin-RevId: 313372995 --- .../android/exoplayer2/upstream/DefaultBandwidthMeter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 44ade5ea4f..6cbc17d3e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -203,10 +203,11 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); - // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback. + // Assume default Wifi and 4G bitrate for Ethernet and 5G, respectively, to prevent using the + // slower fallback. result.append( C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); - result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); return result; } From 9d6a46bccd35e1074a67b0871a646c9898199ce8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 27 May 2020 21:57:37 +0100 Subject: [PATCH 44/80] Clean up release notes --- RELEASENOTES.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 16a0f33e60..e448756339 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,15 +1,14 @@ # Release notes # -### Next release ### +### 2.11.5 (not yet released) ### +* Add `SilenceMediaSource.Factory` to support tags. * Enable the configuration of `SilenceSkippingAudioProcessor` ([#6705](https://github.com/google/ExoPlayer/issues/6705)). -* Add `SilenceMediaSource.Factory` to support tags - ([PR #7245](https://github.com/google/ExoPlayer/pull/7245)). -* Avoid throwing an exception while parsing fragmented MP4 default sample - values where the most-significant bit is set - ([#7207](https://github.com/google/ExoPlayer/issues/7207)). -* Fix `AdsMediaSource` child `MediaSource`s not being released. +* DownloadService: Fix "Not allowed to start service" `IllegalStateException` + ([#7306](https://github.com/google/ExoPlayer/issues/7306)). +* Ads: + * Fix `AdsMediaSource` child `MediaSource`s not being released. * DASH: * Merge trick play adaptation sets (i.e., adaptation sets marked with `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as @@ -17,27 +16,29 @@ marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. * Fix assertion failure in `SampleQueue` when playing DASH streams with EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). -* MP3: Allow MP3 files with XING headers that are larger than 2GB to be played +* FMP4: Avoid throwing an exception while parsing default sample values whose + most significant bits are set + ([#7207](https://github.com/google/ExoPlayer/issues/7207)). +* MP3: Fix issue parsing the XING headers belonging to files larger than 2GB ([#7337](https://github.com/google/ExoPlayer/issues/7337)). * MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265 samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)). * UI: - * Update `TrackSelectionDialogBuilder` to use androidx compat Dialog + * Fix `DefaultTimeBar` to respect touch transformations + ([#7303](https://github.com/google/ExoPlayer/issues/7303)). + * Update `TrackSelectionDialogBuilder` to use AndroidX Compat Dialog ([#7357](https://github.com/google/ExoPlayer/issues/7357)). -* Text: - * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles - ([#6950](https://github.com/google/ExoPlayer/pull/6950)). -* AV1 extension: Add a heuristic to determine the default number of threads - used for AV1 playback using the extension. -* OkHttp extension: Upgrade OkHttp dependency to 3.12.11. +* Text: Use anti-aliasing and bitmap filtering when displaying bitmap + subtitles. * Cronet extension: Default to using the Cronet implementation in Google Play Services rather than Cronet Embedded. This allows Cronet to be used with a negligible increase in application size, compared to approximately 8MB when embedding the library. -* MediaSession extension: Set session playback state to BUFFERING only when - actually playing ([#7367](https://github.com/google/ExoPlayer/pull/7367), - [#7206](https://github.com/google/ExoPlayer/issues/7206)). -* DownloadService: Fix "Not allowed to start service" `IllegalStateException`. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.11. +* MediaSession extension: Set session playback state to `BUFFERING` only when + actually playing ([#7206](https://github.com/google/ExoPlayer/issues/7206)). +* AV1 extension: Add a heuristic to determine the default number of threads + used for AV1 playback using the extension. ### 2.11.4 (2020-04-08) ### From 68059070f4069ccec06f9ab21e780f46e299ebb7 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 22 Apr 2020 16:33:42 +0100 Subject: [PATCH 45/80] Add missing nullable annotations The MediaSessionConnector gets a Bundle passed to the MediaSession.Callback from the framework which can be null. This needs to be properly annotated with @Nullable. Issue: #7234 PiperOrigin-RevId: 307822764 --- RELEASENOTES.md | 7 +++-- .../mediasession/MediaSessionConnector.java | 30 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e448756339..a645a17713 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,8 +35,11 @@ negligible increase in application size, compared to approximately 8MB when embedding the library. * OkHttp extension: Upgrade OkHttp dependency to 3.12.11. -* MediaSession extension: Set session playback state to `BUFFERING` only when - actually playing ([#7206](https://github.com/google/ExoPlayer/issues/7206)). +* MediaSession extension: + * One set the playback state to `BUFFERING` if `playWhenReady` is true + ([#7206](https://github.com/google/ExoPlayer/issues/7206)). + * Add missing `@Nullable` annotations to `MediaSessionConnector` + ([#7234](https://github.com/google/ExoPlayer/issues/7234)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 0847686d21..646351aefe 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -218,25 +218,25 @@ public final class MediaSessionConnector { * * @param mediaId The media id of the media item to be prepared. * @param playWhenReady Whether playback should be started after preparation. - * @param extras A {@link Bundle} of extras passed by the media controller. + * @param extras A {@link Bundle} of extras passed by the media controller, may be null. */ - void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras); + void onPrepareFromMediaId(String mediaId, boolean playWhenReady, @Nullable Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. * * @param query The search query. * @param playWhenReady Whether playback should be started after preparation. - * @param extras A {@link Bundle} of extras passed by the media controller. + * @param extras A {@link Bundle} of extras passed by the media controller, may be null. */ - void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras); + void onPrepareFromSearch(String query, boolean playWhenReady, @Nullable Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. * * @param uri The {@link Uri} of the media item to be prepared. * @param playWhenReady Whether playback should be started after preparation. - * @param extras A {@link Bundle} of extras passed by the media controller. + * @param extras A {@link Bundle} of extras passed by the media controller, may be null. */ - void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras); + void onPrepareFromUri(Uri uri, boolean playWhenReady, @Nullable Bundle extras); } /** @@ -336,7 +336,7 @@ public final class MediaSessionConnector { void onSetRating(Player player, RatingCompat rating); /** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */ - void onSetRating(Player player, RatingCompat rating, Bundle extras); + void onSetRating(Player player, RatingCompat rating, @Nullable Bundle extras); } /** Handles requests for enabling or disabling captions. */ @@ -381,7 +381,7 @@ public final class MediaSessionConnector { * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching * changes to the player. * @param action The name of the action which was sent by a media controller. - * @param extras Optional extras sent by a media controller. + * @param extras Optional extras sent by a media controller, may be null. */ void onCustomAction( Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras); @@ -1321,42 +1321,42 @@ public final class MediaSessionConnector { } @Override - public void onPrepareFromMediaId(String mediaId, Bundle extras) { + public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras); } } @Override - public void onPrepareFromSearch(String query, Bundle extras) { + public void onPrepareFromSearch(String query, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras); } } @Override - public void onPrepareFromUri(Uri uri, Bundle extras) { + public void onPrepareFromUri(Uri uri, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras); } } @Override - public void onPlayFromMediaId(String mediaId, Bundle extras) { + public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras); } } @Override - public void onPlayFromSearch(String query, Bundle extras) { + public void onPlayFromSearch(String query, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras); } } @Override - public void onPlayFromUri(Uri uri, Bundle extras) { + public void onPlayFromUri(Uri uri, @Nullable Bundle extras) { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras); } @@ -1370,7 +1370,7 @@ public final class MediaSessionConnector { } @Override - public void onSetRating(RatingCompat rating, Bundle extras) { + public void onSetRating(RatingCompat rating, @Nullable Bundle extras) { if (canDispatchSetRating()) { ratingCallback.onSetRating(player, rating, extras); } From d14f559e942792b007f81f8e1bf60cb39279850f Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Apr 2020 15:01:44 +0100 Subject: [PATCH 46/80] Fix message indexing bug. We keep an index hint for the next pending player message. This hint wasn't updated correctly when messages are removed due to a timeline update. This change makes sure to only use the hint locally in one method so that it doesn't need to be updated anywhere else and also adds the "hint" suffix to the variable name to make it clearer that it's just a hint and there are no guarantees this index actually exists anymore. issue:#7278 PiperOrigin-RevId: 309217614 --- RELEASENOTES.md | 6 +++++- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a645a17713..697c78265f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,7 +5,11 @@ * Add `SilenceMediaSource.Factory` to support tags. * Enable the configuration of `SilenceSkippingAudioProcessor` ([#6705](https://github.com/google/ExoPlayer/issues/6705)). -* DownloadService: Fix "Not allowed to start service" `IllegalStateException` +* Fix bug where `PlayerMessages` throw an exception after `MediaSources` + are removed from the playlist + ([#7278](https://github.com/google/ExoPlayer/issues/7278)). +* Fix "Not allowed to start service" `IllegalStateException` in + `DownloadService` ([#7306](https://github.com/google/ExoPlayer/issues/7306)). * Ads: * Fix `AdsMediaSource` child `MediaSource`s not being released. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 108d94abb4..ddc54e9e6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -120,7 +120,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; - private int nextPendingMessageIndex; + private int nextPendingMessageIndexHint; private boolean deliverPendingMessageAtStartPositionRequired; public ExoPlayerImplInternal( @@ -928,7 +928,6 @@ import java.util.concurrent.atomic.AtomicBoolean; pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } pendingMessages.clear(); - nextPendingMessageIndex = 0; } MediaPeriodId mediaPeriodId = resetPosition @@ -1082,6 +1081,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) int currentPeriodIndex = playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); + int nextPendingMessageIndex = Math.min(nextPendingMessageIndexHint, pendingMessages.size()); PendingMessageInfo previousInfo = nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; while (previousInfo != null @@ -1127,6 +1127,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ? pendingMessages.get(nextPendingMessageIndex) : null; } + nextPendingMessageIndexHint = nextPendingMessageIndex; } private void ensureStopped(Renderer renderer) throws ExoPlaybackException { From 10b8eff727368276424caf1398ac7adc93b25690 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 6 Dec 2019 13:21:46 +0000 Subject: [PATCH 47/80] Don't overwrite MP4 container fps using capture fps The capture frame rate is currently available both via Format.metadata and decoded in Format.frameRate. As the container Format.frameRate may be useful to apps, only store the capture frame rate in metadata (apps will need to decode it but can now access the container frame rate too). PiperOrigin-RevId: 284165711 --- RELEASENOTES.md | 2 + .../extractor/mp4/MetadataUtil.java | 13 +--- .../assets/mp4/sample_android_slow_motion.mp4 | Bin 0 -> 43481 bytes .../mp4/sample_android_slow_motion.mp4.0.dump | 61 ++++++++++++++++++ .../mp4/sample_android_slow_motion.mp4.1.dump | 61 ++++++++++++++++++ .../mp4/sample_android_slow_motion.mp4.2.dump | 61 ++++++++++++++++++ .../mp4/sample_android_slow_motion.mp4.3.dump | 61 ++++++++++++++++++ .../extractor/mp4/Mp4ExtractorTest.java | 5 ++ 8 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 library/core/src/test/assets/mp4/sample_android_slow_motion.mp4 create mode 100644 library/core/src/test/assets/mp4/sample_android_slow_motion.mp4.0.dump create mode 100644 library/core/src/test/assets/mp4/sample_android_slow_motion.mp4.1.dump create mode 100644 library/core/src/test/assets/mp4/sample_android_slow_motion.mp4.2.dump create mode 100644 library/core/src/test/assets/mp4/sample_android_slow_motion.mp4.3.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 697c78265f..cfb05784c7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ marked with the `C.ROLE_FLAG_TRICK_PLAY` flag. * Fix assertion failure in `SampleQueue` when playing DASH streams with EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)). +* MP4: Store the Android capture frame rate only in `Format.metadata`. + `Format.frameRate` now stores the calculated frame rate. * FMP4: Avoid throwing an exception while parsing default sample values whose most significant bits are set ([#7207](https://github.com/google/ExoPlayer/issues/7207)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index 732a69cecd..4f65836b76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import java.nio.ByteBuffer; /** Utilities for handling metadata in MP4. */ /* package */ final class MetadataUtil { @@ -282,7 +281,6 @@ import java.nio.ByteBuffer; private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; - private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; private MetadataUtil() {} @@ -312,15 +310,8 @@ import java.nio.ByteBuffer; Metadata.Entry entry = mdtaMetadata.get(i); if (entry instanceof MdtaMetadataEntry) { MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; - if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) - && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { - try { - float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); - format = format.copyWithFrameRate(fps); - format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring invalid framerate"); - } + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)) { + format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); } } } diff --git a/library/core/src/test/assets/mp4/sample_android_slow_motion.mp4 b/library/core/src/test/assets/mp4/sample_android_slow_motion.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e5594c83e1f6f1ec8f21b79132525a1178cb7b71 GIT binary patch literal 43481 zcmZs?1C%AfvNe3#wr$(CZA{y??U{B@+nlziZQHh|ZChW@#e4U^|BH2^B63IM&a8~A zQ)|_!0{{RB&0IVkES>Fb0RUjYU;CxZhHl18whpXJ005|=t-ZY)006MDb+a(}vi~6< zM_+y50YHGipZ}HrPvZ;zueRX-X8vCu@E511i<6=CS5U{r`X6J0{72%y*uKX7Z~3or z{;zStzWM-9|Dz+Rsfmm8mxpU=w&g{!Ub+i2k=YP(c)a0Kr zToWhLzd3*9ZyeUu$%gQsJ`k_7i;>NjpX==6{O@}F&58Y6voHHs{*&_`0NCHNmi%)6 z>RD6{7#Fovq3L;Qx$%9n-B%J^ww?ugL>Y{wCQPTG>148##TI^$SA# z2V`mY1-Y3zIa}J>eSuhiL1TMc2SXQ2BOB9yCjTWO_*a1btFx26r3t;Up@WO7lPSHK z!{0E@e~15<65(&i)Xv!6#PsiWYqtF7*2Vqu|1Ke_iQzw{|JMh6UB1j;2J1`zE1c$ohg_SY}lIxN9GskPnIc_4%F7Z0M9b_S$y{M4?}cUtR#9R1#{5~gK2x}4A3ZmI+^ z*wMw2rAgJL1fXEWEv@+y{GX&zb>ucJLnfshaYDIj9=|;d=fTv=IT-#p>s+whOgmEE zd%?|%1@h&WN^{Sa@tKStD-J3f@7TPH8B92 zyhk~8pPgF6BU{MLAs%dE-|=q|^VEL&WflArbL}H%+J} z>NSerQI3r{(~@k?)K7t0ciJXBAm3Gz3DdN z#f zk05~ilX>F&F*?ZO)|uQfVKHP<;?p{4 z%*LLD-9RPWK4*Fl>}jkb1|2cMjM+2}(Fxw=W}PG%8)q;wmN>yUR^LD=z(hcefEg?j7*cfT+KKbcgvFqtx1W3rN^#J==x39* zooQFbyz#88U@9SZ490p~ed z3gwGBy;b)NRSQP@_`NQ9zk`^tek5dkcGdV>>va%XcQ81^MEHt5`4!Z$z$bT5IZy`p z$t;K+Cnnl+vb@B1+7O-14kER$s~!pm@*Q1Kjx~(H8>1Xh@G6JK>RsNwxe6d(LC=6E z4$)89`i7=RZ6Bgg{j%E~w0`_i-fSzL$dIJS;TE%^b8Fs(+t$mfoLtx{75skWD?tK- zima4J<$L03O2D`ys3x%rdyVwdGqMJUAb9h$1OGGLXvG0WOm?x*HI0#p+%**dO|X8N z1A70uM`y^;TbBa(!JA10XvVt$zF--Be=-3QzhK6$XiO0WbgkD$BAP9qmR*xt-)z_x zTk%h@$L(dmJl1ll;hN+!4-C|D(YNTN6zL1rJlxHoHF85MfsIChQXaSt-vbrO#{YnK3#`>O7rO| zT!o97duoZ5fa`PKH96ZrkaRr_h+p3O+?5xf#EvGUM2snuNfc7RN^3~yj?R67pCQdw zi%XX)2*$KLa#}<96)YQRk;GoVWd%O`W|>U)9?!45PRA6QhJ8c}FF+T$4@d&^e$O01 z29ma|TpP7k+N2|{+oqy8RoOrBJDVkMLxk)OTY%!C`N-$=u6kcou=DJxdt7@IJ07(d zhxqTZNk5Ey20@rH!%s+(bk#ivi$QKKqE2#?(ftdA6Sz^$M>>SWrchnps7#{bL$mA7 zG4IobU@bo|+Kdqitlx6e?_BTVTKDO93Un9Z@Ch7&OTrxk6H9#>IM}wuI2G9a=x?SU zkmb)}FU=~4S;PcoofR~S?QUTeCY)7g?l_!;wMPm4wKpngls(W4pfGa5Ri~RTugnc0 z*{}TKFBY76GSnLU#jWmp?_`^<<7jn9=9r}q9HhCO1q529@Hc7z~EY`$i*qekby!Up(VWQ+>{9&}4BBRM=%yBEPP(i^Rtb_25t)uXyAYL9-ZW$;F|6WSx**@U%Q6hn(F37+_PKX0yF7JQ&ax%{`QFcf^!U)%ssc`NXwnc9 z^U^4{<@jVg3>d*m9$82eRU=CKA^4KrS2CGX(k&`-&W7JsZzwfrh^;Zb<(Aecg><@s zI4BJZV-&oycY!Vs{8L-ghEGQ&L`0i?M_(vP|E(*vFhpPKZo#Y~+QfAJW8s^8mhJ41 z?j*oscQBV`T?e%trDX(EWRMEuc{7oo_+$sRf6ez0f>s)$ZxRZqR&BREU!jrzQ(D_%DVpXo%-mz_4IcS*#;3ll4mU@%C^_#w)OV~wNQgdxB z`OErmEKRmdgO?5(z+GKF`XF2$q3z?zWJ%F<8pB0(PhQ+tZkwgG-wOI1qk+wEP*D>L zkW+))W_{y%uEl!IdtM?W2xlzDIdT+6Lv;OpD{QK!cymVHW+C7$s? z7mP=;9f2p7b@CB>Oy%NKM7m>WB6Mu6(T!vXFGx1w5qHyLrB8wWte~Gyw%yEB$WEbxn=%#cwV#I7^u=}w`ju!(MTY|0iQ6z63iTu%+@D2CU9X!q(aU2y#6FP zf4PjPR2x}|GhKKPSQaC$4}#Kv-G@du%A6BU4xc@Xnj0#iYGi7iLMR~nr?kh&cL$@_*po|zs)pVF767}Lfh16RWvtm&7RO6LargEr z+DyKhhvFpG)mWwn6dJ6(OFcUM3d{x%gObZz@rk<92G1_=D8 z4oK1ik&FF26nSIyQ8IQ&i?UrcxfkaZJy#ERqD_L`u>{lS{U!}4Krxc+o7*9<1hV5` zi%~Vjq2i|7xplYRy<2-_12uUF6}!eY!ZvtYg!?-;YPE(}L<_^p4Duj=j1BKI_0?sC z(!222x2J^e+pL`{q4+Vzddu{(^`eO-&+NWv}@j9Pthu?PQyV zqpTsvF>W%*XH!4_$reDm)P+gjVoJT_o;a$nnw? zYeEU|uBn8w?6_kqPJ(I7(CD2Es99uMqi=(Hp4>1Vf(JzhHGJ32Rry81}z~^q<2>{MVpIM2mxn5JB_N+GE@;`=K3pvh)8#N@jZK- z1A>v%_l|HZfZJ7f!G29^*~vin)lZlVl-cIvNqe~!0TV;dhM&%1H+>qyqg_5_Dx2@# z8p@R?g4!B^_*U1wdW36v13E;oXMqUO!4O8Yf=F+AnCHn>_8FZ^aqOJ0%G}SX)wpjzD~(hDwb| zJ?VAc(3pWL6!WE;BhvN^eOrxSEAbyg^$U_SW^Kv~=t zX~Z)+e(LenbI9cHQM1YuFlu))&E54)V7yoRgTKgh7k&Ule)+ss)+S8ts`p^WN2}KT zV>9_;WA&68bo4zcn@holf&;15Fr;>`_#lP_d<~i_W$Oye>DK)6@j$x$NA||GW`m>k z3bD$Q=sNJx&>}2#PXensCjPI-)M*aM(-qe`{r1gAI#w?=uG<6gRI2_QIcnj}V}4;J zbD!q_?dO!6|G0sgQOAj4d5ZrEfbEZG- z_9cW9X4lnHuMkdlbZCJl-=EL1LEPMh6>rVDPQvpha4Lkw(QFq2N%HV&cWv#B1efr= z0f_cJPeIB}+)fpzZOwdW2HwkzGalf;fvwsAhf)b);3U@K5B^Y>{1pBWm0E5S`UqA) zoRoZSQ)hOs4@Z=loe$VpxBaY%(NVMB%jGD?DS09o?0OD*{ir75FZp=bHxK-TDx(Xb zwr8$Si@5bf-jLXs1-pp^kZN~U8maX;6OALOYnq;(~lfR z;@t-H%AG2W?1H$0l49?|vfHS}jKr8N+tso>WV=NI>U?q%Z#x*9mqx)mqNiyu?{KT4 zdfFf!*+R3!gL&`2zdO34AVP)76F0KJ>(c+oNMyN-U^;wo7{d9^flSkJ9 zSJf9d4X;?Uv)n|tJyos(T}P9|Ur4|D*WOA;ic)15-*j8SNE@Hp#MFC+a2#{KVMOsY zm$xl1=*!ww1T2@FXw?j> z%s~3W>T@8B&3-M*QK;Nv*zyAVV(#TFcOQL-g`iC&C|Bx^j*-l?zJ-j+Y~+hUP=wHl zpuOfFNJ4;RJ zxg>QG_y*#OUs^Gq8Rre(YvdlJ(lsYjE2VmD^L5;Ub?ANKN;&c!tGiVsgb%|0fMTXy z1m=CfAhQ6ePwx;Q|({dw_DK)55{MpU!WzS z#XgBD06G$~o3=bUZtxXM&nnYg4-!(*p-xldub&XSmIB{3_6TCkAA&hDEtf24J%5cx zgs;Z%ns`&8HCNL-h-&|)@OajAta0KseYp1~BDS8vjX0{`iHh?{$UTEy z@ncqYcvI$b%;^-5lPRz*u2S{5x{2lq>z?aie|r7|Yu29n6HHbRdQA=e8w#xiuB9)5 z-4Y&xI^y-s(|zRivDi0TwKvd4;%q^9lJQc(ZEFG2!qZIDa5$mo{QrpR)+yJG~uS6(p5qoHEQ_#)vGVq$!)ecpFc(BH^hZd-fQEO zLDsEoyLNl;gqCX#d??-vJ*x^DzTt&@P2ugduGlk zEp_%(fvm-2zd(>$QAArN_#_^kd}>-TNzvVWZH#Ldv0tHqudDZs545a{a+xeMsEX0o zZW3j(a^B?u?4w#Ye>Qbj0_0^CYT4skw~{fDQ%Nc>PRqMok|AN{tJM#8N^^;q;ItuK zA8T+!V!Nj0wmVe(N!i4Q8tZ*;CZ6@F`VUMS29Qn%FGTec)mDu{fRE1aN5KJnGE5QV&K(H;*I2G`on5vZZKHD z<#s5a!N4UWRvg5@tlKUUgfc5z;Q>AwDwnyaTXtJwXwO(g*F8ya0@b{ar4;h^>^qGa6NP|%YL|U9;&~iPlxkjv5BDgo>VuZ9150aBc7NK6A4KFDs4uJrOZFCQO1m%1DWoFdw{8O2$T`^J5`|pzr{e8SEp_ zrD$FGsZ2HKrt?1gceuKRW0;Ce#>6VbHx8tqYVD%K_(Uoe{*Q;LGJYod>&c2H*a)8x zq_p4eO7}Jx6FF!Wq4^kF_7`iCJ4QFY9(e1@H9A`Oh@^Ufy?MNRXZ+-c^{3u)qP=Oa z-$2Ze?`rxbmIMwZc0JA!Rbu^Y6!5au3;AqtkM2~T%a@yawJ!pFwd-iBvDLFsUlR;o zxTbW3Y*D7h4Lw#Vk`6AwWK#o(ZkGx#p%RSE1f)HgPy8Q`?)PMzrtv%$Vc|fij zZ4!C%pox0bMezu+XBJgmwS4nwc8!5S6ojyyt8hnthAvVM+qKc!vSZkxYKi*nc4l-r z@XD+<L|qnZXn)fzPEgl2-q;$?{LE%8hkxtw zPfU!?+Z^*_B`ps9rvUOa~2kmA+{ zLm;++4QaLZUpiH!Ycz^= zmfa|T3dRy_2a-GhJ!4IOFBD^nl^bv6oBojF%gAG1{^LtIC?ii@mSYblhQG1xP7I8^ zcLy(A=kqFFVHM6Zdj=2=yj_=`0baH})R|;6hnbFX*b3ryrK9uPrSlvtWw=_up->1p znY47nA`-@<|A`OE^mBD^z_huRy$EW1=<_|AV@TQ%m~>ziUFy!%W8 z%Fo-tj=W{+$!`+X@7fejX&(t|nUgI|Z<}qrLfm;5lmwoCHdr2wnaWv8or&Zj42^A> z>h>p=8vL}6L1K1Tx_X?m-VIejnp1tGoHNElfS4e7e%<^BQBc^3GAt6FdMI#G{{RO- z0R@TgK$bhQY*R{2KdnEPW&rt^UcKjRtgf{;+CdrAY9_kQpo|8|6#00+UNUf8Ls~C# zPZW;faIt&C$BoD0+f8kM{7)>nVh82zU7?1i>Ls}yj&a2ND=)0$?;-c+hgE(!J5G8_ z>HXfnpGR}g%gqtne(d~=61iQ#lNf(qA1!~N6O8q=sMLx;6Y(I5mTuEu2&9})z(v5T z%}Tx(+OK`iRR;mPNPotU?4O4G*}I)_Reb1Yuz3I@etLMv{tD<61a2TuZ9s`aAH61E z7?}dM|6BYjQ zo{!WTPWOD$1L%3srFnY?jK5-brtu|}WJU-6- zeo>Ewx=}+TR(8Ab?AZQm?4B>ORIsI-L&-xwdKIN!7?4)6i~=PPVR_@)uAiK>wAsY5 z43mZU-cjz;oq^}$ID+%NAb)~^?VL}$ zu*wrXJV^nA$8B0k9P|Tw*#|lNdWPA*{^s4N3w~MYmvxj6FRx>#DPEkS{^J43LUblz z7W&g>`2qFTzA+zMk%e%gO4PEM?@FsWQm<;zu-!^s72;AkB$(a(=%}#bt>USw9WuoM zz2vrN!MPVTa6hp~dBjOd0YjN7)d>@5ueOG!!> z3`z7-LJz853bz5m7y@D;cFQATqnpr|?wERVr`}ruK+!!W1N}X@vGm3FU4l+vVN)~( zZb$`hQocaCdD1#C zK%+9r@t;=^q#<)SIAPLH&anDehufuv*t&4?udl_-F(n-~ug~tGYBGaE>g!Tvq#Kad zO}k3P$OZ+`dCLRcx^SW6Q2r;7vl1$rR=Hxe>1>xu{3GFIxs@ak{~wtRU8hduMZfBS zXWv^*)Y2aJ_!*LQ#t8j}59et)!XjnETIac_QLsrIIpxCNsQ1GO8TQOiaUFPAsWVR# z=xBio?qR7Dl103R$+^!-2p!IHo_-u9$|Qn&f0!~4zWdMm@*nMxrxsRdy4B$QQ2F#j z@M(wmlg-+}sxKsKlIxS7{6{}CzCIppr6ucP%fjh3uMBvOqKf7h{b4jM z&gwGz@_rLB*s3yR!sbAYZtID%fH0lw_rhV$b)sqfKMr!h_cx|$GHfR+GK~Je0c$W= z3kTs+9Nf>=Q@yOjay-EJD9bxg1N++WumT+&~4l- zByUl3-z!-x9L%t**srnnG{8U5%jIHb1M^tCTwzD%(r~5+Rk}1>-P8g zP@~(q91ThXRw+_O5al4L@sJ_A59vNXQ^;=MY8_(cb8(Y^sA|Svt0KT@kK>GOczraT zC_>0GP`_SHs=|S3wNVS7JFmI;&Mn(llwU|H>?OWXrv+W;tI4E2>NUhZacsyb3kB4P z;+C3MIW0vdB*Iy=IhaFC%@HKh&4DQE(eau=d~a#Ae9t3J;_V+TeXHJ=B=x z?@s`pCGVBcRW+iMwohD9dB#~0bm}7CwuFsUz`m5YdK67XaBTqBe}jk3#}I}W!H}vU zd!GXfCSXsroX?B}2Sn6N>RMrLxhW6^64GRaYMkS z$F!HVzXMkMEJ~Z?171KHhU(Xy?H4r$clrSIQ+Eb|#v<@qz;EGRZ{M2%C<6s^80Myc z+Y5vhn+tA4rht;$C3;H`M}o~`-JyMAI{f`?33J=>?;bQngZDf?27s%H?RqXyy?60K zDR+aC;ps(3aC56GW4v>$l-c@b+dt$QFx;;AVfb5~5|0R-v|GnY^SzRsg_|Dw1c@cn2i{C^yCrxkpcfi zMGWjBXR|T464a;ez>=ZEEJ?T*a0gF#$-_BQSC09TZqrg8CyGdpE$>~t3pzhzZb^-# zo~+#AcyW4bf9QolxHR2nJQ5@OIoCQQy_3l?EMJ%<=19MH%%^jv7)&jHismIMigN+2 zU|oeIxUBhGGa@h#%Cki=lsJDN(Dxp#-*>a^VG{y*(m7o(y8k7iTNLRrmO;7T55wG> z#3ABFzaVIvWx@FA&Cffys#Us{AhZw8jUx1z38z~|ewuEys1!%cc%muoVaJFY<8i5$ zCl6z$thG(opwe{mZ!K8)^1$-?R2L}|oWwyGwrt<1$zH>tGi>5;F_=h!4VoMX$=Qc} zib3v9ENw7*&ywXYpqxbcRQk1B%r%)-aqd!jQuVmPh}WN;+R^pmNw9qlOZI=`+>T1k zrN((|x^V{P2um1hd7r$tMcAQ;9aslc``!7CaS|jLuPc0K2GwfB+bsW>I-q%B0r>@H zF22&UUc~)tsi9nOAgmG64;OtiaRycG5SDqBna_E#Zb|){Yy9^3_$NrYOlfVrg(W?@w3R|Jpi zaM?R9noK)wt%y2DnUew$B(8~1>MkVqM8Jl5(k)P|_hOiV&G{WBYpwZ~2gjbNOtyV7S8 zW2)z8_7#`kZB+)2jv+fQ*UQQ{6r$ z7Z^U{!|e4v-x?`DN|sARdtbbq!%IH<2d zCvWzLzpQ-wG<|)z)#mCi*#&75bofDGV{Y`s36i(bUME+_4TNLN?K;Q)27`JS+!rq@ zGn;q3afM{nz?+c*>vYOV-0g* z(OL62mXOsYDOvDJa1U(YH?)nguGa=R`7kir)_I}h5_h{R(kaf>VN1CcH5ft6yRl>+w zqwZbBy$F>Jf1q~5Wlp?2DXcY*qfVG9c!T}td!CO;z=G|Cq|61#O+lm5-FL6MxG60S zu4&U3n+;tIOR#*wBd*{-OF!?=W#csD05$iL7R%XU5FOiVYFWLdbg5?aDlA=C}b^aNRHEI=$Yhe?-s>mqV*tz$ zPb!VDv;7p#U9AdTA>O~4-TmdsJX)4kXbhXXIMaSgcj)1@_QCSuYsk9C`YuYJ@0eMyGzVFOkMQePxo7KlU=FiPYs@@L_svvaOdm>8 z0L8ln+N~k)Pqw6a^&G;UQE*w)iWQXx&R9Z!xiTE41s_RHJwbl~ir7tEAIO`RnF`7U zpq8l=&>QwY{DCpA_TLOhHm9x&#BZ{RTZc@!5$LJs1L0QKH^TC$X7#!Qy zBgKHgcA)bKhf)hkq$Wf_N`I+3$b%`z4OuyqBHUlKPTF9Gfapf6@QAmihOQ>VJg2f6 z$O>J!@50t%@Nay3f%Q~;%RH`Eb^*7Rv^9#0;N{+)ZGni+j=B}|)V$hiNbjdOlcNlR z%RIa}n33&z3lP#}P$?v^&3X>^Bt`CSP@3jcZ~c*2(UnqLVveeBXl%z8_`KkaKau?H zBmmqrsSZ~>LkEs5G>;e8QbscW7Z9nu&UqnB!c&>rnY#i+X8no}gxQ!R*om-(u=j#& zvGzD?7lKd`_B!JI6VSKke*HGIKkain?gNmvRZqeEq_ZY&v8fu)J};S|MI2>Vx#xOF zTZk^-cz|5fhnvJbe|)R`4A?=aUroI@G^-Ng`v+aC8JG_xgplb#* zD*@y^H`%4Y4m*jW;~%^SoW$Vm+CxmG!UkWF*G=YYO5 zFH$_uhNPI8WLLelnMjKDLpu=vc+$X^vY!)m4mB%Z@CD^j0c>Gk`SGVkph?5ZIM@_7 z74=CS7{dj!cr-gZ|fg~;|p4&JHcgeY1V>q92k~msF ztG#Cm4?-YKzmBY(6rX7ZsempoE7W-1)W3Px8olv0QQ4Dk1L+asN-)IoL`e5ND9QYt z46k(e*1VJBRC)&b9*5NiTnXaVUP}~Z<85UJJX!XAgne|vWeUTqA>hg)nANHf+&Pax z@J=E8y<0@e6Mw*Y8Co_tUL{dof{s22w%(0vju@kF=(JdD-^Qc#Y0IKz2aec%+0x&%}sks?~3ezC8{>H1peZabb|F7NU}%xny3}gmSz=)FTV~XHyDE zwcoW&Bf;;l1~P9)?pvM;n|7WbOj{%`joWCw`WRJu_+(7ko3R|gAnmFuTVZ^z~Khj^d5bJDx!2`d%SsUEXB$%y@;*GNh_Dw3xpIU zxYb_Dn~lMjSf%=wy`GULIxLw+K(JkG28xKH_eKSRRx01Kh8Sm??GTBm5mHJUc`o3d z15J6Oit?J6b<_7-4%2ByIg68sw(EKYt~ncSZ4q`8mT22r29sc6tW^dMJpXh|b4B@v zw_ZK?fZ@F0=gRJ`7XOy0to2XapA6dIJ_mt_9_|T*`?be@Ivs2(bk5JryHIbGrxr(V zID2Ait06_A9H6sw!_HYxeM#2Az!#9D`3JqZq|gq^x+L8?enpp{a>jCt5c0j@S$izV zk-#551qZgPdxP@C9;*Oqsb2|JM&Wz1iFSfB zXaj8LW7p5-k;3fb&r(%g1G(zwt($90D)-XoCSgobodyPype>lhhRRHZ)@42oQjIbV zZ#GJQ#xX4tfAx5sGnC$OuMBipXGiwn;cl%#!*GVmmtMM}#~z-tM$%Ug@Q$~0x7qL6 zEw|_26wM=p&E1Nsjg!MT1NzTduUAT)8hJu|MqtBCX&Za2)Ko;+ap3K5phk`~@y0p4 zBg)_ku2UIH^Qbg+*6OLmh{cp*t=!k^Vy1 z$p>na*3l+ktZR4`foZY zjY<-ltW?`3=S@o(c$MT-l#C;rc+^kl9O)+;P>TtJb~6dgXyh=cNv_c$HqDg>H3WQM z&h?{lBdvIfD?!zRSR&xW39D zFlm(G(;qRlZYR|JC>=sTKD;NF+ekW)99l|P!Pz2yaTsa+$l=frAv^5#E2+5y-Gs0! zGi<+`_5MklUhpSMNFS_K8vnT8xY)04#|*ug=Zc_6zgb_4Pz28uOOS_X)PBii8G6(b zNv2u#KVI@b8t;mJCR0u9l^sS=krjP^ribO~rh!LFPU?LBXm2tm_z{|( z5EX*5T3%!edqk5qfrzifUztg4F>7ejS_b($>N4DzO^CChr$aDW95#<*o<#*xMJJ>< z%PTE^V;{47K0KCmU_F4F-NQ#*c4di^5DqpV@0spg8 z8wXe`E(gPb`RB}FY#S@cpUL;eJGCgX0XT|K*_kXy+?Ab&n9-hZN7s!W)YwbkFrZA5 zHN7{&j=fcQUZ`spc#FUPER;i9v!yI(l_g{VqvGG}Ob!i`}>yls(X?2@D@%~>(% z)(EqVDd;|rS6$$9CwR4Buc+|`j&HLmU{Sb6=am^c2DWH0=Yj~P94hJ0jEpizWSc2^ zgHw-c$`YCqqj_{orLALSns1W_!)4{fY9y6j&wIGVhS)2OmcdQ!MT+Fbs{R@tF^}p6 zm6|NtL|9YTLe^A8QHh*V3P^>poNAE)C+aaf8mvjdVz{8-R)lI~2$I0q+EBw-WA490 z4D*TNf}VD;W-JD#Z<{YXEOK5^(-Q>gdYdfcegSnCt6V%qng0v}UavL3##^15941Gz z(C__Q3?39^^6L44bKvu^lf21(Wi6 zv?A$p+FLvQdOs`#Yd%js=)qO~$p;9}QHaG2Nno;^lZ#g3)^BZAhZ)96i%R>r{b#it zb*hru{J?MH7G$)klKM5*jTkg;^zuFJ&B)F%hgVe!J&%$Y+Z+m*oLDK#lD?9KQCL^m zBC^t3u?K`9(6OKZCJ4GPu*y8vlocQ>s#cgNL|hUm8vh-yN4ZtXY2)_*9^`wewJg$_#$X@;RnN;G_g$0ea5a{isVD*zHqy2VlQls2;l%-lPLYBmvdFWrV+YVS zuxq&Zh@2I9e2>>!v((~q5N?V(X2#Z8{V#!btiG z6{*Wm(*T9&VQuZX2I@=3ab4-Q@|+SICeTiji1{Y(MX0{Gr-*BS-@v64YLc;fM8OLe zg+2gP3;JO7P_P4p-z3(?Q{5@mI6u&N`Bhvsc+2}q=)vj+0eo6 zdawMp)&VJ_pBmb-=2s0#B~kNk{ph^(l~RG6DpTzY(R3E8(F!{$Z_$o8tkhSbSR(1A zKnPHHxqTny1bkS{d@2PK{Kj?@znKbd5Kju_RGLd;iI_zO9=2?kH(yIwt|Msl1b=D)65R#POA%_(d%T_!Cl8 z3ghhh!BgT~{np-XZF+icrg&R=W!=CX$rjYzv}c7c+Fj7{@_k+X_ezLk=~Ci_li%&| z^4^N3%+XboL`@qtHZcW`4O7EUO||W|B=`yAn{HwF+P4}@(ID|+Dn$8JYIu(teFLq4i)hzPC9u8~@LzI$7Nz z7zz#nmkWaV8k6azXIix8cj*FH)D>5_8h_Zy)sCjFR&$rg1aG^94m`B!J-nj#0B(rK zdW7FOaIY$l#jtJlYpB9{7(5-uxVbD>aVv*&^qAciZILEPR#*m$XPKo72Xw9>?oIoC zuE3eE9fc<%GyZH0-S!@g8NanfDBWM0sM#$Fw=h znHgNedYQ3JUE$a|Y?;Nj=cx-8@5DFBv&P6`pRR1h@vNLt6hf3S3Ib<(xwS}=4fP$o zkcn&kyG@NdESeaCG$byrZ<~Z51 zp4NCP+($F()3&N_#wVee?`YJZxC&igvZV;D(3iOI)92P0Qh5W zLp!1$^#v6&zW)}q$A1&SB4e~V?~sV%}=2)n#3hlo6@iFmC2qh9gTBW~7BOKfNhHzKEl-jQwI zV$MZ&*AF;`B16dj?Q-`eIUK5>x^h4r=Lh)D1Bh(Bg0S>NgGWdG*^ag|NPX$DXzFDe zW#A{DHo7Hmhf(&e*a%^MncT#$$P;N(*w_QHahxl-s7b)_uTjwcK z+6S0m;`X4#$9>`-YOo0?&FLr`7mpy>@m{l(=EXBX$Ry86!NYb`0PVi+lyA`t;UB`o zhRJTQMKbtaOeN&Y9v*dcV=1f@h2axfym;>pY2q}9A39a-WWnHVsF;wQ8+Q zL`1%4)q|YS1RfJ>890b<(!qqcwyf@L3N^r?psEFaO<=JW?ar@XJqZnwZeXw9-c z>+`k4;AUCoi44QzmOJS4d~wzrZOsy}c&U;&KX76oSNBYg8rMq#4$DPGgUSt7pa8?D z!R!Od|FP2ZKK|2@tG%3_?2p$u5#$`9(s)?;$by7A(jvSSsjI7*>?c^8koEB?pZTem z#nMcKxYw)&IN+*@H_s~~%a8u!!e_L5rH{v`uNLQZ^_?`_N{VXQ0o!rthQ{=Xgi(?9 z!}G+JlX`=tz={>@84(nXX+@8ly>0+@?R6H8-{sh8>D4IEURsL-0|llVB~lN z5Uej(FU2}eQNyC^)H0~RKsG1SQ#DFSHFxq0n2>xby7IxU;PDw;eQe-+>SJ6fmNu=U zcOGA!0*-4OHOJne@gr+Z)^&3$ads~py>~*}tQ#|cIHL5F<;}n^WpmE&rTe7cL4gWd zgxziW`0zT(I{?+a%p{;WdwPg2rFk#?0>q=;(w!iKrM=i+@Gt^R`4O|bJXNSb3SXDo zaArnN!m+c)*H`&;# zcNbZKVHo+OB83v_KwA+=o2uSJnxN5-hj|G4#_^~Ngu`-1V+H@h8RBsA32BV*^k%TE zi`ykY4JihnXv0?*!LaF~TRl8tFeNxP-spAP_x}MaK-9lk1$mi_j8ASwC8x;we?<+h zM>O=3rs_^;?GhI(ICAn$8>uNvzHmEh_dF`0OoymS9@{ z^YbVafqOKXNMno4qeByyVQa*+{hEnHeRwEL`~~Pr6u*zH$A+i4~mNhH8rz~VS9QUBK6WXm7H@>XOIHWOip%b8Q2+nh3&jZp5{6504 zYz()Kpb~x`#8r zV4`^Tl{c5Tx{EB>$wdh94zLjRraVxcB5F|m4PO9my(}Y!L~X#a7K#YoWi87KKcbBV zG$Uxq*xO=FG{ttyI+2130MpyZOgLLUDt0{PCQn2;)^p7T>rb;8Qp5NzN(7c0USd2r zdUoYlZ;>eiauUGWKzj!Km*G@id@K(~#XazN2zqi`v(BwKr&^-fy)H@8r`AIe%K!)B z!3Kx+4p!_>6LoDu2zM#hfwsL4=vR`^6idqo5Y>E-yoY-T9*imUUo0PZg3It#+DWVDkaxOK3%HelD2T; zMVx1dq`_mdmPNr6C}Ri^x5fAEbMxysBuRZrNWrL};U zA4f$H$j{^wX#T=90~tPzP_QeeXq)8gO)W-KWpci#!P7XfwEVhBYCWOAi0gaL4wuNnN)g425Yv&A0ZzYXNuLqr3^bFOHYWRZz~3+| zruxhOrh__;uiEUP6o4!GM%?^y;-;t`3Zk5AbT_O=lE#=sdF2)JkeOGwk@8^f2e(!a z0=^E=AzO>mQm`-N;*_Gz(pBu6h_d`1s@7S$Zcy}CJ523Sm)_j(b}T<3FjCH05sI;J zRSUcd8X-0vhM6qXTG-je-MAs$+K|uX5VGYBUsbDFGxG|CY$T!poI{Q zG1hwvuHVXyYAqXty9_dffI|mn1Hf0{91dYH9AN&(pgIoUt=*pKKlI6P(m$WR_|5m% ztq>?wUk9)XKN%9`1+1EzQ}=TgAy&MV=QkY0X3>RIQoD!AHq8Hn?3Yea5%6uO&KSTn zR<)OyGa@11TfpC_VGUwLt3&)cfbp(9J)%4&uFcBp()BR9^#ZTxQ811-;zpwREwsQ` zFO80T+5+pjHn+OM2cK!Qh1cPU-gXdcC462LJae~tP{WlL0=6!)$&4c1B+33HF9$SS ziw(?l?l&HhYhmpk(_HZYdI_JL-r1I8GHx&sb;Vs~4x)`!`K)UB;4;$hx^{`+oyD|H zv}Xm+Ll2H$x^w?dSspXV5W}y7Pzw23c-Suor)FWUwW3y=UY7W4^e|1mNyCU zfjq*aB7o%!)7cd=oRR@;czzWpym)v_MpdB9+-o&{q|Is2x^G3 zctg9xNfV4&+14eonifceBe@A1^?lv||K?7%dpErlaCx0(bGm)=z=^)tl8>?M^!#0_ zb9Lv3c>7n%&yhN+J2)HCGrbBZvW*QAXF00t=tX>N^J?@pWVt(n9YFu)4g(c+BwcO{ zqNN*(l>>r4q|h^uezR5u*qoyugCg+y6I1xjIMc6%`R!v)b=%88k5F>xwVl-;-@_S9 zkUMgN&B=*i^|YCbr|Yzq%ZmKOdb5c7*&`r#wVrN>YSEeoI?vXr;fkO#66y-ASlVwz z=Vmyt%CBe;g-NR&FKHbBh#J@GR&hkShz44S)&ir`?^`uY+|}vfyU-PS#D7GXX~cp% zY&{30((dpv{W+SNvEDkOA06K|4GRiw7%*4Vf5oKUdRlSJ&7s(xDMqKV9qC$(#LkP5 zac3gE4vGnONaXEY5vK(+ojshlWk2gBkI-O&F&Yfir{qN1HI7yP$S)-rQ@h0j6`M5S z8)9BG-KmdqNjTi|xh2_jNQ{yq`!;MIb^@EkkFvV4av6

^F1H#F? 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 hHMA0o%; Date: Tue, 11 Feb 2020 05:01:55 +0000 Subject: [PATCH 48/80] Apply minimal layout change according to view size - Add method to scale scrubber handle of DefaultTimeBar PiperOrigin-RevId: 294366734 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 8c20d441b2..9c6acc9b2b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -203,6 +203,7 @@ public class DefaultTimeBar extends View implements TimeBar { private int lastCoarseScrubXPosition; @MonotonicNonNull private Rect lastExclusionRectangle; + private float scrubberScale; private boolean scrubbing; private long scrubPosition; private long duration; @@ -329,6 +330,7 @@ public class DefaultTimeBar extends View implements TimeBar { (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) / 2; } + scrubberScale = 1.0f; duration = C.TIME_UNSET; keyTimeIncrement = C.TIME_UNSET; keyCountIncrement = DEFAULT_INCREMENT_COUNT; @@ -359,6 +361,18 @@ public class DefaultTimeBar extends View implements TimeBar { invalidate(seekBounds); } + /** + * Sets the scale factor for the scrubber handle. Scrubber enabled size, scrubber disabled size, + * scrubber dragged size are scaled by the scale factor. If scrubber drawable is set, the scale + * factor isn't applied. + * + * @param scrubberScale The scale factor for the scrubber handle. + */ + public void setScrubberScale(float scrubberScale) { + this.scrubberScale = scrubberScale; + invalidate(seekBounds); + } + /** * Sets the color for the portion of the time bar after the current played position up to the * current buffered position. @@ -815,7 +829,7 @@ public class DefaultTimeBar extends View implements TimeBar { if (scrubberDrawable == null) { int scrubberSize = (scrubbing || isFocused()) ? scrubberDraggedSize : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize); - int playheadRadius = scrubberSize / 2; + int playheadRadius = (int) ((scrubberSize * scrubberScale) / 2); canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint); } else { int scrubberDrawableWidth = scrubberDrawable.getIntrinsicWidth(); From d162c07ecf8472dfab532a48bfa9d6a56bd73fc7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 11 Mar 2020 05:01:42 +0000 Subject: [PATCH 49/80] Add show/hideScrubber to DefaultTimeBar PiperOrigin-RevId: 300249371 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ui/DefaultTimeBar.java | 65 +++++++++++++++---- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cfb05784c7..8ef653d936 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,7 @@ * UI: * Fix `DefaultTimeBar` to respect touch transformations ([#7303](https://github.com/google/ExoPlayer/issues/7303)). + * Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`. * Update `TrackSelectionDialogBuilder` to use AndroidX Compat Dialog ([#7357](https://github.com/google/ExoPlayer/issues/7357)). * Text: Use anti-aliasing and bitmap filtering when displaying bitmap diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 9c6acc9b2b..0d1f21b167 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ui; import android.annotation.TargetApi; +import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -166,6 +167,9 @@ public class DefaultTimeBar extends View implements TimeBar { private static final int DEFAULT_INCREMENT_COUNT = 20; + private static final float SHOWN_SCRUBBER_SCALE = 1.0f; + private static final float HIDDEN_SCRUBBER_SCALE = 0.0f; + /** * The name of the Android SDK view that most closely resembles this custom view. Used as the * class name for accessibility. @@ -203,6 +207,7 @@ public class DefaultTimeBar extends View implements TimeBar { private int lastCoarseScrubXPosition; @MonotonicNonNull private Rect lastExclusionRectangle; + private ValueAnimator scrubberScalingAnimator; private float scrubberScale; private boolean scrubbing; private long scrubPosition; @@ -331,6 +336,12 @@ public class DefaultTimeBar extends View implements TimeBar { / 2; } scrubberScale = 1.0f; + scrubberScalingAnimator = new ValueAnimator(); + scrubberScalingAnimator.addUpdateListener( + animation -> { + scrubberScale = (float) animation.getAnimatedValue(); + invalidate(seekBounds); + }); duration = C.TIME_UNSET; keyTimeIncrement = C.TIME_UNSET; keyCountIncrement = DEFAULT_INCREMENT_COUNT; @@ -340,6 +351,44 @@ public class DefaultTimeBar extends View implements TimeBar { } } + /** Shows the scrubber handle. */ + public void showScrubber() { + showScrubber(/* showAnimationDurationMs= */ 0); + } + + /** + * Shows the scrubber handle with animation. + * + * @param showAnimationDurationMs The duration for scrubber showing animation. + */ + public void showScrubber(long showAnimationDurationMs) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberScalingAnimator.setFloatValues(scrubberScale, SHOWN_SCRUBBER_SCALE); + scrubberScalingAnimator.setDuration(showAnimationDurationMs); + scrubberScalingAnimator.start(); + } + + /** Hides the scrubber handle. */ + public void hideScrubber() { + hideScrubber(/* hideAnimationDurationMs= */ 0); + } + + /** + * Hides the scrubber handle with animation. + * + * @param hideAnimationDurationMs The duration for scrubber hiding animation. + */ + public void hideScrubber(long hideAnimationDurationMs) { + if (scrubberScalingAnimator.isStarted()) { + scrubberScalingAnimator.cancel(); + } + scrubberScalingAnimator.setFloatValues(scrubberScale, HIDDEN_SCRUBBER_SCALE); + scrubberScalingAnimator.setDuration(hideAnimationDurationMs); + scrubberScalingAnimator.start(); + } + /** * Sets the color for the portion of the time bar representing media before the playback position. * @@ -361,18 +410,6 @@ public class DefaultTimeBar extends View implements TimeBar { invalidate(seekBounds); } - /** - * Sets the scale factor for the scrubber handle. Scrubber enabled size, scrubber disabled size, - * scrubber dragged size are scaled by the scale factor. If scrubber drawable is set, the scale - * factor isn't applied. - * - * @param scrubberScale The scale factor for the scrubber handle. - */ - public void setScrubberScale(float scrubberScale) { - this.scrubberScale = scrubberScale; - invalidate(seekBounds); - } - /** * Sets the color for the portion of the time bar after the current played position up to the * current buffered position. @@ -832,8 +869,8 @@ public class DefaultTimeBar extends View implements TimeBar { int playheadRadius = (int) ((scrubberSize * scrubberScale) / 2); canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint); } else { - int scrubberDrawableWidth = scrubberDrawable.getIntrinsicWidth(); - int scrubberDrawableHeight = scrubberDrawable.getIntrinsicHeight(); + int scrubberDrawableWidth = (int) (scrubberDrawable.getIntrinsicWidth() * scrubberScale); + int scrubberDrawableHeight = (int) (scrubberDrawable.getIntrinsicHeight() * scrubberScale); scrubberDrawable.setBounds( playheadX - scrubberDrawableWidth / 2, playheadY - scrubberDrawableHeight / 2, From 4e6fe31ee1aebabb8ab8503d25ed495e389ec35a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 30 Jan 2020 19:30:48 +0000 Subject: [PATCH 50/80] Merge pull request #6724 from nnoury:fix/subtitles-outline-color PiperOrigin-RevId: 292316767 --- RELEASENOTES.md | 7 ++- .../exoplayer2/ui/SubtitlePainter.java | 51 +++++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ef653d936..0dca43f909 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,8 +35,11 @@ * Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`. * Update `TrackSelectionDialogBuilder` to use AndroidX Compat Dialog ([#7357](https://github.com/google/ExoPlayer/issues/7357)). -* Text: Use anti-aliasing and bitmap filtering when displaying bitmap - subtitles. +* Text: + * Use anti-aliasing and bitmap filtering when displaying bitmap + subtitles. + * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct + color. * Cronet extension: Default to using the Cronet implementation in Google Play Services rather than Cronet Embedded. This allows Cronet to be used with a negligible increase in application size, compared to approximately 8MB when diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 714d40ff9a..2258f528d4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -33,6 +33,7 @@ import android.text.TextPaint; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.util.DisplayMetrics; import androidx.annotation.Nullable; @@ -99,6 +100,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Derived drawing variables. private @MonotonicNonNull StaticLayout textLayout; + private @MonotonicNonNull StaticLayout edgeLayout; private int textLeft; private int textTop; private int textPaddingX; @@ -291,11 +293,38 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } + // Remove embedded font color to not destroy edges, otherwise it overrides edge color. + SpannableStringBuilder cueTextEdge = new SpannableStringBuilder(cueText); + if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { + int cueLength = cueTextEdge.length(); + ForegroundColorSpan[] foregroundColorSpans = + cueTextEdge.getSpans(0, cueLength, ForegroundColorSpan.class); + for (ForegroundColorSpan foregroundColorSpan : foregroundColorSpans) { + cueTextEdge.removeSpan(foregroundColorSpan); + } + } + + // EDGE_TYPE_NONE & EDGE_TYPE_DROP_SHADOW both paint in one pass, they ignore cueTextEdge. + // In other cases we use two painters and we need to apply the background in the first one only, + // otherwise the background color gets drawn in front of the edge color + // (https://github.com/google/ExoPlayer/pull/6724#issuecomment-564650572). if (Color.alpha(backgroundColor) > 0) { - SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); - newCueText.setSpan( - new BackgroundColorSpan(backgroundColor), 0, newCueText.length(), Spanned.SPAN_PRIORITY); - cueText = newCueText; + if (edgeType == CaptionStyleCompat.EDGE_TYPE_NONE + || edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { + SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); + newCueText.setSpan( + new BackgroundColorSpan(backgroundColor), + 0, + newCueText.length(), + Spanned.SPAN_PRIORITY); + cueText = newCueText; + } else { + cueTextEdge.setSpan( + new BackgroundColorSpan(backgroundColor), + 0, + cueTextEdge.length(), + Spanned.SPAN_PRIORITY); + } } Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; @@ -371,6 +400,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Update the derived drawing variables. this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); + this.edgeLayout = + new StaticLayout( + cueTextEdge, textPaint, textWidth, textAlignment, spacingMult, spacingAdd, true); this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; @@ -410,8 +442,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } private void drawTextLayout(Canvas canvas) { - StaticLayout layout = textLayout; - if (layout == null) { + StaticLayout textLayout = this.textLayout; + StaticLayout edgeLayout = this.edgeLayout; + if (textLayout == null || edgeLayout == null) { // Nothing to draw. return; } @@ -434,7 +467,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; textPaint.setStrokeWidth(outlineWidth); textPaint.setColor(edgeColor); textPaint.setStyle(Style.FILL_AND_STROKE); - layout.draw(canvas); + edgeLayout.draw(canvas); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor); } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED @@ -446,13 +479,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; textPaint.setColor(foregroundColor); textPaint.setStyle(Style.FILL); textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp); - layout.draw(canvas); + edgeLayout.draw(canvas); textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown); } textPaint.setColor(foregroundColor); textPaint.setStyle(Style.FILL); - layout.draw(canvas); + textLayout.draw(canvas); textPaint.setShadowLayer(0, 0, 0, 0); canvas.restoreToCount(saveCount); From cb85cdd3eb34cc5c263c790473a7f83ac74863ec Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2020 12:36:48 +0100 Subject: [PATCH 51/80] Fix typo in release notes --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0dca43f909..a82e2c19e8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,7 +46,7 @@ embedding the library. * OkHttp extension: Upgrade OkHttp dependency to 3.12.11. * MediaSession extension: - * One set the playback state to `BUFFERING` if `playWhenReady` is true + * Only set the playback state to `BUFFERING` if `playWhenReady` is true ([#7206](https://github.com/google/ExoPlayer/issues/7206)). * Add missing `@Nullable` annotations to `MediaSessionConnector` ([#7234](https://github.com/google/ExoPlayer/issues/7234)). From b3de2b08d456676023f0883d4abe8e73667ceca6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 11 Dec 2019 11:12:01 +0000 Subject: [PATCH 52/80] Inline ENABLE_PRELOADING ImaAdsLoader relies on preloading being enabled (it doesn't work without it) so we may as well remove the constant to avoid potential confusion. PiperOrigin-RevId: 284951356 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index a37294365c..65a8bbe44d 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -272,11 +272,6 @@ public final class ImaAdsLoader private static final boolean DEBUG = false; private static final String TAG = "ImaAdsLoader"; - /** - * Whether to enable preloading of ads in {@link AdsRenderingSettings}. - */ - private static final boolean ENABLE_PRELOADING = true; - private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; @@ -1055,7 +1050,7 @@ public final class ImaAdsLoader private void initializeAdsManager() { AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setEnablePreloading(true); adsRenderingSettings.setMimeTypes(supportedMimeTypes); if (mediaLoadTimeoutMs != TIMEOUT_UNSET) { adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs); From 03be1551a7926cbb64dbf4925c0e5ede2542efee Mon Sep 17 00:00:00 2001 From: Andrew Lewis Date: Fri, 29 May 2020 20:53:06 +0100 Subject: [PATCH 53/80] Clean up player event handling This change is similar to e8293b92d877bd6e5a00606358db610d6fb7a4d6 but without relying on new player events that haven't been released yet, to make it easier to merge changes related to ImaAdsLoader on top. --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 90 ++++++++++--------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 65a8bbe44d..5f7d621b10 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -332,7 +332,7 @@ public final class ImaAdsLoader private VideoProgressUpdate lastAdProgress; private int lastVolumePercentage; - private AdsManager adsManager; + @Nullable private AdsManager adsManager; private boolean initializedAdsManager; private AdLoadException pendingAdLoadError; private Timeline timeline; @@ -975,12 +975,12 @@ public final class ImaAdsLoader initializedAdsManager = true; initializeAdsManager(); } - onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); + checkForContentCompleteOrNewAdGroup(); } @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { - if (adsManager == null) { + if (adsManager == null || player == null) { return; } @@ -993,18 +993,7 @@ public final class ImaAdsLoader adsManager.resume(); return; } - - if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING - && playWhenReady) { - checkForContentComplete(); - } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - if (DEBUG) { - Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); - } - } + handlePlayerStateChanged(playWhenReady, playbackState); } @Override @@ -1018,32 +1007,7 @@ public final class ImaAdsLoader @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (adsManager == null) { - return; - } - if (!playingAd && !player.isPlayingAd()) { - checkForContentComplete(); - if (sentContentComplete) { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } - } - updateAdPlaybackState(); - } else if (!timeline.isEmpty()) { - long positionMs = player.getCurrentPosition(); - timeline.getPeriod(0, period); - int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); - if (newAdGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = positionMs; - if (newAdGroupIndex != adGroupIndex) { - shouldNotifyAdPrepareError = false; - } - } - } - } - updateImaStateForPlayerState(); + checkForContentCompleteOrNewAdGroup(); } // Internal methods. @@ -1176,6 +1140,50 @@ public final class ImaAdsLoader } } + private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + if (imaAdState == IMA_AD_STATE_NONE + && playbackState == Player.STATE_BUFFERING + && playWhenReady) { + checkForContentComplete(); + } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); + } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); + } + } + } + + private void checkForContentCompleteOrNewAdGroup() { + if (adsManager == null || player == null) { + return; + } + if (!playingAd && !player.isPlayingAd()) { + checkForContentComplete(); + if (sentContentComplete) { + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ i); + } + } + updateAdPlaybackState(); + } else if (!timeline.isEmpty()) { + long positionMs = player.getCurrentPosition(); + timeline.getPeriod(/* periodIndex= */ 0, period); + int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); + if (newAdGroupIndex != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = positionMs; + if (newAdGroupIndex != adGroupIndex) { + shouldNotifyAdPrepareError = false; + } + } + } + } + updateImaStateForPlayerState(); + } + private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; From d1b900604a86dc325ce39a7c753e05ccfab2403b Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 4 Mar 2020 14:06:34 +0000 Subject: [PATCH 54/80] Add test to ensure AdsLoader is initialized. This tests explicitly that initialization happens even if the Timeline is a placeholder. No other change is needed. While the Timeline is still a placeholder ImaAdsLoader.getCurrentPeriodPosition will return 0 and trigger pre-rolls (intended behaviour) and it doesn't matter whether the actual initial period position may be somewhere else. PiperOrigin-RevId: 298833867 --- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 55 ++++++++------ .../ext/ima/SingletonImaFactory.java | 72 ------------------- .../exoplayer2/source/MaskingMediaSource.java | 2 +- 3 files changed, 36 insertions(+), 93 deletions(-) delete mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index edaa4cde29..e6c0852bc8 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.verify; @@ -39,11 +40,14 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory; +import com.google.android.exoplayer2.source.MaskingMediaSource; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; import java.util.Arrays; @@ -63,8 +67,11 @@ public class ImaAdsLoaderTest { private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = - new SinglePeriodTimeline( - CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false); + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US)); + private static final long CONTENT_PERIOD_DURATION_US = + CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; private static final Uri TEST_URI = Uri.EMPTY; private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; @@ -72,11 +79,11 @@ public class ImaAdsLoaderTest { private static final FakeAd UNSKIPPABLE_AD = new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1); - private @Mock ImaSdkSettings imaSdkSettings; - private @Mock AdsRenderingSettings adsRenderingSettings; - private @Mock AdDisplayContainer adDisplayContainer; - private @Mock AdsManager adsManager; - private SingletonImaFactory testImaFactory; + @Mock private ImaSdkSettings imaSdkSettings; + @Mock private AdsRenderingSettings adsRenderingSettings; + @Mock private AdDisplayContainer adDisplayContainer; + @Mock private AdsManager adsManager; + @Mock private ImaFactory mockImaFactory; private ViewGroup adViewGroup; private View adOverlayView; private AdsLoader.AdViewProvider adViewProvider; @@ -89,13 +96,11 @@ public class ImaAdsLoaderTest { MockitoAnnotations.initMocks(this); FakeAdsRequest fakeAdsRequest = new FakeAdsRequest(); FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager); - testImaFactory = - new SingletonImaFactory( - imaSdkSettings, - adsRenderingSettings, - adDisplayContainer, - fakeAdsRequest, - fakeAdsLoader); + when(mockImaFactory.createAdDisplayContainer()).thenReturn(adDisplayContainer); + when(mockImaFactory.createAdsRenderingSettings()).thenReturn(adsRenderingSettings); + when(mockImaFactory.createAdsRequest()).thenReturn(fakeAdsRequest); + when(mockImaFactory.createImaSdkSettings()).thenReturn(imaSdkSettings); + when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(fakeAdsLoader); adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext()); adOverlayView = new View(ApplicationProvider.getApplicationContext()); adViewProvider = @@ -136,6 +141,16 @@ public class ImaAdsLoaderTest { verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); } + @Test + public void testStart_withPlaceholderContent_initializedAdsLoader() { + Timeline placeholderTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); + setupPlayback(placeholderTimeline, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.start(adsLoaderListener, adViewProvider); + + // We'll only create the rendering settings when initializing the ads loader. + verify(mockImaFactory).createAdsRenderingSettings(); + } + @Test public void testStart_updatesAdPlaybackState() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); @@ -143,9 +158,9 @@ public class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - new AdPlaybackState(/* adGroupTimesUs= */ 0) + new AdPlaybackState(/* adGroupTimesUs...= */ 0) .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) - .withContentDurationUs(CONTENT_DURATION_US)); + .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @Test @@ -213,8 +228,8 @@ public class ImaAdsLoaderTest { // Verify that the preroll ad has been marked as played. assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - new AdPlaybackState(/* adGroupTimesUs= */ 0) - .withContentDurationUs(CONTENT_DURATION_US) + new AdPlaybackState(/* adGroupTimesUs...= */ 0) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI) .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) @@ -240,7 +255,7 @@ public class ImaAdsLoaderTest { when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); imaAdsLoader = new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) - .setImaFactory(testImaFactory) + .setImaFactory(mockImaFactory) .setImaSdkSettings(imaSdkSettings) .buildForAdTag(TEST_URI); imaAdsLoader.setPlayer(fakeExoPlayer); diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java deleted file mode 100644 index 4efd8cf38c..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2018 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 android.content.Context; -import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdsLoader; -import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; - -/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */ -final class SingletonImaFactory implements ImaAdsLoader.ImaFactory { - - private final ImaSdkSettings imaSdkSettings; - private final AdsRenderingSettings adsRenderingSettings; - private final AdDisplayContainer adDisplayContainer; - private final AdsRequest adsRequest; - private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; - - public SingletonImaFactory( - ImaSdkSettings imaSdkSettings, - AdsRenderingSettings adsRenderingSettings, - AdDisplayContainer adDisplayContainer, - AdsRequest adsRequest, - com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) { - this.imaSdkSettings = imaSdkSettings; - this.adsRenderingSettings = adsRenderingSettings; - this.adDisplayContainer = adDisplayContainer; - this.adsRequest = adsRequest; - this.adsLoader = adsLoader; - } - - @Override - public ImaSdkSettings createImaSdkSettings() { - return imaSdkSettings; - } - - @Override - public AdsRenderingSettings createAdsRenderingSettings() { - return adsRenderingSettings; - } - - @Override - public AdDisplayContainer createAdDisplayContainer() { - return adDisplayContainer; - } - - @Override - public AdsRequest createAdsRequest() { - return adsRequest; - } - - @Override - public AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { - return adsLoader; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 891cb351c1..47279f2358 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -293,7 +293,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ - private static final class DummyTimeline extends Timeline { + public static final class DummyTimeline extends Timeline { @Nullable private final Object tag; From 67d1b728d35f8daeb4a80e251da90c1637601959 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 28 Feb 2020 11:57:56 +0000 Subject: [PATCH 55/80] Clarify/fix position reference points for AdPlaybackState. The positions were interchangeably used with window and period positions. This change more clearly ensures that all positions in the AdPlaybackState are based on periods and that we use the right adjustments for all usages. PiperOrigin-RevId: 297811633 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 33 +++++++++----- .../google/android/exoplayer2/Timeline.java | 21 ++++----- .../analytics/PlaybackStatsListener.java | 8 +++- .../source/ads/AdPlaybackState.java | 43 +++++++++++-------- 4 files changed, 65 insertions(+), 40 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 5f7d621b10..5d5d156b97 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -318,6 +318,7 @@ public final class ImaAdsLoader @Nullable private final AdEventListener adEventListener; private final ImaFactory imaFactory; private final Timeline.Period period; + private final Timeline.Window window; private final List adCallbacks; private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; @@ -469,6 +470,7 @@ public final class ImaAdsLoader imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); period = new Timeline.Period(); + window = new Timeline.Window(); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); @@ -757,14 +759,16 @@ public final class ImaAdsLoader sentPendingContentPositionMs = true; contentPositionMs = pendingContentPositionMs; expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = player.getCurrentPosition(); + contentPositionMs = getContentPeriodPositionMs(); // Update the expected ad group index for the current content position. The update is delayed // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered // just after an ad group isn't incorrectly attributed to the next ad group. @@ -966,7 +970,7 @@ public final class ImaAdsLoader } Assertions.checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; - long contentDurationUs = timeline.getPeriod(0, period).durationUs; + long contentDurationUs = timeline.getPeriod(/* periodIndex= */ 0, period).durationUs; contentDurationMs = C.usToMs(contentDurationUs); if (contentDurationUs != C.TIME_UNSET) { adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs); @@ -1029,9 +1033,10 @@ public final class ImaAdsLoader // Skip ads based on the start position as required. long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - long contentPositionMs = player.getContentPosition(); + long contentPositionMs = getContentPeriodPositionMs(); int adGroupIndexForPosition = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) { // Skip any ad groups before the one at or immediately before the playback position. for (int i = 0; i < adGroupIndexForPosition; i++) { @@ -1169,7 +1174,7 @@ public final class ImaAdsLoader } updateAdPlaybackState(); } else if (!timeline.isEmpty()) { - long positionMs = player.getCurrentPosition(); + long positionMs = getContentPeriodPositionMs(); timeline.getPeriod(/* periodIndex= */ 0, period); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); if (newAdGroupIndex != C.INDEX_UNSET) { @@ -1311,8 +1316,9 @@ public final class ImaAdsLoader } private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs + if (contentDurationMs != C.TIME_UNSET + && pendingContentPositionMs == C.TIME_UNSET + && getContentPeriodPositionMs() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs && !sentContentComplete) { adsLoader.contentComplete(); if (DEBUG) { @@ -1322,7 +1328,8 @@ public final class ImaAdsLoader // After sending content complete IMA will not poll the content position, so set the expected // ad group index. expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentDurationMs)); + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(contentDurationMs), C.msToUs(contentDurationMs)); } } @@ -1374,6 +1381,12 @@ public final class ImaAdsLoader } } + private long getContentPeriodPositionMs() { + long contentWindowPositionMs = player.getContentPosition(); + return contentWindowPositionMs + - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 93a87da0dc..4dac71559a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -466,8 +466,8 @@ public abstract class Timeline { * microseconds. * * @param adGroupIndex The ad group index. - * @return The time of the ad group at the index, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} for a post-roll ad group. + * @return The time of the ad group at the index relative to the start of the enclosing {@link + * Period}, in microseconds, or {@link C#TIME_END_OF_SOURCE} for a post-roll ad group. */ public long getAdGroupTimeUs(int adGroupIndex) { return adPlaybackState.adGroupTimesUs[adGroupIndex]; @@ -510,22 +510,23 @@ public abstract class Timeline { } /** - * Returns the index of the ad group at or before {@code positionUs}, if that ad group is - * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has - * no ads remaining to be played, or if there is no such ad group. + * Returns the index of the ad group at or before {@code positionUs} in the period, if that ad + * group is unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code + * positionUs} has no ads remaining to be played, or if there is no such ad group. * - * @param positionUs The position at or before which to find an ad group, in microseconds. + * @param positionUs The period position at or before which to find an ad group, in + * microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexForPositionUs(long positionUs) { - return adPlaybackState.getAdGroupIndexForPositionUs(positionUs); + return adPlaybackState.getAdGroupIndexForPositionUs(positionUs, durationUs); } /** - * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be - * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * Returns the index of the next ad group after {@code positionUs} in the period that has ads + * remaining to be played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * - * @param positionUs The position after which to find an ad group, in microseconds. + * @param positionUs The period position after which to find an ad group, in microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index a9fd9d8641..43d2496842 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -189,11 +189,15 @@ public final class PlaybackStatsListener @Override public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); - long contentPositionUs = + long contentPeriodPositionUs = eventTime .timeline .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); + long contentWindowPositionUs = + contentPeriodPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : contentPeriodPositionUs + period.getPositionInWindowUs(); EventTime contentEventTime = new EventTime( eventTime.realtimeMs, @@ -203,7 +207,7 @@ public final class PlaybackStatsListener eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber, eventTime.mediaPeriodId.adGroupIndex), - /* eventPlaybackPositionMs= */ C.usToMs(contentPositionUs), + /* eventPlaybackPositionMs= */ C.usToMs(contentWindowPositionUs), eventTime.currentPlaybackPositionMs, eventTime.totalBufferedDurationMs); Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 0a1628b3f9..dee63d819e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -29,8 +29,7 @@ import java.util.Arrays; import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * Represents ad group times relative to the start of the media and information on the state and - * URIs of ads within each ad group. + * Represents ad group times and information on the state and URIs of ads within each ad group. * *

    Instances are immutable. Call the {@code with*} methods to get new instances that have the * required changes. @@ -272,8 +271,9 @@ public final class AdPlaybackState { /** The number of ad groups. */ public final int adGroupCount; /** - * The times of ad groups, in microseconds. A final element with the value {@link - * C#TIME_END_OF_SOURCE} indicates a postroll ad. + * The times of ad groups, in microseconds, relative to the start of the {@link + * com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad. */ public final long[] adGroupTimesUs; /** The ad groups. */ @@ -286,8 +286,9 @@ public final class AdPlaybackState { /** * Creates a new ad playback state with the specified ad group times. * - * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value - * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. + * @param adGroupTimesUs The times of ad groups in microseconds, relative to the start of the + * {@link com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with + * the value {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. */ public AdPlaybackState(long... adGroupTimesUs) { int count = adGroupTimesUs.length; @@ -315,16 +316,18 @@ public final class AdPlaybackState { * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no * ads remaining to be played, or if there is no such ad group. * - * @param positionUs The position at or before which to find an ad group, in microseconds, or - * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any + * @param positionUs The period position at or before which to find an ad group, in microseconds, + * or {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any * unplayed postroll ad group will be returned). + * @param periodDurationUs The duration of the containing timeline period, in microseconds, or + * {@link C#TIME_UNSET} if not known. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ - public int getAdGroupIndexForPositionUs(long positionUs) { + public int getAdGroupIndexForPositionUs(long positionUs, long periodDurationUs) { // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. // In practice we expect there to be few ad groups so the search shouldn't be expensive. int index = adGroupTimesUs.length - 1; - while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) { + while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) { index--; } return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; @@ -334,11 +337,11 @@ public final class AdPlaybackState { * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * - * @param positionUs The position after which to find an ad group, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group - * after the position). - * @param periodDurationUs The duration of the containing period in microseconds, or {@link - * C#TIME_UNSET} if not known. + * @param positionUs The period position after which to find an ad group, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad + * group after the position). + * @param periodDurationUs The duration of the containing timeline period, in microseconds, or + * {@link C#TIME_UNSET} if not known. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) { @@ -425,7 +428,10 @@ public final class AdPlaybackState { return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } - /** Returns an instance with the specified ad resume position, in microseconds. */ + /** + * Returns an instance with the specified ad resume position, in microseconds, relative to the + * start of the current ad. + */ @CheckResult public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) { if (this.adResumePositionUs == adResumePositionUs) { @@ -471,14 +477,15 @@ public final class AdPlaybackState { return result; } - private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) { + private boolean isPositionBeforeAdGroup( + long positionUs, long periodDurationUs, int adGroupIndex) { if (positionUs == C.TIME_END_OF_SOURCE) { // The end of the content is at (but not before) any postroll ad, and after any other ads. return false; } long adGroupPositionUs = adGroupTimesUs[adGroupIndex]; if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { - return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs; + return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs; } else { return positionUs < adGroupPositionUs; } From 6e66dc02fb9b7c86629f47adfb4b1231bd1676cf Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Apr 2020 15:28:11 +0100 Subject: [PATCH 56/80] Replace IMA ad tag fakes with mocks The mocking setup is quite messy/unclear compared to the fakes, but it seems worth switching over because IMA API changes have already required changes to fakes in the past, and there are more API changes in the version we are about to upgrade to. This change should generally remove the need to keep the fakes up-to-date. PiperOrigin-RevId: 308819176 --- .../android/exoplayer2/ext/ima/FakeAd.java | 211 ------------------ .../exoplayer2/ext/ima/FakeAdsLoader.java | 100 --------- .../exoplayer2/ext/ima/FakeAdsRequest.java | 132 ----------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 146 ++++++++---- 4 files changed, 103 insertions(+), 486 deletions(-) delete mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java delete mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java delete mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java deleted file mode 100644 index 59dfc6473c..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2018 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 com.google.ads.interactivemedia.v3.api.Ad; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.CompanionAd; -import com.google.ads.interactivemedia.v3.api.UiElement; -import java.util.List; -import java.util.Set; - -/** A fake ad for testing. */ -/* package */ final class FakeAd implements Ad { - - private final boolean skippable; - private final AdPodInfo adPodInfo; - - public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) { - this.skippable = skippable; - adPodInfo = - new AdPodInfo() { - @Override - public int getTotalAds() { - return totalAds; - } - - @Override - public int getAdPosition() { - return adPosition; - } - - @Override - public int getPodIndex() { - return podIndex; - } - - @Override - public boolean isBumper() { - throw new UnsupportedOperationException(); - } - - @Override - public double getMaxDuration() { - throw new UnsupportedOperationException(); - } - - @Override - public double getTimeOffset() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public int getVastMediaWidth() { - throw new UnsupportedOperationException(); - } - - @Override - public int getVastMediaHeight() { - throw new UnsupportedOperationException(); - } - - @Override - public int getVastMediaBitrate() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isSkippable() { - return skippable; - } - - @Override - public AdPodInfo getAdPodInfo() { - return adPodInfo; - } - - @Override - public String getAdId() { - throw new UnsupportedOperationException(); - } - - @Override - public String getCreativeId() { - throw new UnsupportedOperationException(); - } - - @Override - public String getCreativeAdId() { - throw new UnsupportedOperationException(); - } - - @Override - public String getUniversalAdIdValue() { - throw new UnsupportedOperationException(); - } - - @Override - public String getUniversalAdIdRegistry() { - throw new UnsupportedOperationException(); - } - - @Override - public String getAdSystem() { - throw new UnsupportedOperationException(); - } - - @Override - public String[] getAdWrapperIds() { - throw new UnsupportedOperationException(); - } - - @Override - public String[] getAdWrapperSystems() { - throw new UnsupportedOperationException(); - } - - @Override - public String[] getAdWrapperCreativeIds() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isLinear() { - throw new UnsupportedOperationException(); - } - - @Override - public double getSkipTimeOffset() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isUiDisabled() { - throw new UnsupportedOperationException(); - } - - @Override - public String getDescription() { - throw new UnsupportedOperationException(); - } - - @Override - public String getTitle() { - throw new UnsupportedOperationException(); - } - - @Override - public String getContentType() { - throw new UnsupportedOperationException(); - } - - @Override - public String getAdvertiserName() { - throw new UnsupportedOperationException(); - } - - @Override - public String getSurveyUrl() { - throw new UnsupportedOperationException(); - } - - @Override - public String getDealId() { - throw new UnsupportedOperationException(); - } - - @Override - public int getWidth() { - throw new UnsupportedOperationException(); - } - - @Override - public int getHeight() { - throw new UnsupportedOperationException(); - } - - @Override - public String getTraffickingParameters() { - throw new UnsupportedOperationException(); - } - - @Override - public double getDuration() { - throw new UnsupportedOperationException(); - } - - @Override - public Set getUiElements() { - throw new UnsupportedOperationException(); - } - - @Override - public List getCompanionAds() { - throw new UnsupportedOperationException(); - } -} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java deleted file mode 100644 index a8f3daae33..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2018 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 com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; -import com.google.ads.interactivemedia.v3.api.AdsManager; -import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; -import com.google.ads.interactivemedia.v3.api.StreamManager; -import com.google.ads.interactivemedia.v3.api.StreamRequest; -import com.google.android.exoplayer2.util.Assertions; -import java.util.ArrayList; - -/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */ -public final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader { - - private final ImaSdkSettings imaSdkSettings; - private final AdsManager adsManager; - private final ArrayList adsLoadedListeners; - private final ArrayList adErrorListeners; - - public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) { - this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); - this.adsManager = Assertions.checkNotNull(adsManager); - adsLoadedListeners = new ArrayList<>(); - adErrorListeners = new ArrayList<>(); - } - - @Override - public void contentComplete() { - // Do nothing. - } - - @Override - public ImaSdkSettings getSettings() { - return imaSdkSettings; - } - - @Override - public void requestAds(AdsRequest adsRequest) { - for (AdsLoadedListener listener : adsLoadedListeners) { - listener.onAdsManagerLoaded( - new AdsManagerLoadedEvent() { - @Override - public AdsManager getAdsManager() { - return adsManager; - } - - @Override - public StreamManager getStreamManager() { - throw new UnsupportedOperationException(); - } - - @Override - public Object getUserRequestContext() { - return adsRequest.getUserRequestContext(); - } - }); - } - } - - @Override - public String requestStream(StreamRequest streamRequest) { - throw new UnsupportedOperationException(); - } - - @Override - public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) { - adsLoadedListeners.add(adsLoadedListener); - } - - @Override - public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) { - adsLoadedListeners.remove(adsLoadedListener); - } - - @Override - public void addAdErrorListener(AdErrorListener adErrorListener) { - adErrorListeners.add(adErrorListener); - } - - @Override - public void removeAdErrorListener(AdErrorListener adErrorListener) { - adErrorListeners.remove(adErrorListener); - } -} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java deleted file mode 100644 index 7c2c8a6e0b..0000000000 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2018 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 com.google.ads.interactivemedia.v3.api.AdDisplayContainer; -import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; -import java.util.List; -import java.util.Map; - -/** Fake {@link AdsRequest} implementation for tests. */ -public final class FakeAdsRequest implements AdsRequest { - - private String adTagUrl; - private String adsResponse; - private Object userRequestContext; - private AdDisplayContainer adDisplayContainer; - private ContentProgressProvider contentProgressProvider; - - @Override - public void setAdTagUrl(String adTagUrl) { - this.adTagUrl = adTagUrl; - } - - @Override - public String getAdTagUrl() { - return adTagUrl; - } - - @Override - public void setExtraParameter(String s, String s1) { - throw new UnsupportedOperationException(); - } - - @Override - public String getExtraParameter(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public Map getExtraParameters() { - throw new UnsupportedOperationException(); - } - - @Override - public void setUserRequestContext(Object userRequestContext) { - this.userRequestContext = userRequestContext; - } - - @Override - public Object getUserRequestContext() { - return userRequestContext; - } - - @Override - public AdDisplayContainer getAdDisplayContainer() { - return adDisplayContainer; - } - - @Override - public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) { - this.adDisplayContainer = adDisplayContainer; - } - - @Override - public ContentProgressProvider getContentProgressProvider() { - return contentProgressProvider; - } - - @Override - public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) { - this.contentProgressProvider = contentProgressProvider; - } - - @Override - public String getAdsResponse() { - return adsResponse; - } - - @Override - public void setAdsResponse(String adsResponse) { - this.adsResponse = adsResponse; - } - - @Override - public void setAdWillAutoPlay(boolean b) { - throw new UnsupportedOperationException(); - } - - @Override - public void setAdWillPlayMuted(boolean b) { - throw new UnsupportedOperationException(); - } - - @Override - public void setContentDuration(float v) { - throw new UnsupportedOperationException(); - } - - @Override - public void setContentKeywords(List list) { - throw new UnsupportedOperationException(); - } - - @Override - public void setContentTitle(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public void setVastLoadTimeout(float v) { - throw new UnsupportedOperationException(); - } - - @Override - public void setLiveStreamPrefetchSeconds(float v) { - throw new UnsupportedOperationException(); - } -} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index e6c0852bc8..d50fff5ae8 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.ext.ima; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,8 +35,11 @@ import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -48,22 +53,29 @@ import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; /** Test for {@link ImaAdsLoader}. */ @RunWith(AndroidJUnit4.class) -public class ImaAdsLoaderTest { +public final class ImaAdsLoaderTest { private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = @@ -76,14 +88,20 @@ public class ImaAdsLoaderTest { private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f}; - private static final FakeAd UNSKIPPABLE_AD = - new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1); - @Mock private ImaSdkSettings imaSdkSettings; - @Mock private AdsRenderingSettings adsRenderingSettings; - @Mock private AdDisplayContainer adDisplayContainer; - @Mock private AdsManager adsManager; + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private ImaSdkSettings mockImaSdkSettings; + @Mock private AdsRenderingSettings mockAdsRenderingSettings; + @Mock private AdDisplayContainer mockAdDisplayContainer; + @Mock private AdsManager mockAdsManager; + @Mock private AdsRequest mockAdsRequest; + @Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent; + @Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader; @Mock private ImaFactory mockImaFactory; + @Mock private AdPodInfo mockPrerollSingleAdAdPodInfo; + @Mock private Ad mockPrerollSingleAd; + private ViewGroup adViewGroup; private View adOverlayView; private AdsLoader.AdViewProvider adViewProvider; @@ -93,14 +111,7 @@ public class ImaAdsLoaderTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); - FakeAdsRequest fakeAdsRequest = new FakeAdsRequest(); - FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager); - when(mockImaFactory.createAdDisplayContainer()).thenReturn(adDisplayContainer); - when(mockImaFactory.createAdsRenderingSettings()).thenReturn(adsRenderingSettings); - when(mockImaFactory.createAdsRequest()).thenReturn(fakeAdsRequest); - when(mockImaFactory.createImaSdkSettings()).thenReturn(imaSdkSettings); - when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(fakeAdsLoader); + setupMocks(); adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext()); adOverlayView = new View(ApplicationProvider.getApplicationContext()); adViewProvider = @@ -125,24 +136,24 @@ public class ImaAdsLoaderTest { } @Test - public void testBuilder_overridesPlayerType() { - when(imaSdkSettings.getPlayerType()).thenReturn("test player type"); + public void builder_overridesPlayerType() { + when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type"); setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); - verify(imaSdkSettings).setPlayerType("google/exo.ext.ima"); + verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima"); } @Test - public void testStart_setsAdUiViewGroup() { + public void start_setsAdUiViewGroup() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); - verify(adDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); - verify(adDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); + verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); + verify(mockAdDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); } @Test - public void testStart_withPlaceholderContent_initializedAdsLoader() { + public void start_withPlaceholderContent_initializedAdsLoader() { Timeline placeholderTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); setupPlayback(placeholderTimeline, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); @@ -152,7 +163,7 @@ public class ImaAdsLoaderTest { } @Test - public void testStart_updatesAdPlaybackState() { + public void start_updatesAdPlaybackState() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); @@ -164,14 +175,14 @@ public class ImaAdsLoaderTest { } @Test - public void testStartAfterRelease() { + public void startAfterRelease() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start(adsLoaderListener, adViewProvider); } @Test - public void testStartAndCallbacksAfterRelease() { + public void startAndCallbacksAfterRelease() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start(adsLoaderListener, adViewProvider); @@ -183,11 +194,11 @@ public class ImaAdsLoaderTest { // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA // SDK being proguarded. imaAdsLoader.requestAds(adViewGroup); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); imaAdsLoader.loadAd(TEST_URI.toString()); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); imaAdsLoader.playAd(); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); imaAdsLoader.pauseAd(); imaAdsLoader.stopAd(); imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); @@ -198,14 +209,14 @@ public class ImaAdsLoaderTest { } @Test - public void testPlayback_withPrerollAd_marksAdAsPlayed() { + public void playback_withPrerollAd_marksAdAsPlayed() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); // Load the preroll ad. imaAdsLoader.start(adsLoaderListener, adViewProvider); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); imaAdsLoader.loadAd(TEST_URI.toString()); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); // Play the preroll ad. imaAdsLoader.playAd(); @@ -215,10 +226,10 @@ public class ImaAdsLoaderTest { /* position= */ 0, /* contentPosition= */ 0); fakeExoPlayer.setState(Player.STATE_READY, true); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD)); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, UNSKIPPABLE_AD)); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, UNSKIPPABLE_AD)); - imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd)); // Play the content. fakeExoPlayer.setPlayingContentPosition(0); @@ -238,29 +249,77 @@ public class ImaAdsLoaderTest { } @Test - public void testStop_unregistersAllVideoControlOverlays() { + public void stop_unregistersAllVideoControlOverlays() { setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.stop(); - InOrder inOrder = inOrder(adDisplayContainer); - inOrder.verify(adDisplayContainer).registerVideoControlsOverlay(adOverlayView); - inOrder.verify(adDisplayContainer).unregisterAllVideoControlsOverlays(); + InOrder inOrder = inOrder(mockAdDisplayContainer); + inOrder.verify(mockAdDisplayContainer).registerVideoControlsOverlay(adOverlayView); + inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays(); } private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) { fakeExoPlayer = new FakePlayer(); adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs); - when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); + when(mockAdsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); imaAdsLoader = new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext()) .setImaFactory(mockImaFactory) - .setImaSdkSettings(imaSdkSettings) + .setImaSdkSettings(mockImaSdkSettings) .buildForAdTag(TEST_URI); imaAdsLoader.setPlayer(fakeExoPlayer); } + private void setupMocks() { + ArgumentCaptor userRequestContextCaptor = ArgumentCaptor.forClass(Object.class); + doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture()); + when(mockAdsRequest.getUserRequestContext()) + .thenAnswer((Answer) invocation -> userRequestContextCaptor.getValue()); + List adsLoadedListeners = + new ArrayList<>(); + doAnswer( + invocation -> { + adsLoadedListeners.add(invocation.getArgument(0)); + return null; + }) + .when(mockAdsLoader) + .addAdsLoadedListener(any()); + doAnswer( + invocation -> { + adsLoadedListeners.remove(invocation.getArgument(0)); + return null; + }) + .when(mockAdsLoader) + .removeAdsLoadedListener(any()); + when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager); + when(mockAdsManagerLoadedEvent.getUserRequestContext()) + .thenAnswer(invocation -> mockAdsRequest.getUserRequestContext()); + doAnswer( + (Answer) + invocation -> { + for (com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener listener : + adsLoadedListeners) { + listener.onAdsManagerLoaded(mockAdsManagerLoadedEvent); + } + return null; + }) + .when(mockAdsLoader) + .requestAds(mockAdsRequest); + + when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer); + when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); + when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); + when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); + + when(mockPrerollSingleAdAdPodInfo.getPodIndex()).thenReturn(0); + when(mockPrerollSingleAdAdPodInfo.getTotalAds()).thenReturn(1); + when(mockPrerollSingleAdAdPodInfo.getAdPosition()).thenReturn(1); + + when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockPrerollSingleAdAdPodInfo); + } + private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { return new AdEvent() { @Override @@ -301,7 +360,8 @@ public class ImaAdsLoaderTest { public void onAdPlaybackState(AdPlaybackState adPlaybackState) { adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); this.adPlaybackState = adPlaybackState; - fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); + fakeExoPlayer.updateTimeline( + new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); } @Override From de03e389c0927d1e1b76c13b556533048a3daf88 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 29 Apr 2020 08:52:01 +0100 Subject: [PATCH 57/80] Enable nullness checking for the IMA extension adPlaybackState is now non-null, and the uninitialized case is covered by a new boolean hasAdPlaybackState. Position progress updates are now non-null and initialized with IMA's VIDEO_TIME_NOT_READY constant. Also fix some misc code issues: - Remove empty branch for SmoothStreaming (Android Studio warns about this). - Tidy onTimelineChanged and onPositionDiscontinuity and the methods they call to improve naming. - Remove logging for IMA events after release, as these methods are expected to be called in the current IMA SDK behavior. PiperOrigin-RevId: 308977116 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 122 ++++++++++-------- .../exoplayer2/source/ads/AdsMediaSource.java | 2 +- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 5d5d156b97..ceb1dd35dc 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.ima; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.net.Uri; import android.os.Looper; @@ -282,7 +284,7 @@ public final class ImaAdsLoader * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. */ - private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; + private static final long END_OF_CONTENT_THRESHOLD_MS = 5000; /** The maximum duration before an ad break that IMA may start preloading the next ad. */ private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; @@ -325,7 +327,7 @@ public final class ImaAdsLoader private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; - private Object pendingAdRequestContext; + @Nullable private Object pendingAdRequestContext; private List supportedMimeTypes; @Nullable private EventListener eventListener; @Nullable private Player player; @@ -335,7 +337,8 @@ public final class ImaAdsLoader @Nullable private AdsManager adsManager; private boolean initializedAdsManager; - private AdLoadException pendingAdLoadError; + private boolean hasAdPlaybackState; + @Nullable private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -439,6 +442,7 @@ public final class ImaAdsLoader /* imaFactory= */ new DefaultImaFactory()); } + @SuppressWarnings("nullness:argument.type.incompatible") private ImaAdsLoader( Context context, @Nullable Uri adTagUri, @@ -479,12 +483,16 @@ public final class ImaAdsLoader context.getApplicationContext(), imaSdkSettings, adDisplayContainer); adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); + supportedMimeTypes = Collections.emptyList(); + lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; + lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET; pendingContentPositionMs = C.TIME_UNSET; adGroupIndex = C.INDEX_UNSET; contentDurationMs = C.TIME_UNSET; timeline = Timeline.EMPTY; + adPlaybackState = AdPlaybackState.NONE; } /** @@ -532,22 +540,22 @@ public final class ImaAdsLoader * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ public void requestAds(ViewGroup adViewGroup) { - if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { + if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) { // Ads have already been requested. return; } adDisplayContainer.setAdContainer(adViewGroup); - pendingAdRequestContext = new Object(); AdsRequest request = imaFactory.createAdsRequest(); if (adTagUri != null) { request.setAdTagUrl(adTagUri.toString()); - } else /* adsResponse != null */ { - request.setAdsResponse(adsResponse); + } else { + request.setAdsResponse(castNonNull(adsResponse)); } if (vastLoadTimeoutMs != TIMEOUT_UNSET) { request.setVastLoadTimeout(vastLoadTimeoutMs); } request.setContentProgressProvider(this); + pendingAdRequestContext = new Object(); request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); } @@ -567,6 +575,7 @@ public final class ImaAdsLoader public void setSupportedContentTypes(@C.ContentType int... contentTypes) { List supportedMimeTypes = new ArrayList<>(); for (@C.ContentType int contentType : contentTypes) { + // IMA does not support Smooth Streaming ad media. if (contentType == C.TYPE_DASH) { supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); } else if (contentType == C.TYPE_HLS) { @@ -579,8 +588,6 @@ public final class ImaAdsLoader MimeTypes.VIDEO_H263, MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); - } else if (contentType == C.TYPE_SS) { - // IMA does not support Smooth Streaming ad media. } } this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); @@ -594,22 +601,23 @@ public final class ImaAdsLoader if (player == null) { return; } + player.addListener(this); + boolean playWhenReady = player.getPlayWhenReady(); this.eventListener = eventListener; lastVolumePercentage = 0; - lastAdProgress = null; - lastContentProgress = null; + lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; + lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; ViewGroup adViewGroup = adViewProvider.getAdViewGroup(); adDisplayContainer.setAdContainer(adViewGroup); View[] adOverlayViews = adViewProvider.getAdOverlayViews(); for (View view : adOverlayViews) { adDisplayContainer.registerVideoControlsOverlay(view); } - player.addListener(this); maybeNotifyPendingAdLoadError(); - if (adPlaybackState != null) { + if (hasAdPlaybackState) { // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState); - if (imaPausedContent && player.getPlayWhenReady()) { + if (adsManager != null && imaPausedContent && playWhenReady) { adsManager.resume(); } } else if (adsManager != null) { @@ -623,21 +631,22 @@ public final class ImaAdsLoader @Override public void stop() { + @Nullable Player player = this.player; if (player == null) { return; } if (adsManager != null && imaPausedContent) { + adsManager.pause(); adPlaybackState = adPlaybackState.withAdResumePositionUs( playingAd ? C.msToUs(player.getCurrentPosition()) : 0); - adsManager.pause(); } lastVolumePercentage = getVolume(); lastAdProgress = getAdProgress(); lastContentProgress = getContentProgress(); adDisplayContainer.unregisterAllVideoControlsOverlays(); player.removeListener(this); - player = null; + this.player = null; eventListener = null; } @@ -659,6 +668,7 @@ public final class ImaAdsLoader imaAdState = IMA_AD_STATE_NONE; pendingAdLoadError = null; adPlaybackState = AdPlaybackState.NONE; + hasAdPlaybackState = false; updateAdPlaybackState(); } @@ -694,6 +704,7 @@ public final class ImaAdsLoader // If a player is attached already, start playback immediately. try { adPlaybackState = new AdPlaybackState(getAdGroupTimesUs(adsManager.getAdCuePoints())); + hasAdPlaybackState = true; updateAdPlaybackState(); } catch (Exception e) { maybeNotifyInternalError("onAdsManagerLoaded", e); @@ -710,7 +721,7 @@ public final class ImaAdsLoader Log.d(TAG, "onAdEvent: " + adEventType); } if (adsManager == null) { - Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); + // Drop events after release. return; } try { @@ -731,7 +742,8 @@ public final class ImaAdsLoader if (adsManager == null) { // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(); + adPlaybackState = AdPlaybackState.NONE; + hasAdPlaybackState = true; updateAdPlaybackState(); } else if (isAdGroupLoadError(error)) { try { @@ -768,7 +780,7 @@ public final class ImaAdsLoader adPlaybackState.getAdGroupIndexForPositionUs( C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = getContentPeriodPositionMs(); + contentPositionMs = getContentPeriodPositionMs(player, timeline, period); // Update the expected ad group index for the current content position. The update is delayed // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered // just after an ad group isn't incorrectly attributed to the next ad group. @@ -808,11 +820,12 @@ public final class ImaAdsLoader @Override public int getVolume() { + @Nullable Player player = this.player; if (player == null) { return lastVolumePercentage; } - Player.AudioComponent audioComponent = player.getAudioComponent(); + @Nullable Player.AudioComponent audioComponent = player.getAudioComponent(); if (audioComponent != null) { return (int) (audioComponent.getVolume() * 100); } @@ -834,16 +847,16 @@ public final class ImaAdsLoader Log.d(TAG, "loadAd in ad group " + adGroupIndex); } if (adsManager == null) { - Log.w(TAG, "Ignoring loadAd after release"); + // Drop events after release. return; } if (adGroupIndex == C.INDEX_UNSET) { + adGroupIndex = expectedAdGroupIndex; + adsManager.start(); Log.w( TAG, "Unexpected loadAd without LOADED event; assuming ad group index is actually " + expectedAdGroupIndex); - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); } int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); if (adIndexInAdGroup == C.INDEX_UNSET) { @@ -874,7 +887,7 @@ public final class ImaAdsLoader Log.d(TAG, "playAd"); } if (adsManager == null) { - Log.w(TAG, "Ignoring playAd after release"); + // Drop events after release. return; } switch (imaAdState) { @@ -911,7 +924,7 @@ public final class ImaAdsLoader // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. Log.w(TAG, "Unexpected playAd while detached"); } else if (!player.getPlayWhenReady()) { - adsManager.pause(); + Assertions.checkNotNull(adsManager).pause(); } } @@ -921,7 +934,7 @@ public final class ImaAdsLoader Log.d(TAG, "stopAd"); } if (adsManager == null) { - Log.w(TAG, "Ignoring stopAd after release"); + // Drop event after release. return; } if (player == null) { @@ -977,9 +990,14 @@ public final class ImaAdsLoader } if (!initializedAdsManager && adsManager != null) { initializedAdsManager = true; - initializeAdsManager(); + initializeAdsManager(adsManager); } - checkForContentCompleteOrNewAdGroup(); + handleTimelineOrPositionChanged(); + } + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + handleTimelineOrPositionChanged(); } @Override @@ -1009,14 +1027,9 @@ public final class ImaAdsLoader } } - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - checkForContentCompleteOrNewAdGroup(); - } - // Internal methods. - private void initializeAdsManager() { + private void initializeAdsManager(AdsManager adsManager) { AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(true); adsRenderingSettings.setMimeTypes(supportedMimeTypes); @@ -1033,7 +1046,8 @@ public final class ImaAdsLoader // Skip ads based on the start position as required. long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - long contentPositionMs = getContentPeriodPositionMs(); + long contentPositionMs = + getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period); int adGroupIndexForPosition = adPlaybackState.getAdGroupIndexForPositionUs( C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); @@ -1086,7 +1100,7 @@ public final class ImaAdsLoader podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); int adCount = adPodInfo.getTotalAds(); - adsManager.start(); + Assertions.checkNotNull(adsManager).start(); if (DEBUG) { Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } @@ -1138,8 +1152,6 @@ public final class ImaAdsLoader handleAdGroupLoadError(new IOException(message)); } break; - case STARTED: - case ALL_ADS_COMPLETED: default: break; } @@ -1160,7 +1172,8 @@ public final class ImaAdsLoader } } - private void checkForContentCompleteOrNewAdGroup() { + private void handleTimelineOrPositionChanged() { + @Nullable Player player = this.player; if (adsManager == null || player == null) { return; } @@ -1174,7 +1187,7 @@ public final class ImaAdsLoader } updateAdPlaybackState(); } else if (!timeline.isEmpty()) { - long positionMs = getContentPeriodPositionMs(); + long positionMs = getContentPeriodPositionMs(player, timeline, period); timeline.getPeriod(/* periodIndex= */ 0, period); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); if (newAdGroupIndex != C.INDEX_UNSET) { @@ -1186,10 +1199,7 @@ public final class ImaAdsLoader } } } - updateImaStateForPlayerState(); - } - private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); @@ -1316,10 +1326,11 @@ public final class ImaAdsLoader } private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET + long positionMs = getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period); + if (!sentContentComplete + && contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && getContentPeriodPositionMs() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs - && !sentContentComplete) { + && positionMs + END_OF_CONTENT_THRESHOLD_MS >= contentDurationMs) { adsLoader.contentComplete(); if (DEBUG) { Log.d(TAG, "adsLoader.contentComplete"); @@ -1357,7 +1368,7 @@ public final class ImaAdsLoader private void maybeNotifyPendingAdLoadError() { if (pendingAdLoadError != null && eventListener != null) { - eventListener.onAdLoadError(pendingAdLoadError, new DataSpec(adTagUri)); + eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri)); pendingAdLoadError = null; } } @@ -1366,22 +1377,23 @@ public final class ImaAdsLoader String message = "Internal error in " + name; Log.e(TAG, message, cause); // We can't recover from an unexpected error in general, so skip all remaining ads. - if (adPlaybackState == null) { - adPlaybackState = AdPlaybackState.NONE; - } else { - for (int i = 0; i < adPlaybackState.adGroupCount; i++) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(i); - } + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(i); } updateAdPlaybackState(); if (eventListener != null) { eventListener.onAdLoadError( AdLoadException.createForUnexpected(new RuntimeException(message, cause)), - new DataSpec(adTagUri)); + getAdsDataSpec(adTagUri)); } } - private long getContentPeriodPositionMs() { + private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { + return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); + } + + private static long getContentPeriodPositionMs( + Player player, Timeline timeline, Timeline.Period period) { long contentWindowPositionMs = player.getContentPosition(); return contentWindowPositionMs - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 4ecef1bd5b..3481042c98 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -271,7 +271,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( MediaPeriodId childId, MediaPeriodId mediaPeriodId) { // The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need // to forward the reported mediaPeriodId in this case. From 3b99a84dae94cf90e5335f05b22c77bed9440127 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 May 2020 10:09:01 +0100 Subject: [PATCH 58/80] Migrate to new IMA preloading APIs issue:#6429 PiperOrigin-RevId: 309906760 --- RELEASENOTES.md | 2 + extensions/ima/build.gradle | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 489 ++++++++++-------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 26 +- 4 files changed, 292 insertions(+), 227 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a82e2c19e8..64e1bdf381 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -52,6 +52,8 @@ ([#7234](https://github.com/google/ExoPlayer/issues/7234)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. +* IMA extension: Upgrade to IMA SDK version 3.18.1, and migrate to new + preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)). ### 2.11.4 (2020-04-08) ### diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index e2292aed8f..5b63456f74 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index ceb1dd35dc..d2fe3e621e 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import android.content.Context; import android.net.Uri; +import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.view.View; @@ -26,7 +27,6 @@ import android.view.ViewGroup; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; @@ -45,6 +45,7 @@ import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.UiElement; +import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; @@ -54,7 +55,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -71,6 +71,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -277,6 +278,14 @@ public final class ImaAdsLoader private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + /** + * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is + * the interval recommended by the IMA documentation. + * + * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback + */ + private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; + /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ private static final long IMA_DURATION_UNSET = -1L; @@ -286,9 +295,6 @@ public final class ImaAdsLoader */ private static final long END_OF_CONTENT_THRESHOLD_MS = 5000; - /** The maximum duration before an ad break that IMA may start preloading the next ad. */ - private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; - private static final int TIMEOUT_UNSET = -1; private static final int BITRATE_UNSET = -1; @@ -302,11 +308,12 @@ public final class ImaAdsLoader */ private static final int IMA_AD_STATE_NONE = 0; /** - * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}. + * The ad playback state when IMA has called {@link #playAd(AdMediaInfo)} and not {@link + * #pauseAd(AdMediaInfo)}. */ private static final int IMA_AD_STATE_PLAYING = 1; /** - * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad. + * The ad playback state when IMA has called {@link #pauseAd(AdMediaInfo)} while playing an ad. */ private static final int IMA_AD_STATE_PAUSED = 2; @@ -320,10 +327,12 @@ public final class ImaAdsLoader @Nullable private final AdEventListener adEventListener; private final ImaFactory imaFactory; private final Timeline.Period period; - private final Timeline.Window window; + private final Handler handler; private final List adCallbacks; private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private final Runnable updateAdProgressRunnable; + private final Map adInfoByAdMediaInfo; private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; @@ -341,19 +350,18 @@ public final class ImaAdsLoader @Nullable private AdLoadException pendingAdLoadError; private Timeline timeline; private long contentDurationMs; - private int podIndexOffset; private AdPlaybackState adPlaybackState; // Fields tracking IMA's state. - /** The expected ad group index that IMA should load next. */ - private int expectedAdGroupIndex; - /** The index of the current ad group that IMA is loading. */ - private int adGroupIndex; /** Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; /** The current ad playback state. */ private @ImaAdState int imaAdState; + /** The current ad media info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */ + @Nullable private AdMediaInfo imaAdMediaInfo; + /** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */ + @Nullable private AdInfo imaAdInfo; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been * called since starting ad playback. @@ -364,20 +372,23 @@ public final class ImaAdsLoader /** Whether the player is playing an ad. */ private boolean playingAd; + /** Whether the player is buffering an ad. */ + private boolean bufferingAd; /** * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} * otherwise. */ private int playingAdIndexInAdGroup; /** - * Whether there's a pending ad preparation error which IMA needs to be notified of when it - * transitions from playing content to playing the ad. + * The ad info for a pending ad for which the media failed preparation, or {@code null} if no + * pending ads have failed to prepare. */ - private boolean shouldNotifyAdPrepareError; + @Nullable private AdInfo pendingAdPrepareErrorAdInfo; /** - * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value - * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to - * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. + * If a content period has finished but IMA has not yet called {@link #playAd(AdMediaInfo)}, + * stores the value of {@link SystemClock#elapsedRealtime()} when the content stopped playing. + * This can be used to determine a fake, increasing content position. {@link C#TIME_UNSET} + * otherwise. */ private long fakeContentProgressElapsedRealtimeMs; /** @@ -442,7 +453,7 @@ public final class ImaAdsLoader /* imaFactory= */ new DefaultImaFactory()); } - @SuppressWarnings("nullness:argument.type.incompatible") + @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"}) private ImaAdsLoader( Context context, @Nullable Uri adTagUri, @@ -474,7 +485,7 @@ public final class ImaAdsLoader imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); period = new Timeline.Period(); - window = new Timeline.Window(); + handler = Util.createHandler(getImaLooper(), /* callback= */ null); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); @@ -483,13 +494,14 @@ public final class ImaAdsLoader context.getApplicationContext(), imaSdkSettings, adDisplayContainer); adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); + updateAdProgressRunnable = this::updateAdProgress; + adInfoByAdMediaInfo = new HashMap<>(); supportedMimeTypes = Collections.emptyList(); lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET; pendingContentPositionMs = C.TIME_UNSET; - adGroupIndex = C.INDEX_UNSET; contentDurationMs = C.TIME_UNSET; timeline = Timeline.EMPTY; adPlaybackState = AdPlaybackState.NONE; @@ -564,9 +576,8 @@ public final class ImaAdsLoader @Override public void setPlayer(@Nullable Player player) { - Assertions.checkState(Looper.getMainLooper() == Looper.myLooper()); - Assertions.checkState( - player == null || player.getApplicationLooper() == Looper.getMainLooper()); + Assertions.checkState(Looper.myLooper() == getImaLooper()); + Assertions.checkState(player == null || player.getApplicationLooper() == getImaLooper()); nextPlayer = player; wasSetPlayerCalled = true; } @@ -642,7 +653,7 @@ public final class ImaAdsLoader playingAd ? C.msToUs(player.getCurrentPosition()) : 0); } lastVolumePercentage = getVolume(); - lastAdProgress = getAdProgress(); + lastAdProgress = getAdVideoProgressUpdate(); lastContentProgress = getContentProgress(); adDisplayContainer.unregisterAllVideoControlsOverlays(); player.removeListener(this); @@ -666,6 +677,8 @@ public final class ImaAdsLoader adsLoader.removeAdErrorListener(/* adErrorListener= */ this); imaPausedContent = false; imaAdState = IMA_AD_STATE_NONE; + imaAdMediaInfo = null; + imaAdInfo = null; pendingAdLoadError = null; adPlaybackState = AdPlaybackState.NONE; hasAdPlaybackState = false; @@ -770,32 +783,11 @@ public final class ImaAdsLoader if (pendingContentPositionMs != C.TIME_UNSET) { sentPendingContentPositionMs = true; contentPositionMs = pendingContentPositionMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - // Update the expected ad group index for the current content position. The update is delayed - // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered - // just after an ad group isn't incorrectly attributed to the next ad group. - int nextAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - C.msToUs(contentPositionMs), C.msToUs(contentDurationMs)); - if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { - long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); - if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { - nextAdGroupTimeMs = contentDurationMs; - } - if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) { - expectedAdGroupIndex = nextAdGroupIndex; - } - } } else { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } @@ -807,15 +799,7 @@ public final class ImaAdsLoader @Override public VideoProgressUpdate getAdProgress() { - if (player == null) { - return lastAdProgress; - } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { - long adDuration = player.getDuration(); - return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY - : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } + throw new IllegalStateException("Unexpected call to getAdProgress when using preloading"); } @Override @@ -841,30 +825,37 @@ public final class ImaAdsLoader } @Override - public void loadAd(String adUriString) { + public void loadAd(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) { try { if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); + Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo); } if (adsManager == null) { // Drop events after release. return; } - if (adGroupIndex == C.INDEX_UNSET) { - adGroupIndex = expectedAdGroupIndex; - adsManager.start(); - Log.w( - TAG, - "Unexpected loadAd without LOADED event; assuming ad group index is actually " - + expectedAdGroupIndex); + int adGroupIndex = getAdGroupIndex(adPodInfo); + int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; + AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); + adInfoByAdMediaInfo.put(adMediaInfo, adInfo); + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; + if (adGroup.count == C.LENGTH_UNSET) { + adPlaybackState = + adPlaybackState.withAdCount( + adInfo.adGroupIndex, Math.max(adPodInfo.getTotalAds(), adGroup.states.length)); + adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; } - int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); - if (adIndexInAdGroup == C.INDEX_UNSET) { - Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); - return; + for (int i = 0; i < adIndexInAdGroup; i++) { + // Any preceding ads that haven't loaded are not going to load. + if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { + adPlaybackState = + adPlaybackState.withAdLoadError( + /* adGroupIndex= */ adGroupIndex, /* adIndexInAdGroup= */ i); + } } + Uri adUri = Uri.parse(adMediaInfo.getUrl()); adPlaybackState = - adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); + adPlaybackState.withAdUri(adInfo.adGroupIndex, adInfo.adIndexInAdGroup, adUri); updateAdPlaybackState(); } catch (Exception e) { maybeNotifyInternalError("loadAd", e); @@ -882,69 +873,62 @@ public final class ImaAdsLoader } @Override - public void playAd() { + public void playAd(AdMediaInfo adMediaInfo) { if (DEBUG) { - Log.d(TAG, "playAd"); + Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo)); } if (adsManager == null) { // Drop events after release. return; } - switch (imaAdState) { - case IMA_AD_STATE_PLAYING: - // IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028, b/63320878]. - Log.w(TAG, "Unexpected playAd without stopAd"); - break; - case IMA_AD_STATE_NONE: - // IMA is requesting to play the ad, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(); - } - if (shouldNotifyAdPrepareError) { - shouldNotifyAdPrepareError = false; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - break; - case IMA_AD_STATE_PAUSED: - imaAdState = IMA_AD_STATE_PLAYING; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(); - } - break; - default: - throw new IllegalStateException(); + + if (imaAdState == IMA_AD_STATE_PLAYING) { + // IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028]. + Log.w(TAG, "Unexpected playAd without stopAd"); } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected playAd while detached"); - } else if (!player.getPlayWhenReady()) { + + if (imaAdState == IMA_AD_STATE_NONE) { + // IMA is requesting to play the ad, so stop faking the content position. + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + fakeContentProgressOffsetMs = C.TIME_UNSET; + imaAdState = IMA_AD_STATE_PLAYING; + imaAdMediaInfo = adMediaInfo; + imaAdInfo = Assertions.checkNotNull(adInfoByAdMediaInfo.get(adMediaInfo)); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPlay(adMediaInfo); + } + if (pendingAdPrepareErrorAdInfo != null && pendingAdPrepareErrorAdInfo.equals(imaAdInfo)) { + pendingAdPrepareErrorAdInfo = null; + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(adMediaInfo); + } + } + updateAdProgress(); + } else { + imaAdState = IMA_AD_STATE_PLAYING; + Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo)); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onResume(adMediaInfo); + } + } + if (!Assertions.checkNotNull(player).getPlayWhenReady()) { Assertions.checkNotNull(adsManager).pause(); } } @Override - public void stopAd() { + public void stopAd(AdMediaInfo adMediaInfo) { if (DEBUG) { - Log.d(TAG, "stopAd"); + Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo)); } if (adsManager == null) { // Drop event after release. return; } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected stopAd while detached"); - } - if (imaAdState == IMA_AD_STATE_NONE) { - Log.w(TAG, "Unexpected stopAd"); - return; - } + + Assertions.checkNotNull(player); + Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); try { stopAdInternal(); } catch (Exception e) { @@ -953,26 +937,21 @@ public final class ImaAdsLoader } @Override - public void pauseAd() { + public void pauseAd(AdMediaInfo adMediaInfo) { if (DEBUG) { - Log.d(TAG, "pauseAd"); + Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo)); } if (imaAdState == IMA_AD_STATE_NONE) { // This method is called after content is resumed. return; } + Assertions.checkState(adMediaInfo.equals(imaAdMediaInfo)); imaAdState = IMA_AD_STATE_PAUSED; for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPause(); + adCallbacks.get(i).onPause(adMediaInfo); } } - @Override - public void resumeAd() { - // This method is never called. See [Internal: b/18931719]. - maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd")); - } - // Player.EventListener implementation. @Override @@ -1021,8 +1000,9 @@ public final class ImaAdsLoader @Override public void onPlayerError(ExoPlaybackException error) { if (imaAdState != IMA_AD_STATE_NONE) { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); + adCallbacks.get(i).onError(adMediaInfo); } } } @@ -1064,25 +1044,13 @@ public final class ImaAdsLoader adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); } - // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0. - // Store an index offset as we want to index all ads (including skipped ones) from 0. - if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) { - // We are playing a preroll. - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - // There's no ad to play which means there's no preroll. - podIndexOffset = -1; - } else { - // We are playing a midroll and any ads before it were skipped. - podIndexOffset = adGroupIndexForPosition - 1; - } - if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { // Provide the player's initial position to trigger loading and playing the ad. pendingContentPositionMs = contentPositionMs; } adsManager.init(adsRenderingSettings); + adsManager.start(); updateAdPlaybackState(); if (DEBUG) { Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); @@ -1090,39 +1058,32 @@ public final class ImaAdsLoader } private void handleAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); switch (adEvent.getType()) { - case LOADED: - // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. - AdPodInfo adPodInfo = ad.getAdPodInfo(); - int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = - podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); - int adPosition = adPodInfo.getAdPosition(); - int adCount = adPodInfo.getTotalAds(); - Assertions.checkNotNull(adsManager).start(); + case AD_BREAK_FETCH_ERROR: + String adGroupTimeSecondsString = + Assertions.checkNotNull(adEvent.getAdData().get("adBreakTime")); if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); + Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds"); } - int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; - if (adCount != oldAdCount) { - if (oldAdCount == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); - } else { - // IMA sometimes unexpectedly decreases the ad count in an ad group. - Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); + int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString); + int adGroupIndex = + Arrays.binarySearch( + adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds); + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; + if (adGroup.count == C.LENGTH_UNSET) { + adPlaybackState = + adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); + adGroup = adPlaybackState.adGroups[adGroupIndex]; + } + for (int i = 0; i < adGroup.count; i++) { + if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { + if (DEBUG) { + Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); + } + adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); } } - if (adGroupIndex != expectedAdGroupIndex) { - Log.w( - TAG, - "Expected ad group index " - + expectedAdGroupIndex - + ", actual ad group index " - + adGroupIndex); - expectedAdGroupIndex = adGroupIndex; - } + updateAdPlaybackState(); break; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads @@ -1148,23 +1109,65 @@ public final class ImaAdsLoader Map adData = adEvent.getAdData(); String message = "AdEvent: " + adData; Log.i(TAG, message); - if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(new IOException(message)); - } break; default: break; } } + private VideoProgressUpdate getAdVideoProgressUpdate() { + if (player == null) { + return lastAdProgress; + } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { + long adDuration = player.getDuration(); + return adDuration == C.TIME_UNSET + ? VideoProgressUpdate.VIDEO_TIME_NOT_READY + : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); + } else { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } + } + + private void updateAdProgress() { + VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate(); + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate); + } + handler.removeCallbacks(updateAdProgressRunnable); + handler.postDelayed(updateAdProgressRunnable, AD_PROGRESS_UPDATE_INTERVAL_MS); + } + + private void stopUpdatingAdProgress() { + handler.removeCallbacks(updateAdProgressRunnable); + } + private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) { + if (!bufferingAd && playbackState == Player.STATE_BUFFERING) { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onBuffering(adMediaInfo); + } + stopUpdatingAdProgress(); + } else if (bufferingAd && playbackState == Player.STATE_READY) { + bufferingAd = false; + updateAdProgress(); + } + } + if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING && playWhenReady) { checkForContentComplete(); } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); + if (adMediaInfo == null) { + Log.w(TAG, "onEnded without ad media info"); + } else { + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(adMediaInfo); + } } if (DEBUG) { Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); @@ -1193,9 +1196,6 @@ public final class ImaAdsLoader if (newAdGroupIndex != C.INDEX_UNSET) { sentPendingContentPositionMs = false; pendingContentPositionMs = positionMs; - if (newAdGroupIndex != adGroupIndex) { - shouldNotifyAdPrepareError = false; - } } } } @@ -1208,8 +1208,13 @@ public final class ImaAdsLoader if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); + @Nullable AdMediaInfo adMediaInfo = imaAdMediaInfo; + if (adMediaInfo == null) { + Log.w(TAG, "onEnded without ad media info"); + } else { + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(adMediaInfo); + } } if (DEBUG) { Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); @@ -1227,15 +1232,8 @@ public final class ImaAdsLoader } private void resumeContentInternal() { - if (imaAdState != IMA_AD_STATE_NONE) { - imaAdState = IMA_AD_STATE_NONE; - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - } - if (adGroupIndex != C.INDEX_UNSET) { - adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); - adGroupIndex = C.INDEX_UNSET; + if (imaAdInfo != null) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex); updateAdPlaybackState(); } } @@ -1250,23 +1248,40 @@ public final class ImaAdsLoader private void stopAdInternal() { imaAdState = IMA_AD_STATE_NONE; - int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); + stopUpdatingAdProgress(); // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. + Assertions.checkNotNull(imaAdInfo); + int adGroupIndex = imaAdInfo.adGroupIndex; + int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup; adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); updateAdPlaybackState(); if (!playingAd) { - adGroupIndex = C.INDEX_UNSET; + imaAdMediaInfo = null; + imaAdInfo = null; } } private void handleAdGroupLoadError(Exception error) { - int adGroupIndex = - this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; - if (adGroupIndex == C.INDEX_UNSET) { - // Drop the error, as we don't know which ad group it relates to. + if (player == null) { return; } + + // TODO: Once IMA signals which ad group failed to load, clean up this code. + long playerPositionMs = player.getContentPosition(); + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs( + C.msToUs(playerPositionMs), C.msToUs(contentDurationMs)); + if (adGroupIndex == C.INDEX_UNSET) { + adGroupIndex = + adPlaybackState.getAdGroupIndexAfterPositionUs( + C.msToUs(playerPositionMs), C.msToUs(contentDurationMs)); + if (adGroupIndex == C.INDEX_UNSET) { + // The error doesn't seem to relate to any ad group so give up handling it. + return; + } + } + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = @@ -1306,19 +1321,20 @@ public final class ImaAdsLoader if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { fakeContentProgressOffsetMs = contentDurationMs; } - shouldNotifyAdPrepareError = true; + pendingAdPrepareErrorAdInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); } else { + AdMediaInfo adMediaInfo = Assertions.checkNotNull(imaAdMediaInfo); // We're already playing an ad. if (adIndexInAdGroup > playingAdIndexInAdGroup) { // Mark the playing ad as ended so we can notify the error on the next ad and remove it, // which means that the ad after will load (if any). for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); + adCallbacks.get(i).onEnded(adMediaInfo); } } playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); + adCallbacks.get(i).onError(Assertions.checkNotNull(adMediaInfo)); } } adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); @@ -1336,11 +1352,6 @@ public final class ImaAdsLoader Log.d(TAG, "adsLoader.contentComplete"); } sentContentComplete = true; - // After sending content complete IMA will not poll the content position, so set the expected - // ad group index. - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(contentDurationMs), C.msToUs(contentDurationMs)); } } @@ -1351,21 +1362,6 @@ public final class ImaAdsLoader } } - /** - * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all - * ads in the ad group have loaded. - */ - private int getAdIndexInAdGroupToLoad(int adGroupIndex) { - @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; - int adIndexInAdGroup = 0; - // IMA loads ads in order. - while (adIndexInAdGroup < states.length - && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - adIndexInAdGroup++; - } - return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; - } - private void maybeNotifyPendingAdLoadError() { if (pendingAdLoadError != null && eventListener != null) { eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri)); @@ -1399,6 +1395,22 @@ public final class ImaAdsLoader - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); } + private int getAdGroupIndex(AdPodInfo adPodInfo) { + if (adPodInfo.getPodIndex() == -1) { + // This is a postroll ad. + return adPlaybackState.adGroupCount - 1; + } + + // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. + long adGroupTimeUs = (long) (((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND); + for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { + if (adPlaybackState.adGroupTimesUs[adGroupIndex] == adGroupTimeUs) { + return adGroupIndex; + } + } + throw new IllegalStateException("Failed to find cue point"); + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. @@ -1428,6 +1440,12 @@ public final class ImaAdsLoader || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; } + private static Looper getImaLooper() { + // IMA SDK callbacks occur on the main thread. This method can be used to check that the player + // is using the same looper, to ensure all interaction with this class is on the main thread. + return Looper.getMainLooper(); + } + private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { int count = adGroupTimesUs.length; if (count == 1) { @@ -1456,6 +1474,49 @@ public final class ImaAdsLoader Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); } + private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { + @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); + return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; + } + + // TODO: Consider moving this into AdPlaybackState. + private static final class AdInfo { + public final int adGroupIndex; + public final int adIndexInAdGroup; + + public AdInfo(int adGroupIndex, int adIndexInAdGroup) { + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AdInfo adInfo = (AdInfo) o; + if (adGroupIndex != adInfo.adGroupIndex) { + return false; + } + return adIndexInAdGroup == adInfo.adIndexInAdGroup; + } + + @Override + public int hashCode() { + int result = adGroupIndex; + result = 31 * result + adIndexInAdGroup; + return result; + } + + @Override + public String toString() { + return "(" + adGroupIndex + ", " + adIndexInAdGroup + ')'; + } + } + /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ private static final class DefaultImaFactory implements ImaFactory { @Override diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index d50fff5ae8..ddcd1ae483 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -41,6 +41,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Player; @@ -85,6 +86,7 @@ public final class ImaAdsLoaderTest { private static final long CONTENT_PERIOD_DURATION_US = CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; private static final Uri TEST_URI = Uri.EMPTY; + private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString()); private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f}; @@ -99,7 +101,7 @@ public final class ImaAdsLoaderTest { @Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent; @Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader; @Mock private ImaFactory mockImaFactory; - @Mock private AdPodInfo mockPrerollSingleAdAdPodInfo; + @Mock private AdPodInfo mockAdPodInfo; @Mock private Ad mockPrerollSingleAd; private ViewGroup adViewGroup; @@ -195,12 +197,12 @@ public final class ImaAdsLoaderTest { // SDK being proguarded. imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); - imaAdsLoader.loadAd(TEST_URI.toString()); + imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); - imaAdsLoader.playAd(); + imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd)); - imaAdsLoader.pauseAd(); - imaAdsLoader.stopAd(); + imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO); + imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); @@ -215,11 +217,11 @@ public final class ImaAdsLoaderTest { // Load the preroll ad. imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd)); - imaAdsLoader.loadAd(TEST_URI.toString()); + imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd)); // Play the preroll ad. - imaAdsLoader.playAd(); + imaAdsLoader.playAd(TEST_AD_MEDIA_INFO); fakeExoPlayer.setPlayingAdPosition( /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, @@ -233,7 +235,7 @@ public final class ImaAdsLoaderTest { // Play the content. fakeExoPlayer.setPlayingContentPosition(0); - imaAdsLoader.stopAd(); + imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO); imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); // Verify that the preroll ad has been marked as played. @@ -313,11 +315,11 @@ public final class ImaAdsLoaderTest { when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); - when(mockPrerollSingleAdAdPodInfo.getPodIndex()).thenReturn(0); - when(mockPrerollSingleAdAdPodInfo.getTotalAds()).thenReturn(1); - when(mockPrerollSingleAdAdPodInfo.getAdPosition()).thenReturn(1); + when(mockAdPodInfo.getPodIndex()).thenReturn(0); + when(mockAdPodInfo.getTotalAds()).thenReturn(1); + when(mockAdPodInfo.getAdPosition()).thenReturn(1); - when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockPrerollSingleAdAdPodInfo); + when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockAdPodInfo); } private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { From 8c108fb5fd402320b4cbfeac59173d6a4214a441 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 May 2020 12:00:14 +0100 Subject: [PATCH 59/80] Upgrade IMA SDK to 3.18.2 PiperOrigin-RevId: 310883076 --- RELEASENOTES.md | 2 +- extensions/ima/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 64e1bdf381..c81782c46d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -52,7 +52,7 @@ ([#7234](https://github.com/google/ExoPlayer/issues/7234)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. -* IMA extension: Upgrade to IMA SDK version 3.18.1, and migrate to new +* IMA extension: Upgrade to IMA SDK version 3.18.2, and migrate to new preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)). ### 2.11.4 (2020-04-08) ### diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 5b63456f74..af2c407d03 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.2' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' From 5529c124fee75745e361f6e6294f1ebeafe981e6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 May 2020 14:43:05 +0100 Subject: [PATCH 60/80] Remove deprecated symbols in ImaAdsLoader PiperOrigin-RevId: 310901647 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index d2fe3e621e..a2afa3c873 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -41,7 +41,6 @@ import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRequest; -import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.UiElement; @@ -69,7 +68,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -426,33 +424,6 @@ public final class ImaAdsLoader /* imaFactory= */ new DefaultImaFactory()); } - /** - * Creates a new IMA ads loader. - * - * @param context The context. - * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See - * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for - * more information. - * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to - * use the default settings. If set, the player type and version fields may be overwritten. - * @deprecated Use {@link ImaAdsLoader.Builder}. - */ - @Deprecated - public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) { - this( - context, - adTagUri, - imaSdkSettings, - /* adsResponse= */ null, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* adUiElements= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); - } - @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"}) private ImaAdsLoader( Context context, @@ -529,19 +500,6 @@ public final class ImaAdsLoader return adDisplayContainer; } - /** - * Sets the slots for displaying companion ads. Individual slots can be created using {@link - * ImaSdkFactory#createCompanionAdSlot()}. - * - * @param companionSlots Slots for displaying companion ads. - * @see AdDisplayContainer#setCompanionSlots(Collection) - * @deprecated Use {@code getAdDisplayContainer().setCompanionSlots(...)}. - */ - @Deprecated - public void setCompanionSlots(Collection companionSlots) { - adDisplayContainer.setCompanionSlots(companionSlots); - } - /** * Requests ads, if they have not already been requested. Must be called on the main thread. * From 929b60e209ae975f0bb24a65a3f2e47d498c00f4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 May 2020 08:52:15 +0100 Subject: [PATCH 61/80] Improve DEBUG logging in ImaAdsLoader Log content progress events, as these are helpful to debug triggering of events based on the content progress. Don't log AD_PROGRESS events as they occur several times per second while ads are playing, and the verbosity makes logs difficult to read. PiperOrigin-RevId: 311077302 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index a2afa3c873..84bdc8e8a3 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -688,7 +688,7 @@ public final class ImaAdsLoader @Override public void onAdEvent(AdEvent adEvent) { AdEventType adEventType = adEvent.getType(); - if (DEBUG) { + if (DEBUG && adEventType != AdEventType.AD_PROGRESS) { Log.d(TAG, "onAdEvent: " + adEventType); } if (adsManager == null) { @@ -733,24 +733,11 @@ public final class ImaAdsLoader @Override public VideoProgressUpdate getContentProgress() { - if (player == null) { - return lastContentProgress; + VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate(); + if (DEBUG) { + Log.d(TAG, "Content progress: " + videoProgressUpdate); } - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentPositionMs; - if (pendingContentPositionMs != C.TIME_UNSET) { - sentPendingContentPositionMs = true; - contentPositionMs = pendingContentPositionMs; - } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { - contentPositionMs = getContentPeriodPositionMs(player, timeline, period); - } else { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; - return new VideoProgressUpdate(contentPositionMs, contentDurationMs); + return videoProgressUpdate; } // VideoAdPlayer implementation. @@ -1073,6 +1060,27 @@ public final class ImaAdsLoader } } + private VideoProgressUpdate getContentVideoProgressUpdate() { + if (player == null) { + return lastContentProgress; + } + boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; + long contentPositionMs; + if (pendingContentPositionMs != C.TIME_UNSET) { + sentPendingContentPositionMs = true; + contentPositionMs = pendingContentPositionMs; + } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { + long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; + contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; + } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { + contentPositionMs = getContentPeriodPositionMs(player, timeline, period); + } else { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } + long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; + return new VideoProgressUpdate(contentPositionMs, contentDurationMs); + } + private VideoProgressUpdate getAdVideoProgressUpdate() { if (player == null) { return lastAdProgress; From e482aa2be8388cefc3553ad6b0c2d32af4076d99 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 May 2020 08:55:27 +0100 Subject: [PATCH 62/80] Fix method ordering in ImaAdsLoader Put static methods at the end. Also add a couple of missing parameter name comments. PiperOrigin-RevId: 311077684 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 84bdc8e8a3..0b9a8d747b 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -237,7 +237,7 @@ public final class ImaAdsLoader context, adTagUri, imaSdkSettings, - null, + /* adsResponse= */ null, vastLoadTimeoutMs, mediaLoadTimeoutMs, mediaBitrate, @@ -257,7 +257,7 @@ public final class ImaAdsLoader public ImaAdsLoader buildForAdsResponse(String adsResponse) { return new ImaAdsLoader( context, - null, + /* adTagUri= */ null, imaSdkSettings, adsResponse, vastLoadTimeoutMs, @@ -1350,17 +1350,6 @@ public final class ImaAdsLoader } } - private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { - return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); - } - - private static long getContentPeriodPositionMs( - Player player, Timeline timeline, Timeline.Period period) { - long contentWindowPositionMs = player.getContentPosition(); - return contentWindowPositionMs - - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); - } - private int getAdGroupIndex(AdPodInfo adPodInfo) { if (adPodInfo.getPodIndex() == -1) { // This is a postroll ad. @@ -1377,6 +1366,22 @@ public final class ImaAdsLoader throw new IllegalStateException("Failed to find cue point"); } + private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { + @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); + return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; + } + + private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { + return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); + } + + private static long getContentPeriodPositionMs( + Player player, Timeline timeline, Timeline.Period period) { + long contentWindowPositionMs = player.getContentPosition(); + return contentWindowPositionMs + - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. @@ -1440,11 +1445,6 @@ public final class ImaAdsLoader Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); } - private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { - @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); - return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; - } - // TODO: Consider moving this into AdPlaybackState. private static final class AdInfo { public final int adGroupIndex; From e8c74055451a65b7db11e4bf7e95c919cc7da0d6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 May 2020 13:35:57 +0100 Subject: [PATCH 63/80] Upgrade IMA SDK to 3.19.0 PiperOrigin-RevId: 311106612 --- RELEASENOTES.md | 2 +- extensions/ima/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c81782c46d..ab7e9998c5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -52,7 +52,7 @@ ([#7234](https://github.com/google/ExoPlayer/issues/7234)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. -* IMA extension: Upgrade to IMA SDK version 3.18.2, and migrate to new +* IMA extension: Upgrade to IMA SDK version 3.19.0, and migrate to new preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)). ### 2.11.4 (2020-04-08) ### diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index af2c407d03..2ed44f638c 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.2' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.0' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' From 51b2a0f7a9db5fd8b0cc2ca8dd58a5da9e6e6aae Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 May 2020 16:00:00 +0100 Subject: [PATCH 64/80] Add support for timing out ad preloading Detect stuck buffering cases in ImaAdsLoader, and discard the ad group after a timeout. This is intended to make the IMA extension more robust in the case where an ad group unexpectedly doesn't load. The timing out behavior is enabled by default but apps can choose to retain the old behavior by setting an unset timeout on ImaAdsLoader.Builder. PiperOrigin-RevId: 311729798 --- RELEASENOTES.md | 8 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 142 +++++++++++++++--- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 114 ++++++++++---- .../source/ads/AdPlaybackState.java | 12 ++ .../source/ads/AdPlaybackStateTest.java | 2 + 5 files changed, 228 insertions(+), 50 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ab7e9998c5..a2a439c52f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -52,8 +52,12 @@ ([#7234](https://github.com/google/ExoPlayer/issues/7234)). * AV1 extension: Add a heuristic to determine the default number of threads used for AV1 playback using the extension. -* IMA extension: Upgrade to IMA SDK version 3.19.0, and migrate to new - preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)). +* IMA extension: + * Upgrade to IMA SDK version 3.19.0, and migrate to new + preloading APIs + ([#6429](https://github.com/google/ExoPlayer/issues/6429)). + * Add support for timing out ad preloading, to avoid playback getting + stuck if an ad group unexpectedly fails to load. ### 2.11.4 (2020-04-08) ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 0b9a8d747b..b151a595c0 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -102,11 +102,23 @@ public final class ImaAdsLoader /** Builder for {@link ImaAdsLoader}. */ public static final class Builder { + /** + * The default duration in milliseconds for which the player must buffer while preloading an ad + * group before that ad group is skipped and marked as having failed to load. + * + *

    This value should be large enough not to trigger discarding the ad when it actually might + * load soon, but small enough so that user is not waiting for too long. + * + * @see #setAdPreloadTimeoutMs(long) + */ + public static final long DEFAULT_AD_PRELOAD_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND; + private final Context context; @Nullable private ImaSdkSettings imaSdkSettings; @Nullable private AdEventListener adEventListener; @Nullable private Set adUiElements; + private long adPreloadTimeoutMs; private int vastLoadTimeoutMs; private int mediaLoadTimeoutMs; private int mediaBitrate; @@ -120,6 +132,7 @@ public final class ImaAdsLoader */ public Builder(Context context) { this.context = Assertions.checkNotNull(context); + adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS; vastLoadTimeoutMs = TIMEOUT_UNSET; mediaLoadTimeoutMs = TIMEOUT_UNSET; mediaBitrate = BITRATE_UNSET; @@ -165,6 +178,25 @@ public final class ImaAdsLoader return this; } + /** + * Sets the duration in milliseconds for which the player must buffer while preloading an ad + * group before that ad group is skipped and marked as having failed to load. Pass {@link + * C#TIME_UNSET} if there should be no such timeout. The default value is {@value + * DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms. + * + *

    The purpose of this timeout is to avoid playback getting stuck in the unexpected case that + * the IMA SDK does not load an ad break based on the player's reported content position. + * + * @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link + * C#TIME_UNSET} for no timeout. + * @return This builder, for convenience. + */ + public Builder setAdPreloadTimeoutMs(long adPreloadTimeoutMs) { + Assertions.checkArgument(adPreloadTimeoutMs == C.TIME_UNSET || adPreloadTimeoutMs > 0); + this.adPreloadTimeoutMs = adPreloadTimeoutMs; + return this; + } + /** * Sets the VAST load timeout, in milliseconds. * @@ -238,6 +270,7 @@ public final class ImaAdsLoader adTagUri, imaSdkSettings, /* adsResponse= */ null, + adPreloadTimeoutMs, vastLoadTimeoutMs, mediaLoadTimeoutMs, mediaBitrate, @@ -260,6 +293,7 @@ public final class ImaAdsLoader /* adTagUri= */ null, imaSdkSettings, adsResponse, + adPreloadTimeoutMs, vastLoadTimeoutMs, mediaLoadTimeoutMs, mediaBitrate, @@ -291,7 +325,12 @@ public final class ImaAdsLoader * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. */ - private static final long END_OF_CONTENT_THRESHOLD_MS = 5000; + private static final long THRESHOLD_END_OF_CONTENT_MS = 5000; + /** + * Threshold before the start of an ad at which IMA is expected to be able to preload the ad, in + * milliseconds. + */ + private static final long THRESHOLD_AD_PRELOAD_MS = 4000; private static final int TIMEOUT_UNSET = -1; private static final int BITRATE_UNSET = -1; @@ -317,6 +356,7 @@ public final class ImaAdsLoader @Nullable private final Uri adTagUri; @Nullable private final String adsResponse; + private final long adPreloadTimeoutMs; private final int vastLoadTimeoutMs; private final int mediaLoadTimeoutMs; private final boolean focusSkipButtonWhenAvailable; @@ -398,6 +438,11 @@ public final class ImaAdsLoader private long pendingContentPositionMs; /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; + /** + * Stores the real time in milliseconds at which the player started buffering, possibly due to not + * having preloaded an ad, or {@link C#TIME_UNSET} if not applicable. + */ + private long waitingForPreloadElapsedRealtimeMs; /** * Creates a new IMA ads loader. @@ -415,6 +460,7 @@ public final class ImaAdsLoader adTagUri, /* imaSdkSettings= */ null, /* adsResponse= */ null, + /* adPreloadTimeoutMs= */ Builder.DEFAULT_AD_PRELOAD_TIMEOUT_MS, /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, /* mediaBitrate= */ BITRATE_UNSET, @@ -430,6 +476,7 @@ public final class ImaAdsLoader @Nullable Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings, @Nullable String adsResponse, + long adPreloadTimeoutMs, int vastLoadTimeoutMs, int mediaLoadTimeoutMs, int mediaBitrate, @@ -440,6 +487,7 @@ public final class ImaAdsLoader Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; this.adsResponse = adsResponse; + this.adPreloadTimeoutMs = adPreloadTimeoutMs; this.vastLoadTimeoutMs = vastLoadTimeoutMs; this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; this.mediaBitrate = mediaBitrate; @@ -473,6 +521,7 @@ public final class ImaAdsLoader fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET; pendingContentPositionMs = C.TIME_UNSET; + waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; contentDurationMs = C.TIME_UNSET; timeline = Timeline.EMPTY; adPlaybackState = AdPlaybackState.NONE; @@ -636,6 +685,7 @@ public final class ImaAdsLoader imaPausedContent = false; imaAdState = IMA_AD_STATE_NONE; imaAdMediaInfo = null; + stopUpdatingAdProgress(); imaAdInfo = null; pendingAdLoadError = null; adPlaybackState = AdPlaybackState.NONE; @@ -737,6 +787,19 @@ public final class ImaAdsLoader if (DEBUG) { Log.d(TAG, "Content progress: " + videoProgressUpdate); } + + if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) { + // IMA is polling the player position but we are buffering for an ad to preload, so playback + // may be stuck. Detect this case and signal an error if applicable. + long stuckElapsedRealtimeMs = + SystemClock.elapsedRealtime() - waitingForPreloadElapsedRealtimeMs; + if (stuckElapsedRealtimeMs >= THRESHOLD_AD_PRELOAD_MS) { + waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; + handleAdGroupLoadError(new IOException("Ad preloading timed out")); + maybeNotifyPendingAdLoadError(); + } + } + return videoProgressUpdate; } @@ -779,10 +842,15 @@ public final class ImaAdsLoader // Drop events after release. return; } - int adGroupIndex = getAdGroupIndex(adPodInfo); + int adGroupIndex = getAdGroupIndexForAdPod(adPodInfo); int adIndexInAdGroup = adPodInfo.getAdPosition() - 1; AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup); adInfoByAdMediaInfo.put(adMediaInfo, adInfo); + if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { + // We have already marked this ad as having failed to load, so ignore the request. IMA will + // timeout after its media load timeout. + return; + } AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adInfo.adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = @@ -926,10 +994,34 @@ public final class ImaAdsLoader @Override public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { + @Nullable Player player = this.player; if (adsManager == null || player == null) { return; } + if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd()) { + // Check whether we are waiting for an ad to preload. + int adGroupIndex = getLoadingAdGroupIndex(); + if (adGroupIndex == C.INDEX_UNSET) { + return; + } + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; + if (adGroup.count != C.LENGTH_UNSET + && adGroup.count != 0 + && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) { + // An ad is available already so we must be buffering for some other reason. + return; + } + long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); + long contentPositionMs = getContentPeriodPositionMs(player, timeline, period); + long timeUntilAdMs = adGroupTimeMs - contentPositionMs; + if (timeUntilAdMs < adPreloadTimeoutMs) { + waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime(); + } + } else if (playbackState == Player.STATE_READY) { + waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET; + } + if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { adsManager.pause(); return; @@ -939,6 +1031,7 @@ public final class ImaAdsLoader adsManager.resume(); return; } + handlePlayerStateChanged(playWhenReady, playbackState); } @@ -1219,6 +1312,10 @@ public final class ImaAdsLoader Assertions.checkNotNull(imaAdInfo); int adGroupIndex = imaAdInfo.adGroupIndex; int adIndexInAdGroup = imaAdInfo.adIndexInAdGroup; + if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) { + // We have already marked this ad as having failed to load, so ignore the request. + return; + } adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); updateAdPlaybackState(); @@ -1233,19 +1330,11 @@ public final class ImaAdsLoader return; } - // TODO: Once IMA signals which ad group failed to load, clean up this code. - long playerPositionMs = player.getContentPosition(); - int adGroupIndex = - adPlaybackState.getAdGroupIndexForPositionUs( - C.msToUs(playerPositionMs), C.msToUs(contentDurationMs)); + // TODO: Once IMA signals which ad group failed to load, remove this call. + int adGroupIndex = getLoadingAdGroupIndex(); if (adGroupIndex == C.INDEX_UNSET) { - adGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs( - C.msToUs(playerPositionMs), C.msToUs(contentDurationMs)); - if (adGroupIndex == C.INDEX_UNSET) { - // The error doesn't seem to relate to any ad group so give up handling it. - return; - } + Log.w(TAG, "Unable to determine ad group index for ad group load error", error); + return; } AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; @@ -1312,7 +1401,7 @@ public final class ImaAdsLoader if (!sentContentComplete && contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET - && positionMs + END_OF_CONTENT_THRESHOLD_MS >= contentDurationMs) { + && positionMs + THRESHOLD_END_OF_CONTENT_MS >= contentDurationMs) { adsLoader.contentComplete(); if (DEBUG) { Log.d(TAG, "adsLoader.contentComplete"); @@ -1350,7 +1439,7 @@ public final class ImaAdsLoader } } - private int getAdGroupIndex(AdPodInfo adPodInfo) { + private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) { if (adPodInfo.getPodIndex() == -1) { // This is a postroll ad. return adPlaybackState.adGroupCount - 1; @@ -1366,6 +1455,23 @@ public final class ImaAdsLoader throw new IllegalStateException("Failed to find cue point"); } + /** + * Returns the index of the ad group that will preload next, or {@link C#INDEX_UNSET} if there is + * no such ad group. + */ + private int getLoadingAdGroupIndex() { + long playerPositionUs = + C.msToUs(getContentPeriodPositionMs(Assertions.checkNotNull(player), timeline, period)); + int adGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs)); + if (adGroupIndex == C.INDEX_UNSET) { + adGroupIndex = + adPlaybackState.getAdGroupIndexAfterPositionUs( + playerPositionUs, C.msToUs(contentDurationMs)); + } + return adGroupIndex; + } + private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; @@ -1379,7 +1485,9 @@ public final class ImaAdsLoader Player player, Timeline timeline, Timeline.Period period) { long contentWindowPositionMs = player.getContentPosition(); return contentWindowPositionMs - - timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs(); + - (timeline.isEmpty() + ? 0 + : timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs()); } private static long[] getAdGroupTimesUs(List cuePoints) { diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index ddcd1ae483..18515f0625 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -48,7 +48,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory; -import com.google.android.exoplayer2.source.MaskingMediaSource; +import com.google.android.exoplayer2.source.MaskingMediaSource.DummyTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; @@ -57,6 +57,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -73,22 +74,23 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; +import org.robolectric.shadows.ShadowSystemClock; -/** Test for {@link ImaAdsLoader}. */ +/** Tests for {@link ImaAdsLoader}. */ @RunWith(AndroidJUnit4.class) public final class ImaAdsLoaderTest { private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final Timeline CONTENT_TIMELINE = new FakeTimeline( - new FakeTimeline.TimelineWindowDefinition( + new TimelineWindowDefinition( /* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US)); private static final long CONTENT_PERIOD_DURATION_US = CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; private static final Uri TEST_URI = Uri.EMPTY; private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString()); private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; - private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; + private static final long[][] ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f}; @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -140,14 +142,14 @@ public final class ImaAdsLoaderTest { @Test public void builder_overridesPlayerType() { when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type"); - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima"); } @Test public void start_setsAdUiViewGroup() { - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); @@ -156,8 +158,8 @@ public final class ImaAdsLoaderTest { @Test public void start_withPlaceholderContent_initializedAdsLoader() { - Timeline placeholderTimeline = new MaskingMediaSource.DummyTimeline(/* tag= */ null); - setupPlayback(placeholderTimeline, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null); + setupPlayback(placeholderTimeline, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); // We'll only create the rendering settings when initializing the ads loader. @@ -166,26 +168,26 @@ public final class ImaAdsLoaderTest { @Test public void start_updatesAdPlaybackState() { - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( new AdPlaybackState(/* adGroupTimesUs...= */ 0) - .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) + .withAdDurationsUs(ADS_DURATIONS_US) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @Test public void startAfterRelease() { - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start(adsLoaderListener, adViewProvider); } @Test public void startAndCallbacksAfterRelease() { - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.release(); imaAdsLoader.start(adsLoaderListener, adViewProvider); fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); @@ -212,7 +214,7 @@ public final class ImaAdsLoaderTest { @Test public void playback_withPrerollAd_marksAdAsPlayed() { - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); // Load the preroll ad. imaAdsLoader.start(adsLoaderListener, adViewProvider); @@ -245,14 +247,64 @@ public final class ImaAdsLoaderTest { .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI) - .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) + .withAdDurationsUs(ADS_DURATIONS_US) .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) .withAdResumePositionUs(/* adResumePositionUs= */ 0)); } + @Test + public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() { + // Simulate an ad at 2 seconds. + long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND; + setupPlayback( + CONTENT_TIMELINE, + ADS_DURATIONS_US, + new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND}); + + // Advance playback to just before the midroll and simulate buffering. + imaAdsLoader.start(adsLoaderListener, adViewProvider); + fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs)); + fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); + // Advance before the timeout and simulating polling content progress. + ShadowSystemClock.advanceBy(Duration.ofSeconds(1)); + imaAdsLoader.getContentProgress(); + + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdDurationsUs(ADS_DURATIONS_US)); + } + + @Test + public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() { + // Simulate an ad at 2 seconds. + long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND; + setupPlayback( + CONTENT_TIMELINE, + ADS_DURATIONS_US, + new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND}); + + // Advance playback to just before the midroll and simulate buffering. + imaAdsLoader.start(adsLoaderListener, adViewProvider); + fakeExoPlayer.setPlayingContentPosition(C.usToMs(adGroupPositionInWindowUs)); + fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true); + // Advance past the timeout and simulate polling content progress. + ShadowSystemClock.advanceBy(Duration.ofSeconds(5)); + imaAdsLoader.getContentProgress(); + + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdDurationsUs(ADS_DURATIONS_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); + } + @Test public void stop_unregistersAllVideoControlOverlays() { - setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.requestAds(adViewGroup); imaAdsLoader.stop(); @@ -282,31 +334,31 @@ public final class ImaAdsLoaderTest { List adsLoadedListeners = new ArrayList<>(); doAnswer( - invocation -> { - adsLoadedListeners.add(invocation.getArgument(0)); - return null; - }) + invocation -> { + adsLoadedListeners.add(invocation.getArgument(0)); + return null; + }) .when(mockAdsLoader) .addAdsLoadedListener(any()); doAnswer( - invocation -> { - adsLoadedListeners.remove(invocation.getArgument(0)); - return null; - }) + invocation -> { + adsLoadedListeners.remove(invocation.getArgument(0)); + return null; + }) .when(mockAdsLoader) .removeAdsLoadedListener(any()); when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager); when(mockAdsManagerLoadedEvent.getUserRequestContext()) .thenAnswer(invocation -> mockAdsRequest.getUserRequestContext()); doAnswer( - (Answer) - invocation -> { - for (com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener listener : - adsLoadedListeners) { - listener.onAdsManagerLoaded(mockAdsManagerLoadedEvent); - } - return null; - }) + (Answer) + invocation -> { + for (com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener listener : + adsLoadedListeners) { + listener.onAdsManagerLoaded(mockAdsManagerLoadedEvent); + } + return null; + }) .when(mockAdsLoader) .requestAds(mockAdsRequest); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index dee63d819e..783a452b1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -360,6 +360,18 @@ public final class AdPlaybackState { return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; } + /** Returns whether the specified ad has been marked as in {@link #AD_STATE_ERROR}. */ + public boolean isAdInErrorState(int adGroupIndex, int adIndexInAdGroup) { + if (adGroupIndex >= adGroups.length) { + return false; + } + AdGroup adGroup = adGroups[adGroupIndex]; + if (adGroup.count == C.LENGTH_UNSET || adIndexInAdGroup >= adGroup.count) { + return false; + } + return adGroup.states[adIndexInAdGroup] == AdPlaybackState.AD_STATE_ERROR; + } + /** * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}. * The ad count must be greater than zero. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index 0cd27a90c0..bd4dd8876f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -64,7 +64,9 @@ public final class AdPlaybackStateTest { assertThat(state.adGroups[0].uris[0]).isNull(); assertThat(state.adGroups[0].states[0]).isEqualTo(AdPlaybackState.AD_STATE_ERROR); + assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)).isTrue(); assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); + assertThat(state.isAdInErrorState(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)).isFalse(); } @Test From 51a3f214ed6c5bd49ef1a43470399cfd01a40f49 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 18 May 2020 14:05:47 +0100 Subject: [PATCH 65/80] Fix handling of fetch errors for post-rolls The ad break time in seconds from IMA was "-1" for postrolls, but this didn't match C.TIME_END_OF_SOURCE in the ad group times array. Handle an ad break time of -1 directly by mapping it onto the last ad group, instead of trying to look it up in the array. PiperOrigin-RevId: 312064886 --- extensions/ima/build.gradle | 1 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 6 +++-- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 23 +++++++++++++++++++ .../google/android/exoplayer2/util/Util.java | 18 +++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 2ed44f638c..b83caf62ee 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' testImplementation project(modulePrefix + 'testutils') + testImplementation 'com.google.guava:guava:' + guavaVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index b151a595c0..19109d9c04 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -1105,8 +1105,10 @@ public final class ImaAdsLoader } int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString); int adGroupIndex = - Arrays.binarySearch( - adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds); + adGroupTimeSeconds == -1 + ? adPlaybackState.adGroupCount - 1 + : Util.linearSearch( + adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds); AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 18515f0625..804434b835 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -56,6 +56,7 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; @@ -105,6 +106,7 @@ public final class ImaAdsLoaderTest { @Mock private ImaFactory mockImaFactory; @Mock private AdPodInfo mockAdPodInfo; @Mock private Ad mockPrerollSingleAd; + @Mock private AdEvent mockPostrollFetchErrorAdEvent; private ViewGroup adViewGroup; private View adOverlayView; @@ -252,6 +254,23 @@ public final class ImaAdsLoaderTest { .withAdResumePositionUs(/* adResumePositionUs= */ 0)); } + @Test + public void playback_withPostrollFetchError_marksAdAsInErrorState() { + setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, new Float[] {-1f}); + + // Simulate loading an empty postroll ad. + imaAdsLoader.start(adsLoaderListener, adViewProvider); + imaAdsLoader.onAdEvent(mockPostrollFetchErrorAdEvent); + + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdDurationsUs(ADS_DURATIONS_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); + } + @Test public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() { // Simulate an ad at 2 seconds. @@ -372,6 +391,10 @@ public final class ImaAdsLoaderTest { when(mockAdPodInfo.getAdPosition()).thenReturn(1); when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockAdPodInfo); + + when(mockPostrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); + when(mockPostrollFetchErrorAdEvent.getAdData()) + .thenReturn(ImmutableMap.of("adBreakTime", "-1")); } private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index ea43ee7bb3..a7a46b163d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -726,6 +726,24 @@ public final class Util { return C.INDEX_UNSET; } + /** + * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link + * C#INDEX_UNSET} if {@code value} is not contained in {@code array}. + * + * @param array The array to search. + * @param value The value to search for. + * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET} + * if {@code value} is not contained in {@code array}. + */ + public static int linearSearch(long[] array, long value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) { + return i; + } + } + return C.INDEX_UNSET; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. From 35a705e92a278a70becfbb9c9c7197eded4c0069 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 18 May 2020 16:10:17 +0100 Subject: [PATCH 66/80] Add release notes for issues fixed by preloading migration PiperOrigin-RevId: 312080838 --- RELEASENOTES.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a2a439c52f..25acc4f022 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -55,9 +55,23 @@ * IMA extension: * Upgrade to IMA SDK version 3.19.0, and migrate to new preloading APIs - ([#6429](https://github.com/google/ExoPlayer/issues/6429)). + ([#6429](https://github.com/google/ExoPlayer/issues/6429)). This fixes + several issues involving preloading and handling of ad loading error + cases: ([#4140](https://github.com/google/ExoPlayer/issues/4140), + [#5006](https://github.com/google/ExoPlayer/issues/5006), + [#6030](https://github.com/google/ExoPlayer/issues/6030), + [#6097](https://github.com/google/ExoPlayer/issues/6097), + [#6425](https://github.com/google/ExoPlayer/issues/6425), + [#6967](https://github.com/google/ExoPlayer/issues/6967), + [#7041](https://github.com/google/ExoPlayer/issues/7041), + [#7161](https://github.com/google/ExoPlayer/issues/7161), + [#7212](https://github.com/google/ExoPlayer/issues/7212), + [#7340](https://github.com/google/ExoPlayer/issues/7340)). * Add support for timing out ad preloading, to avoid playback getting - stuck if an ad group unexpectedly fails to load. + stuck if an ad group unexpectedly fails to load + ([#5444](https://github.com/google/ExoPlayer/issues/5444), + [#5966](https://github.com/google/ExoPlayer/issues/5966) + [#7002](https://github.com/google/ExoPlayer/issues/7002)). ### 2.11.4 (2020-04-08) ### From b5e5b55ef850a30df3a73a7c4e4a449313b52fde Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 18 May 2020 16:30:12 +0100 Subject: [PATCH 67/80] Fix typo PiperOrigin-RevId: 312083761 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 25acc4f022..370dd47775 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -70,7 +70,7 @@ * Add support for timing out ad preloading, to avoid playback getting stuck if an ad group unexpectedly fails to load ([#5444](https://github.com/google/ExoPlayer/issues/5444), - [#5966](https://github.com/google/ExoPlayer/issues/5966) + [#5966](https://github.com/google/ExoPlayer/issues/5966), [#7002](https://github.com/google/ExoPlayer/issues/7002)). ### 2.11.4 (2020-04-08) ### From 1062edf52e3b31553d23693a41f1363af536040f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 1 Jun 2020 09:50:40 +0100 Subject: [PATCH 68/80] Revert "Update TrackSelectionDialogBuilder to use androidx compat Dialog." This reverts commit b05e9944ea95c2b1a341610568e5cfbe8df6f333. --- RELEASENOTES.md | 2 -- library/ui/build.gradle | 1 - .../android/exoplayer2/ui/TrackSelectionDialogBuilder.java | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 370dd47775..2706e302cd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,8 +33,6 @@ * Fix `DefaultTimeBar` to respect touch transformations ([#7303](https://github.com/google/ExoPlayer/issues/7303)). * Add `showScrubber` and `hideScrubber` methods to `DefaultTimeBar`. - * Update `TrackSelectionDialogBuilder` to use AndroidX Compat Dialog - ([#7357](https://github.com/google/ExoPlayer/issues/7357)). * Text: * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles. diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 8727ba416a..b6bf139963 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -40,7 +40,6 @@ dependencies { implementation project(modulePrefix + 'library-core') api 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java index 5c91645a4c..f8a016bc8b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java @@ -15,12 +15,12 @@ */ package com.google.android.exoplayer2.ui; +import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; From 18eccf9a819c7c3a0c840711a4b1ae0e8364e864 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 Apr 2020 19:59:10 +0100 Subject: [PATCH 69/80] Make sure not to create new playback sessions while still IDLE. The first session should only be created once we have the media items and/or called prepare. Otherwise the first session is created with an EventTime having an empty timeline making it less useful. issue:#7193 PiperOrigin-RevId: 308100555 --- .../analytics/PlaybackStatsListener.java | 41 +++++++++++-------- .../analytics/PlaybackStatsListenerTest.java | 39 +++++++++++++++++- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 43d2496842..d45e96166f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -243,7 +243,7 @@ public final class PlaybackStatsListener EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { this.playWhenReady = playWhenReady; this.playbackState = playbackState; - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); playbackStatsTrackers @@ -256,7 +256,7 @@ public final class PlaybackStatsListener public void onPlaybackSuppressionReasonChanged( EventTime eventTime, int playbackSuppressionReason) { isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); playbackStatsTrackers @@ -268,7 +268,7 @@ public final class PlaybackStatsListener @Override public void onTimelineChanged(EventTime eventTime, int reason) { sessionManager.handleTimelineUpdate(eventTime); - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); @@ -279,7 +279,7 @@ public final class PlaybackStatsListener @Override public void onPositionDiscontinuity(EventTime eventTime, int reason) { sessionManager.handlePositionDiscontinuity(eventTime, reason); - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); @@ -289,7 +289,7 @@ public final class PlaybackStatsListener @Override public void onSeekStarted(EventTime eventTime) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onSeekStarted(eventTime); @@ -299,7 +299,7 @@ public final class PlaybackStatsListener @Override public void onSeekProcessed(EventTime eventTime) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onSeekProcessed(eventTime); @@ -309,7 +309,7 @@ public final class PlaybackStatsListener @Override public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onFatalError(eventTime, error); @@ -321,7 +321,7 @@ public final class PlaybackStatsListener public void onPlaybackParametersChanged( EventTime eventTime, PlaybackParameters playbackParameters) { playbackSpeed = playbackParameters.speed; - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); } @@ -330,7 +330,7 @@ public final class PlaybackStatsListener @Override public void onTracksChanged( EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); @@ -341,7 +341,7 @@ public final class PlaybackStatsListener @Override public void onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onLoadStarted(eventTime); @@ -351,7 +351,7 @@ public final class PlaybackStatsListener @Override public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); @@ -366,7 +366,7 @@ public final class PlaybackStatsListener int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); @@ -377,7 +377,7 @@ public final class PlaybackStatsListener @Override public void onBandwidthEstimate( EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); @@ -388,7 +388,7 @@ public final class PlaybackStatsListener @Override public void onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onAudioUnderrun(); @@ -398,7 +398,7 @@ public final class PlaybackStatsListener @Override public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); @@ -413,7 +413,7 @@ public final class PlaybackStatsListener MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); @@ -423,7 +423,7 @@ public final class PlaybackStatsListener @Override public void onDrmSessionManagerError(EventTime eventTime, Exception error) { - sessionManager.updateSessions(eventTime); + maybeAddSession(eventTime); for (String session : playbackStatsTrackers.keySet()) { if (sessionManager.belongsToSession(eventTime, session)) { playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); @@ -431,6 +431,13 @@ public final class PlaybackStatsListener } } + private void maybeAddSession(EventTime eventTime) { + boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE; + if (!isCompletelyIdle) { + sessionManager.updateSessions(eventTime); + } + } + /** Tracker for playback stats of a single playback. */ private static final class PlaybackStatsTracker { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java index 10122d36ec..ef3e4d1434 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -21,6 +21,8 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,7 +30,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class PlaybackStatsListenerTest { - private static final AnalyticsListener.EventTime TEST_EVENT_TIME = + private static final AnalyticsListener.EventTime EMPTY_TIMELINE_EVENT_TIME = new AnalyticsListener.EventTime( /* realtimeMs= */ 500, Timeline.EMPTY, @@ -37,6 +39,41 @@ public final class PlaybackStatsListenerTest { /* eventPlaybackPositionMs= */ 0, /* currentPlaybackPositionMs= */ 0, /* totalBufferedDurationMs= */ 0); + private static final Timeline TEST_TIMELINE = new FakeTimeline(/* windowCount= */ 1); + private static final AnalyticsListener.EventTime TEST_EVENT_TIME = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 700, + TEST_TIMELINE, + /* windowIndex= */ 0, + new MediaSource.MediaPeriodId( + TEST_TIMELINE.getPeriod( + /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true) + .uid, + /* windowSequenceNumber= */ 42), + /* eventPlaybackPositionMs= */ 123, + /* currentPlaybackPositionMs= */ 123, + /* totalBufferedDurationMs= */ 456); + + @Test + public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); + + playbackStatsListener.onPlayerStateChanged( + EMPTY_TIMELINE_EVENT_TIME, /* playWhenReady= */ false, Player.STATE_BUFFERING); + + assertThat(playbackStatsListener.getPlaybackStats()).isNotNull(); + } + + @Test + public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() { + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null); + + playbackStatsListener.onTimelineChanged(TEST_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_DYNAMIC); + + assertThat(playbackStatsListener.getPlaybackStats()).isNotNull(); + } @Test public void playback_withKeepHistory_updatesStats() { From f4cc1d6250adb1cb110b43221c87a39624ed8b37 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 24 Apr 2020 16:02:35 +0100 Subject: [PATCH 70/80] Make sure finishAllSessions() can be called without removing listener Currently, this method is only supposed to be called before removing the listener from the player or when releasing the player. If called at other times, it will throw an exception later when a playback session is ended automatically. issue:#7193 PiperOrigin-RevId: 308254993 --- .../DefaultPlaybackSessionManager.java | 15 ++++ .../analytics/PlaybackSessionManager.java | 8 ++ .../analytics/PlaybackStatsListener.java | 5 +- .../DefaultPlaybackSessionManagerTest.java | 26 +++++++ .../analytics/PlaybackStatsListenerTest.java | 77 ++++++++++++++++++- 5 files changed, 126 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java index 44f8c10afe..1fbcf80dc1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -168,6 +168,21 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag updateActiveSession(eventTime, activeSessionDescriptor); } + @Override + public void finishAllSessions(EventTime eventTime) { + currentMediaPeriodId = null; + activeSessionId = null; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + iterator.remove(); + if (session.isCreated && listener != null) { + listener.onSessionFinished( + eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); + } + } + } + private SessionDescriptor getOrAddSession( int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java index 53d63e23fc..7045779125 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java @@ -117,4 +117,12 @@ public interface PlaybackSessionManager { * @param reason The {@link DiscontinuityReason}. */ void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); + + /** + * Finishes all existing sessions and calls their respective {@link + * Listener#onSessionFinished(EventTime, String, boolean)} callback. + * + * @param eventTime The event time at which sessions are finished. + */ + void finishAllSessions(EventTime eventTime); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index d45e96166f..46c0a05342 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -148,7 +148,6 @@ public final class PlaybackStatsListener // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with // an actual EventTime. Should also simplify other cases where the listener needs to be released // separately from the player. - HashMap trackerCopy = new HashMap<>(playbackStatsTrackers); EventTime dummyEventTime = new EventTime( SystemClock.elapsedRealtime(), @@ -158,9 +157,7 @@ public final class PlaybackStatsListener /* eventPlaybackPositionMs= */ 0, /* currentPlaybackPositionMs= */ 0, /* totalBufferedDurationMs= */ 0); - for (String session : trackerCopy.keySet()) { - onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false); - } + sessionManager.finishAllSessions(dummyEventTime); } // PlaybackSessionManager.Listener implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java index f0b18b4a20..6828a04a37 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -1044,6 +1045,31 @@ public final class DefaultPlaybackSessionManagerTest { verify(mockListener, never()).onSessionActive(any(), eq(adSessionId2)); } + @Test + public void finishAllSessions_callsOnSessionFinishedForAllCreatedSessions() { + Timeline timeline = new FakeTimeline(/* windowCount= */ 4); + EventTime eventTimeWindow0 = + createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + EventTime eventTimeWindow2 = + createEventTime(timeline, /* windowIndex= */ 2, /* mediaPeriodId= */ null); + // Actually create sessions for window 0 and 2. + sessionManager.updateSessions(eventTimeWindow0); + sessionManager.updateSessions(eventTimeWindow2); + // Query information about session for window 1, but don't create it. + sessionManager.getSessionForMediaPeriodId( + timeline, + new MediaPeriodId( + timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true).uid, + /* windowSequenceNumber= */ 123)); + verify(mockListener, times(2)).onSessionCreated(any(), anyString()); + + EventTime finishEventTime = + createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null); + sessionManager.finishAllSessions(finishEventTime); + + verify(mockListener, times(2)).onSessionFinished(eq(finishEventTime), anyString(), eq(false)); + } + private static EventTime createEventTime( Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { return new EventTime( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java index ef3e4d1434..41db4dc570 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -16,7 +16,14 @@ package com.google.android.exoplayer2.analytics; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import android.os.SystemClock; import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player; @@ -42,7 +49,7 @@ public final class PlaybackStatsListenerTest { private static final Timeline TEST_TIMELINE = new FakeTimeline(/* windowCount= */ 1); private static final AnalyticsListener.EventTime TEST_EVENT_TIME = new AnalyticsListener.EventTime( - /* realtimeMs= */ 700, + /* realtimeMs= */ 500, TEST_TIMELINE, /* windowIndex= */ 0, new MediaSource.MediaPeriodId( @@ -108,4 +115,72 @@ public final class PlaybackStatsListenerTest { assertThat(playbackStats).isNotNull(); assertThat(playbackStats.endedCount).isEqualTo(1); } + + @Test + public void finishedSession_callsCallback() { + PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, callback); + + // Create session with an event and finish it by simulating removal from playlist. + playbackStatsListener.onPlayerStateChanged( + TEST_EVENT_TIME, /* playWhenReady= */ false, Player.STATE_BUFFERING); + verify(callback, never()).onPlaybackStatsReady(any(), any()); + playbackStatsListener.onTimelineChanged( + EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_DYNAMIC); + + verify(callback).onPlaybackStatsReady(eq(TEST_EVENT_TIME), any()); + } + + @Test + public void finishAllSessions_callsAllPendingCallbacks() { + AnalyticsListener.EventTime eventTimeWindow0 = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 0, + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + AnalyticsListener.EventTime eventTimeWindow1 = + new AnalyticsListener.EventTime( + /* realtimeMs= */ 0, + Timeline.EMPTY, + /* windowIndex= */ 1, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, callback); + playbackStatsListener.onPlayerStateChanged( + eventTimeWindow0, /* playWhenReady= */ false, Player.STATE_BUFFERING); + playbackStatsListener.onPlayerStateChanged( + eventTimeWindow1, /* playWhenReady= */ false, Player.STATE_BUFFERING); + + playbackStatsListener.finishAllSessions(); + + verify(callback, times(2)).onPlaybackStatsReady(any(), any()); + verify(callback).onPlaybackStatsReady(eq(eventTimeWindow0), any()); + verify(callback).onPlaybackStatsReady(eq(eventTimeWindow1), any()); + } + + @Test + public void finishAllSessions_doesNotCallCallbackAgainWhenSessionWouldBeAutomaticallyFinished() { + PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); + PlaybackStatsListener playbackStatsListener = + new PlaybackStatsListener(/* keepHistory= */ true, callback); + playbackStatsListener.onPlayerStateChanged( + TEST_EVENT_TIME, /* playWhenReady= */ false, Player.STATE_BUFFERING); + SystemClock.setCurrentTimeMillis(TEST_EVENT_TIME.realtimeMs + 100); + + playbackStatsListener.finishAllSessions(); + // Simulate removing the playback item to ensure the session would finish if it hadn't already. + playbackStatsListener.onTimelineChanged( + EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_DYNAMIC); + + verify(callback).onPlaybackStatsReady(any(), any()); + } } From 145754618d3a5f3aa88dd7b7fba47803b19c61d5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 13 Jan 2020 14:52:14 +0000 Subject: [PATCH 71/80] Simplify keeping track of current id in DefaultPlaybackSessionManager We currently have a currentMediaPeriodId and an activeSessionId that are more or less tracking the same thing unless the current media period isn't "active" yet. Simplify this logic by using a single currentSessionId field and the respective isActive flag of this session. Also move all session creation and activation code in the same method to make it easier to reason about the code. This change also fixes a subtle bug where events after a seek to a new window are not ignored as they should. PiperOrigin-RevId: 289432181 --- .../DefaultPlaybackSessionManager.java | 95 ++++++++++--------- .../DefaultPlaybackSessionManagerTest.java | 14 +++ 2 files changed, 62 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java index 1fbcf80dc1..04f3ba154a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -29,7 +29,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Random; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the @@ -48,8 +47,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag private @MonotonicNonNull Listener listener; private Timeline currentTimeline; - @Nullable private MediaPeriodId currentMediaPeriodId; - @Nullable private String activeSessionId; + @Nullable private String currentSessionId; /** Creates session manager. */ public DefaultPlaybackSessionManager() { @@ -83,22 +81,34 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag @Override public synchronized void updateSessions(EventTime eventTime) { - boolean isObviouslyFinished = - eventTime.mediaPeriodId != null - && currentMediaPeriodId != null - && eventTime.mediaPeriodId.windowSequenceNumber - < currentMediaPeriodId.windowSequenceNumber; - if (!isObviouslyFinished) { - SessionDescriptor descriptor = - getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); - if (!descriptor.isCreated) { - descriptor.isCreated = true; - Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId); - if (activeSessionId == null) { - updateActiveSession(eventTime, descriptor); - } + Assertions.checkNotNull(listener); + @Nullable SessionDescriptor currentSession = sessions.get(currentSessionId); + if (eventTime.mediaPeriodId != null && currentSession != null) { + // If we receive an event associated with a media period, then it needs to be either part of + // the current window if it's the first created media period, or a window that will be played + // in the future. Otherwise, we know that it belongs to a session that was already finished + // and we can ignore the event. + boolean isAlreadyFinished = + currentSession.windowSequenceNumber == C.INDEX_UNSET + ? currentSession.windowIndex != eventTime.windowIndex + : eventTime.mediaPeriodId.windowSequenceNumber < currentSession.windowSequenceNumber; + if (isAlreadyFinished) { + return; } } + SessionDescriptor eventSession = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (currentSessionId == null) { + currentSessionId = eventSession.sessionId; + } + if (!eventSession.isCreated) { + eventSession.isCreated = true; + listener.onSessionCreated(eventTime, eventSession.sessionId); + } + if (eventSession.sessionId.equals(currentSessionId) && !eventSession.isActive) { + eventSession.isActive = true; + listener.onSessionActive(eventTime, eventSession.sessionId); + } } @Override @@ -112,8 +122,8 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) { iterator.remove(); if (session.isCreated) { - if (session.sessionId.equals(activeSessionId)) { - activeSessionId = null; + if (session.sessionId.equals(currentSessionId)) { + currentSessionId = null; } listener.onSessionFinished( eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); @@ -136,42 +146,46 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag if (session.isFinishedAtEventTime(eventTime)) { iterator.remove(); if (session.isCreated) { - boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId); - boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession; - if (isRemovingActiveSession) { - activeSessionId = null; + boolean isRemovingCurrentSession = session.sessionId.equals(currentSessionId); + boolean isAutomaticTransition = + hasAutomaticTransition && isRemovingCurrentSession && session.isActive; + if (isRemovingCurrentSession) { + currentSessionId = null; } listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition); } } } - SessionDescriptor activeSessionDescriptor = + @Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId); + SessionDescriptor currentSessionDescriptor = getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + currentSessionId = currentSessionDescriptor.sessionId; if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd() - && (currentMediaPeriodId == null - || currentMediaPeriodId.windowSequenceNumber + && (previousSessionDescriptor == null + || previousSessionDescriptor.windowSequenceNumber != eventTime.mediaPeriodId.windowSequenceNumber - || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex - || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) { + || previousSessionDescriptor.adMediaPeriodId == null + || previousSessionDescriptor.adMediaPeriodId.adGroupIndex + != eventTime.mediaPeriodId.adGroupIndex + || previousSessionDescriptor.adMediaPeriodId.adIndexInAdGroup + != eventTime.mediaPeriodId.adIndexInAdGroup)) { // New ad playback started. Find corresponding content session and notify ad playback started. MediaPeriodId contentMediaPeriodId = new MediaPeriodId( eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber); SessionDescriptor contentSession = getOrAddSession(eventTime.windowIndex, contentMediaPeriodId); - if (contentSession.isCreated && activeSessionDescriptor.isCreated) { + if (contentSession.isCreated && currentSessionDescriptor.isCreated) { listener.onAdPlaybackStarted( - eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId); + eventTime, contentSession.sessionId, currentSessionDescriptor.sessionId); } } - updateActiveSession(eventTime, activeSessionDescriptor); } @Override public void finishAllSessions(EventTime eventTime) { - currentMediaPeriodId = null; - activeSessionId = null; + currentSessionId = null; Iterator iterator = sessions.values().iterator(); while (iterator.hasNext()) { SessionDescriptor session = iterator.next(); @@ -214,18 +228,6 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag return bestMatch; } - @RequiresNonNull("listener") - private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) { - currentMediaPeriodId = eventTime.mediaPeriodId; - if (sessionDescriptor.isCreated) { - activeSessionId = sessionDescriptor.sessionId; - if (!sessionDescriptor.isActive) { - sessionDescriptor.isActive = true; - listener.onSessionActive(eventTime, sessionDescriptor.sessionId); - } - } - } - private static String generateSessionId() { byte[] randomBytes = new byte[SESSION_ID_LENGTH]; RANDOM.nextBytes(randomBytes); @@ -299,8 +301,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { if (windowSequenceNumber == C.INDEX_UNSET && eventWindowIndex == windowIndex - && eventMediaPeriodId != null - && !eventMediaPeriodId.isAd()) { + && eventMediaPeriodId != null) { // Set window sequence number for this session as soon as we have one. windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java index 6828a04a37..1de3a8d1e5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java @@ -502,6 +502,7 @@ public final class DefaultPlaybackSessionManagerTest { createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null); sessionManager.handleTimelineUpdate(newTimelineEventTime); + sessionManager.updateSessions(newTimelineEventTime); ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class); ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class); @@ -658,6 +659,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + sessionManager.updateSessions(eventTime2); verify(mockListener).onSessionCreated(eq(eventTime1), anyString()); verify(mockListener).onSessionActive(eq(eventTime1), anyString()); @@ -689,6 +691,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + sessionManager.updateSessions(eventTime2); verify(mockListener).onSessionCreated(eventTime1, sessionId1); verify(mockListener).onSessionActive(eventTime1, sessionId1); @@ -723,6 +726,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(eventTime2); verify(mockListener).onSessionCreated(eventTime1, sessionId1); verify(mockListener).onSessionActive(eventTime1, sessionId1); @@ -749,6 +753,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.updateSessions(eventTime2); sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(eventTime2); verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); } @@ -791,6 +796,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId); sessionManager.handlePositionDiscontinuity(eventTime3, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(eventTime3); verify(mockListener).onSessionCreated(eventTime1, sessionId1); verify(mockListener).onSessionActive(eventTime1, sessionId1); @@ -852,6 +858,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( contentEventTime, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(contentEventTime); verify(mockListener).onSessionCreated(adEventTime1, adSessionId1); verify(mockListener).onSessionActive(adEventTime1, adSessionId1); @@ -859,6 +866,8 @@ public final class DefaultPlaybackSessionManagerTest { verify(mockListener) .onSessionFinished( contentEventTime, adSessionId1, /* automaticTransitionToNextPlayback= */ true); + verify(mockListener).onSessionCreated(eq(contentEventTime), anyString()); + verify(mockListener).onSessionActive(eq(contentEventTime), anyString()); verifyNoMoreInteractions(mockListener); } @@ -909,6 +918,7 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(adEventTime1); verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean()); } @@ -965,7 +975,9 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.handlePositionDiscontinuity( adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(adEventTime1); sessionManager.handlePositionDiscontinuity(adEventTime2, Player.DISCONTINUITY_REASON_SEEK); + sessionManager.updateSessions(adEventTime2); verify(mockListener).onSessionCreated(eq(contentEventTime), anyString()); verify(mockListener).onSessionActive(eq(contentEventTime), anyString()); @@ -1035,8 +1047,10 @@ public final class DefaultPlaybackSessionManagerTest { sessionManager.updateSessions(adEventTime1); sessionManager.handlePositionDiscontinuity( adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(adEventTime1); sessionManager.handlePositionDiscontinuity( contentEventTime2, Player.DISCONTINUITY_REASON_AD_INSERTION); + sessionManager.updateSessions(contentEventTime2); String adSessionId2 = sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId); From 9f87c2eaefa2262dadcfb59d64ada484fe491f9e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 1 Jun 2020 19:12:39 +0100 Subject: [PATCH 72/80] Finalize release notes --- RELEASENOTES.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2706e302cd..2422591442 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,18 +38,6 @@ subtitles. * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct color. -* Cronet extension: Default to using the Cronet implementation in Google Play - Services rather than Cronet Embedded. This allows Cronet to be used with a - negligible increase in application size, compared to approximately 8MB when - embedding the library. -* OkHttp extension: Upgrade OkHttp dependency to 3.12.11. -* MediaSession extension: - * Only set the playback state to `BUFFERING` if `playWhenReady` is true - ([#7206](https://github.com/google/ExoPlayer/issues/7206)). - * Add missing `@Nullable` annotations to `MediaSessionConnector` - ([#7234](https://github.com/google/ExoPlayer/issues/7234)). -* AV1 extension: Add a heuristic to determine the default number of threads - used for AV1 playback using the extension. * IMA extension: * Upgrade to IMA SDK version 3.19.0, and migrate to new preloading APIs @@ -70,6 +58,18 @@ ([#5444](https://github.com/google/ExoPlayer/issues/5444), [#5966](https://github.com/google/ExoPlayer/issues/5966), [#7002](https://github.com/google/ExoPlayer/issues/7002)). +* Cronet extension: Default to using the Cronet implementation in Google Play + Services rather than Cronet Embedded. This allows Cronet to be used with a + negligible increase in application size, compared to approximately 8MB when + embedding the library. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.11. +* MediaSession extension: + * Only set the playback state to `BUFFERING` if `playWhenReady` is true + ([#7206](https://github.com/google/ExoPlayer/issues/7206)). + * Add missing `@Nullable` annotations to `MediaSessionConnector` + ([#7234](https://github.com/google/ExoPlayer/issues/7234)). +* AV1 extension: Add a heuristic to determine the default number of threads + used for AV1 playback using the extension. ### 2.11.4 (2020-04-08) ### From 5e2b89b562e6970c1f8ffca8efa9fb20d23dbac6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 1 Jun 2020 19:15:15 +0100 Subject: [PATCH 73/80] Add release date --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2422591442..e953578ee2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes # -### 2.11.5 (not yet released) ### +### 2.11.5 (2020-06-03) ### * Add `SilenceMediaSource.Factory` to support tags. * Enable the configuration of `SilenceSkippingAudioProcessor` From 79acadcc893675afdfa01f10f47d05f7736522ef Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 2 Jun 2020 14:53:15 +0100 Subject: [PATCH 74/80] Check for section_syntax_indicator in TS tables Issue:#7325 PiperOrigin-RevId: 314321914 --- .../exoplayer2/extractor/ts/TsExtractor.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2cd7398d7c..2bd5b12551 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -460,10 +460,15 @@ public final class TsExtractor implements Extractor { // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. return; } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), - // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), - // section_number (8), last_section_number (8) - sectionData.skipBytes(7); + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(4) + int secondHeaderByte = sectionData.readUnsignedByte(); + if ((secondHeaderByte & 0x80) == 0) { + // section_syntax_indicator must be 1. See ISO/IEC 13818-1, section 2.4.4.5. + return; + } + // section_length(8), transport_stream_id (16), reserved (2), version_number (5), + // current_next_indicator (1), section_number (8), last_section_number (8) + sectionData.skipBytes(6); int programCount = sectionData.bytesLeft() / 4; for (int i = 0; i < programCount; i++) { @@ -535,8 +540,14 @@ public final class TsExtractor implements Extractor { timestampAdjusters.add(timestampAdjuster); } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) - sectionData.skipBytes(2); + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(4) + int secondHeaderByte = sectionData.readUnsignedByte(); + if ((secondHeaderByte & 0x80) == 0) { + // section_syntax_indicator must be 1. See ISO/IEC 13818-1, section 2.4.4.9. + return; + } + // section_length(8) + sectionData.skipBytes(1); int programNumber = sectionData.readUnsignedShort(); // Skip 3 bytes (24 bits), including: From fb011e66a6ff164c76dc8f17b4df152a1848835b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jun 2020 22:48:15 +0100 Subject: [PATCH 75/80] AudioTrackPositionTracker: Prevent negative timestamps Issue: #7456 PiperOrigin-RevId: 314408767 --- RELEASENOTES.md | 6 ++++-- .../android/exoplayer2/audio/AudioTrackPositionTracker.java | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e953578ee2..5c37ac1245 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,8 +11,9 @@ * Fix "Not allowed to start service" `IllegalStateException` in `DownloadService` ([#7306](https://github.com/google/ExoPlayer/issues/7306)). -* Ads: - * Fix `AdsMediaSource` child `MediaSource`s not being released. +* Fix issue in `AudioTrackPositionTracker` that could cause negative positions + to be reported at the start of playback and immediately after seeking + ([#7456](https://github.com/google/ExoPlayer/issues/7456). * DASH: * Merge trick play adaptation sets (i.e., adaptation sets marked with `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as @@ -58,6 +59,7 @@ ([#5444](https://github.com/google/ExoPlayer/issues/5444), [#5966](https://github.com/google/ExoPlayer/issues/5966), [#7002](https://github.com/google/ExoPlayer/issues/7002)). + * Fix `AdsMediaSource` child `MediaSource`s not being released. * Cronet extension: Default to using the Cronet implementation in Google Play Services rather than Cronet Embedded. This allows Cronet to be used with a negligible increase in application size, compared to approximately 8MB when diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index 4ee70bd813..f227a6f3d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -206,6 +206,7 @@ import java.lang.reflect.Method; hasData = false; stopTimestampUs = C.TIME_UNSET; forceResetWorkaroundTimeMs = C.TIME_UNSET; + lastLatencySampleTimeUs = 0; latencyUs = 0; } @@ -239,7 +240,7 @@ import java.lang.reflect.Method; positionUs = systemTimeUs + smoothedPlayheadOffsetUs; } if (!sourceEnded) { - positionUs -= latencyUs; + positionUs = Math.max(0, positionUs - latencyUs); } return positionUs; } @@ -353,7 +354,7 @@ import java.lang.reflect.Method; } /** - * Resets the position tracker. Should be called when the audio track previous passed to {@link + * Resets the position tracker. Should be called when the audio track previously passed to {@link * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use. */ public void reset() { From a818049143a24e75d1d395221c45fc90a87f47c1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jun 2020 23:42:26 +0100 Subject: [PATCH 76/80] Fix position jank after pausing and seeking Issue: #6901 PiperOrigin-RevId: 314418536 --- RELEASENOTES.md | 3 ++ .../audio/AudioTimestampPoller.java | 10 ++-- .../audio/AudioTrackPositionTracker.java | 46 ++++++++++++++++--- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5c37ac1245..3323402192 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### 2.11.5 (2020-06-03) ### +* Improve the smoothness of video playback immediately after starting, seeking + or resuming a playback + ([#6901](https://github.com/google/ExoPlayer/issues/6901)). * Add `SilenceMediaSource.Factory` to support tags. * Enable the configuration of `SilenceSkippingAudioProcessor` ([#6705](https://github.com/google/ExoPlayer/issues/6705)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java index 0564591f1f..200c917954 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -37,7 +37,7 @@ import java.lang.annotation.RetentionPolicy; * *

    If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to * get the system time at which the latest timestamp was sampled and {@link - * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()} + * #getTimestampPositionFrames()} to get its position in frames. If {@link #hasAdvancingTimestamp()} * returns {@code true}, the caller should assume that the timestamp has been increasing in real * time since it was sampled. Otherwise, it may be stationary. * @@ -68,7 +68,7 @@ import java.lang.annotation.RetentionPolicy; private static final int STATE_ERROR = 4; /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */ - private static final int FAST_POLL_INTERVAL_US = 5_000; + private static final int FAST_POLL_INTERVAL_US = 10_000; /** * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}. */ @@ -110,7 +110,7 @@ import java.lang.annotation.RetentionPolicy; * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link - * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated. + * #hasTimestamp()} and {@link #hasAdvancingTimestamp()} may be updated. * * @param systemTimeUs The current system time, in microseconds. * @return Whether the timestamp was updated. @@ -200,12 +200,12 @@ import java.lang.annotation.RetentionPolicy; } /** - * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link + * Returns whether this instance has an advancing timestamp. If {@code true}, call {@link * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A * current position for the track can be extrapolated based on elapsed real time since the system * time at which the timestamp was sampled. */ - public boolean isTimestampAdvancing() { + public boolean hasAdvancingTimestamp() { return state == STATE_TIMESTAMP_ADVANCING; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index f227a6f3d8..d944edc197 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -123,6 +123,8 @@ import java.lang.reflect.Method; *

    This is a fail safe that should not be required on correctly functioning devices. */ private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; + /** The duration of time used to smooth over an adjustment between position sampling modes. */ + private static final long MODE_SWITCH_SMOOTHING_DURATION_US = C.MICROS_PER_SECOND; private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; @@ -160,6 +162,15 @@ import java.lang.reflect.Method; private long stopPlaybackHeadPosition; private long endPlaybackHeadPosition; + // Results from the previous call to getCurrentPositionUs. + private long lastPositionUs; + private long lastSystemTimeUs; + private boolean lastSampleUsedGetTimestampMode; + + // Results from the last call to getCurrentPositionUs that used a different sample mode. + private long previousModePositionUs; + private long previousModeSystemTimeUs; + /** * Creates a new audio track position tracker. * @@ -218,18 +229,16 @@ import java.lang.reflect.Method; // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. // Otherwise, derive a smoothed position by sampling the track's frame position. long systemTimeUs = System.nanoTime() / 1000; + long positionUs; AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); - if (audioTimestampPoller.hasTimestamp()) { + boolean useGetTimestampMode = audioTimestampPoller.hasAdvancingTimestamp(); + if (useGetTimestampMode) { // Calculate the speed-adjusted position using the timestamp (which may be in the future). long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); long timestampPositionUs = framesToDurationUs(timestampPositionFrames); - if (!audioTimestampPoller.isTimestampAdvancing()) { - return timestampPositionUs; - } long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); - return timestampPositionUs + elapsedSinceTimestampUs; + positionUs = timestampPositionUs + elapsedSinceTimestampUs; } else { - long positionUs; if (playheadOffsetCount == 0) { // The AudioTrack has started, but we don't have any samples to compute a smoothed position. positionUs = getPlaybackHeadPositionUs(); @@ -242,8 +251,29 @@ import java.lang.reflect.Method; if (!sourceEnded) { positionUs = Math.max(0, positionUs - latencyUs); } - return positionUs; } + + if (lastSampleUsedGetTimestampMode != useGetTimestampMode) { + // We've switched sampling mode. + previousModeSystemTimeUs = lastSystemTimeUs; + previousModePositionUs = lastPositionUs; + } + long elapsedSincePreviousModeUs = systemTimeUs - previousModeSystemTimeUs; + if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) { + // Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden + // jump if the two modes disagree. + long previousModeProjectedPositionUs = previousModePositionUs + elapsedSincePreviousModeUs; + // A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US. + long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US; + positionUs *= rampPoint; + positionUs += (1000 - rampPoint) * previousModeProjectedPositionUs; + positionUs /= 1000; + } + + lastSystemTimeUs = systemTimeUs; + lastPositionUs = positionUs; + lastSampleUsedGetTimestampMode = useGetTimestampMode; + return positionUs; } /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */ @@ -458,6 +488,8 @@ import java.lang.reflect.Method; playheadOffsetCount = 0; nextPlayheadOffsetIndex = 0; lastPlayheadSampleTimeUs = 0; + lastSystemTimeUs = 0; + previousModeSystemTimeUs = 0; } /** From 1347a2200f1d0e07ddd1a6bf01dc575903e6688a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jun 2020 13:15:51 +0100 Subject: [PATCH 77/80] Fix more cases of downloads not being resumed Issue: #7453 PiperOrigin-RevId: 314710328 --- RELEASENOTES.md | 5 +- .../exoplayer2/scheduler/Requirements.java | 25 +++++----- .../scheduler/RequirementsWatcher.java | 48 ++++++++++++++++--- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3323402192..78a7e7233b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes # -### 2.11.5 (2020-06-03) ### +### 2.11.5 (2020-06-04) ### * Improve the smoothness of video playback immediately after starting, seeking or resuming a playback @@ -17,6 +17,9 @@ * Fix issue in `AudioTrackPositionTracker` that could cause negative positions to be reported at the start of playback and immediately after seeking ([#7456](https://github.com/google/ExoPlayer/issues/7456). +* Fix further cases where downloads would sometimes not resume after their + network requirements are met + ([#7453](https://github.com/google/ExoPlayer/issues/7453). * DASH: * Merge trick play adaptation sets (i.e., adaptation sets marked with `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 8919a26720..f4183897eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -129,11 +129,9 @@ public final class Requirements implements Parcelable { } ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo(); - if (networkInfo == null - || !networkInfo.isConnected() - || !isInternetConnectivityValidated(connectivityManager)) { + (ConnectivityManager) + Assertions.checkNotNull(context.getSystemService(Context.CONNECTIVITY_SERVICE)); + if (!isInternetConnectivityValidated(connectivityManager)) { return requirements & (NETWORK | NETWORK_UNMETERED); } @@ -156,23 +154,28 @@ public final class Requirements implements Parcelable { } private boolean isDeviceIdle(Context context) { - PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + PowerManager powerManager = + (PowerManager) Assertions.checkNotNull(context.getSystemService(Context.POWER_SERVICE)); return Util.SDK_INT >= 23 ? powerManager.isDeviceIdleMode() : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); } private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { - // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only - // fires an event to update its Requirements when NetworkCapabilities change from API level 24. - // Since Requirements won't be updated, we assume connectivity is validated on API level 23. + // It's possible to check NetworkCapabilities.NET_CAPABILITY_VALIDATED from API level 23, but + // RequirementsWatcher only fires an event to re-check the requirements when NetworkCapabilities + // change from API level 24. We use the legacy path for API level 23 here to keep in sync. if (Util.SDK_INT < 24) { - return true; + // Legacy path. + @Nullable NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnected(); } - Network activeNetwork = connectivityManager.getActiveNetwork(); + + @Nullable Network activeNetwork = connectivityManager.getActiveNetwork(); if (activeNetwork == null) { return false; } + @Nullable NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork); return networkCapabilities != null diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index f55978c28a..80015cf3a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -150,6 +150,23 @@ public final class RequirementsWatcher { } } + /** + * Re-checks the requirements if there are network requirements that are currently not met. + * + *

    When we receive an event that implies newly established network connectivity, we re-check + * the requirements by calling {@link #checkRequirements()}. This check sometimes sees that there + * is still no active network, meaning that any network requirements will remain not met. By + * calling this method when we receive other events that imply continued network connectivity, we + * can detect that the requirements are met once an active network does exist. + */ + private void recheckNotMetNetworkRequirements() { + if ((notMetRequirements & (Requirements.NETWORK | Requirements.NETWORK_UNMETERED)) == 0) { + // No unmet network requirements to recheck. + return; + } + checkRequirements(); + } + private class DeviceStatusChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -161,17 +178,25 @@ public final class RequirementsWatcher { @RequiresApi(24) private final class NetworkCallback extends ConnectivityManager.NetworkCallback { - boolean receivedCapabilitiesChange; - boolean networkValidated; + + private boolean receivedCapabilitiesChange; + private boolean networkValidated; @Override public void onAvailable(Network network) { - onNetworkCallback(); + postCheckRequirements(); } @Override public void onLost(Network network) { - onNetworkCallback(); + postCheckRequirements(); + } + + @Override + public void onBlockedStatusChanged(Network network, boolean blocked) { + if (!blocked) { + postRecheckNotMetNetworkRequirements(); + } } @Override @@ -181,11 +206,13 @@ public final class RequirementsWatcher { if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) { receivedCapabilitiesChange = true; this.networkValidated = networkValidated; - onNetworkCallback(); + postCheckRequirements(); + } else if (networkValidated) { + postRecheckNotMetNetworkRequirements(); } } - private void onNetworkCallback() { + private void postCheckRequirements() { handler.post( () -> { if (networkCallback != null) { @@ -193,5 +220,14 @@ public final class RequirementsWatcher { } }); } + + private void postRecheckNotMetNetworkRequirements() { + handler.post( + () -> { + if (networkCallback != null) { + recheckNotMetNetworkRequirements(); + } + }); + } } } From 7a8f878a1f6de82470cb28a91eb79f3a50a69920 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 5 Jun 2020 12:15:54 +0100 Subject: [PATCH 78/80] Bump version to 2.11.5 PiperOrigin-RevId: 314903986 --- RELEASENOTES.md | 2 +- constants.gradle | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 78a7e7233b..9055d86943 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes # -### 2.11.5 (2020-06-04) ### +### 2.11.5 (2020-06-05) ### * Improve the smoothness of video playback immediately after starting, seeking or resuming a playback diff --git a/constants.gradle b/constants.gradle index c79130cacb..1d7a0f0ebd 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.11.4' - releaseVersionCode = 2011004 + releaseVersion = '2.11.5' + releaseVersionCode = 2011005 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 06743732e7..15d43c7b79 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.11.4"; + public static final String VERSION = "2.11.5"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.5"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2011004; + public static final int VERSION_INT = 2011005; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From cdb40257581c1a87b0fc6bac57a0bd3d86866605 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 5 Jun 2020 13:14:55 +0100 Subject: [PATCH 79/80] Revert "Update Gradle plugins." This reverts commit c20b85ac60701a34081d8d7488ae434b9792aa9b. --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d520925fb0..a4823b94ee 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.novoda:bintray-release:0.9.1' - classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' + classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0' } } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dc65d6734f..7fefd1c665 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip From 46d29b25c9bec032440d545de1494f18815cc00a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 5 Jun 2020 15:37:49 +0100 Subject: [PATCH 80/80] Reintroduce isConnected check for download requirements PiperOrigin-RevId: 314925639 --- .../android/exoplayer2/scheduler/Requirements.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index f4183897eb..4e2c83d5d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -131,7 +131,10 @@ public final class Requirements implements Parcelable { ConnectivityManager connectivityManager = (ConnectivityManager) Assertions.checkNotNull(context.getSystemService(Context.CONNECTIVITY_SERVICE)); - if (!isInternetConnectivityValidated(connectivityManager)) { + @Nullable NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + if (networkInfo == null + || !networkInfo.isConnected() + || !isInternetConnectivityValidated(connectivityManager)) { return requirements & (NETWORK | NETWORK_UNMETERED); } @@ -164,11 +167,10 @@ public final class Requirements implements Parcelable { private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { // It's possible to check NetworkCapabilities.NET_CAPABILITY_VALIDATED from API level 23, but // RequirementsWatcher only fires an event to re-check the requirements when NetworkCapabilities - // change from API level 24. We use the legacy path for API level 23 here to keep in sync. + // change from API level 24. We assume that network capability is validated for API level 23 to + // keep in sync. if (Util.SDK_INT < 24) { - // Legacy path. - @Nullable NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); - return networkInfo != null && networkInfo.isConnected(); + return true; } @Nullable Network activeNetwork = connectivityManager.getActiveNetwork();