From 6ec53f4717df4cfe1fa85fa21a217fe032572823 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 07:24:10 -0700 Subject: [PATCH 001/417] Add support for 608/708 captions in HLS+fMP4 This also allows exposing multiple CC channels to any fMP4 extractor client. Issue:#1661 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174458725 --- .../mp4/FragmentedMp4ExtractorTest.java | 23 ++++---- .../extractor/mp4/FragmentedMp4Extractor.java | 54 ++++++++++++------- .../source/dash/DefaultDashChunkSource.java | 11 ++-- .../hls/DefaultHlsExtractorFactory.java | 3 +- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index c9364aa605..d24788f74a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -16,9 +16,13 @@ package com.google.android.exoplayer2.extractor.mp4; import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Collections; +import java.util.List; /** * Unit test for {@link FragmentedMp4Extractor}. @@ -26,26 +30,23 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", - getInstrumentation()); + ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.emptyList()), + "mp4/sample_fragmented.mp4", getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorAsserts.assertBehavior( - getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), - "mp4/sample_fragmented_sei.mp4", getInstrumentation()); + ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); + ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4", + getInstrumentation()); } - private static ExtractorFactory getExtractorFactory() { - return getExtractorFactory(0); - } - - private static ExtractorFactory getExtractorFactory(final int flags) { + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return new ExtractorFactory() { @Override public Extractor create() { - return new FragmentedMp4Extractor(flags, null); + return new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } }; } 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 867e4501fa..e86157dd92 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 @@ -46,6 +46,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Stack; @@ -73,8 +74,8 @@ public final class FragmentedMp4Extractor implements Extractor { */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, - FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -93,20 +94,15 @@ public final class FragmentedMp4Extractor implements Extractor { * messages in the stream will be delivered as samples to this track. */ public static final int FLAG_ENABLE_EMSG_TRACK = 4; - /** - * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages - * contained within SEI NAL units in the stream will be delivered as samples to this track. - */ - public static final int FLAG_ENABLE_CEA608_TRACK = 8; /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 16; + private static final int FLAG_SIDELOADED = 8; /** * Flag to ignore any edit lists in the stream. */ - public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32; + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16; private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); @@ -124,7 +120,8 @@ public final class FragmentedMp4Extractor implements Extractor { @Flags private final int flags; private final Track sideloadedTrack; - // Manifest DRM data. + // Sideloaded data. + private final List closedCaptionFormats; private final DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. @@ -193,15 +190,33 @@ public final class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { + this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, + Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + */ + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; this.sideloadedDrmInitData = sideloadedDrmInitData; + this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -483,12 +498,13 @@ public final class FragmentedMp4Extractor implements Extractor { eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { - TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, - C.TRACK_TYPE_TEXT); - cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, - null)); - cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; + if (cea608TrackOutputs == null) { + cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; + for (int i = 0; i < cea608TrackOutputs.length; i++) { + TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); + output.format(closedCaptionFormats.get(i)); + cea608TrackOutputs[i] = output; + } } } @@ -1123,7 +1139,7 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - processSeiNalUnitPayload = cea608TrackOutputs != null + processSeiNalUnitPayload = cea608TrackOutputs.length > 0 && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 1eac1b5616..66455b2f04 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -424,10 +425,12 @@ public class DefaultDashChunkSource implements DashChunkSource { if (enableEventMessageTrack) { flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; } - if (enableCea608Track) { - flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; - } - extractor = new FragmentedMp4Extractor(flags); + // TODO: Use caption format information from the manifest if available. + List closedCaptionFormats = enableCea608Track + ? Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) + : Collections.emptyList(); + extractor = new FragmentedMp4Extractor(flags, null, null, null, closedCaptionFormats); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 9f0989e444..1aa7c47e04 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -74,7 +74,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); + extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { // For any other file extension, we assume TS format. @DefaultTsPayloadReaderFactory.Flags From dbe0e602ef76f8491e8da41635be3fef2e7b7c50 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 3 Nov 2017 07:46:47 -0700 Subject: [PATCH 002/417] Use Timeline.getPeriodPosition to resolve period index in ExoPlayerImpl.seekTo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174460558 --- .../android/exoplayer2/ExoPlayerImpl.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 8ee8af5980..d28f72e739 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -21,6 +21,7 @@ import android.os.Looper; import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; +import android.util.Pair; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -254,19 +255,12 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; maskingPeriodIndex = 0; } else { - timeline.getWindow(windowIndex, window); - long windowPositionUs = positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() - : C.msToUs(positionMs); - int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; - long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); - while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs - && periodIndex < window.lastPeriodIndex) { - periodPositionUs -= periodDurationUs; - periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); - } + long windowPositionUs = positionMs == C.TIME_UNSET + ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs); + Pair periodIndexAndPositon = + timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); maskingWindowPositionMs = C.usToMs(windowPositionUs); - maskingPeriodIndex = periodIndex; + maskingPeriodIndex = periodIndexAndPositon.first; } internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); for (Player.EventListener listener : listeners) { From 4630fa2b4c595aeac5e99d98601bb4acc382dc09 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 08:01:00 -0700 Subject: [PATCH 003/417] Propagate codec information from EXT-X-STREAM-INF to EXT-X-MEDIA This is the first CL in a series to add chunkless preparation support. Also did a bit a tidying up in HlsSampleStreamWrappen and HlsMasterPlaylistParserTest. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174461737 --- .../google/android/exoplayer2/util/Util.java | 26 +++++ .../android/exoplayer2/util/UtilTest.java | 13 +++ .../playlist/HlsMasterPlaylistParserTest.java | 42 +++++-- .../source/hls/HlsSampleStreamWrapper.java | 33 +----- .../hls/playlist/HlsPlaylistParser.java | 103 +++++++++++------- 5 files changed, 136 insertions(+), 81 deletions(-) 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 24132e400c..6c61c221fd 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 @@ -749,6 +749,32 @@ public final class Util { + ") " + ExoPlayerLibraryInfo.VERSION_SLASHY; } + /** + * Returns a copy of {@code codecs} without the codecs whose track type doesn't match + * {@code trackType}. + * + * @param codecs A codec sequence string, as defined in RFC 6381. + * @param trackType One of {@link C}{@code .TRACK_TYPE_*}. + * @return A copy of {@code codecs} without the codecs whose track type doesn't match + * {@code trackType}. + */ + public static String getCodecsOfType(String codecs, int trackType) { + if (TextUtils.isEmpty(codecs)) { + return null; + } + String[] codecArray = codecs.trim().split("(\\s*,\\s*)"); + StringBuilder builder = new StringBuilder(); + for (String codec : codecArray) { + if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { + if (builder.length() > 0) { + builder.append(","); + } + builder.append(codec); + } + } + return builder.length() > 0 ? builder.toString() : null; + } + /** * Converts a sample bit depth to a corresponding PCM encoding constant. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 1afe380483..68ed686c62 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import static com.google.android.exoplayer2.util.Util.binarySearchCeil; import static com.google.android.exoplayer2.util.Util.binarySearchFloor; import static com.google.android.exoplayer2.util.Util.escapeFileName; +import static com.google.android.exoplayer2.util.Util.getCodecsOfType; import static com.google.android.exoplayer2.util.Util.parseXsDateTime; import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.unescapeFileName; @@ -181,6 +182,18 @@ public class UtilTest { assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); } + @Test + public void testGetCodecsOfType() { + assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull(); + assertThat(getCodecsOfType("avc1.64001e,vp9.63.1", C.TRACK_TYPE_AUDIO)).isNull(); + assertThat(getCodecsOfType(" vp9.63.1, ec-3 ", C.TRACK_TYPE_AUDIO)).isEqualTo("ec-3"); + assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) + .isEqualTo("avc1.61e,vp9.63.1"); + assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) + .isEqualTo("avc1.61e,vp9.63.1"); + assertThat(getCodecsOfType("invalidCodec1, invalidCodec2 ", C.TRACK_TYPE_AUDIO)).isNull(); + } + @Test public void testUnescapeInvalidFileName() { assertThat(Util.unescapeFileName("%a")).isNull(); diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 8b0d76d2e5..45c9d15242 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -34,7 +34,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { private static final String PLAYLIST_URI = "https://example.com/test.m3u8"; - private static final String MASTER_PLAYLIST = " #EXTM3U \n" + private static final String PLAYLIST_SIMPLE = " #EXTM3U \n" + "\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n" @@ -51,7 +51,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "http://example.com/audio-only.m3u8"; - private static final String AVG_BANDWIDTH_MASTER_PLAYLIST = " #EXTM3U \n" + private static final String PLAYLIST_WITH_AVG_BANDWIDTH = " #EXTM3U \n" + "\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n" @@ -64,19 +64,33 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; - private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" + private static final String PLAYLIST_WITH_CC = " #EXTM3U \n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; - private static final String MASTER_PLAYLIST_WITHOUT_CC = " #EXTM3U \n" + private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128," + "CLOSED-CAPTIONS=NONE\n" + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG = "#EXTM3U\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n" + + "uri1.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n" + + "uri2.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n" + + "uri1.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n" + + "uri2.m3u8\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\"," + + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\"," + + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n"; + public void testParseMasterPlaylist() throws IOException{ - HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); + HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); List variants = masterPlaylist.variants; assertEquals(5, variants.size()); @@ -116,7 +130,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { public void testMasterPlaylistWithBandwdithAverage() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, - AVG_BANDWIDTH_MASTER_PLAYLIST); + PLAYLIST_WITH_AVG_BANDWIDTH); List variants = masterPlaylist.variants; @@ -134,7 +148,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { } public void testPlaylistWithClosedCaption() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC); + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); assertEquals(1, playlist.muxedCaptionFormats.size()); Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType); @@ -143,10 +157,22 @@ public class HlsMasterPlaylistParserTest extends TestCase { } public void testPlaylistWithoutClosedCaptions() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITHOUT_CC); + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); } + public void testCodecPropagation() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); + + Format firstAudioFormat = playlist.audios.get(0).format; + assertEquals("mp4a.40.2", firstAudioFormat.codecs); + assertEquals(MimeTypes.AUDIO_AAC, firstAudioFormat.sampleMimeType); + + Format secondAudioFormat = playlist.audios.get(1).format; + assertEquals("ac-3", secondAudioFormat.codecs); + assertEquals(MimeTypes.AUDIO_AC3, secondAudioFormat.sampleMimeType); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); 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 3eae83624b..9816e4041c 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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -748,13 +747,8 @@ import java.util.LinkedList; if (containerFormat == null) { return sampleFormat; } - String codecs = null; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - if (sampleTrackType == C.TRACK_TYPE_AUDIO) { - codecs = getAudioCodecs(containerFormat.codecs); - } else if (sampleTrackType == C.TRACK_TYPE_VIDEO) { - codecs = getVideoCodecs(containerFormat.codecs); - } + String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, containerFormat.width, containerFormat.height, containerFormat.selectionFlags, containerFormat.language); @@ -793,29 +787,4 @@ import java.util.LinkedList; return true; } - private static String getAudioCodecs(String codecs) { - return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO); - } - - private static String getVideoCodecs(String codecs) { - return getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO); - } - - private static String getCodecsOfType(String codecs, int trackType) { - if (TextUtils.isEmpty(codecs)) { - return null; - } - String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); - StringBuilder builder = new StringBuilder(); - for (String codec : codecArray) { - if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { - if (builder.length() > 0) { - builder.append(","); - } - builder.append(codec); - } - } - return builder.length() > 0 ? builder.toString() : null; - } - } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index c63ded6275..6536be3ffe 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -35,6 +35,7 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Queue; @@ -88,6 +89,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantUrls = new HashSet<>(); + HashMap audioGroupIdToCodecs = new HashMap<>(); ArrayList variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); + ArrayList mediaTags = new ArrayList<>(); ArrayList tags = new ArrayList<>(); Format muxedAudioFormat = null; List muxedCaptionFormats = null; @@ -208,47 +213,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser(); - } - muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, - Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); - break; - default: - // Do nothing. - break; - } + // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF + // tags. + mediaTags.add(line); } else if (line.startsWith(TAG_STREAM_INF)) { noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); @@ -279,6 +246,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser(); + } + muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, + Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); + break; + default: + // Do nothing. + break; + } + } + if (noClosedCaptions) { muxedCaptionFormats = Collections.emptyList(); } From b5b87d6a27bbc5286024eec5d840cf7c6e512520 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 Nov 2017 09:00:45 -0700 Subject: [PATCH 004/417] Bump to 2.6.0 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174467964 --- RELEASENOTES.md | 50 +++++++++++++++++++ constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 +- demos/ima/src/main/AndroidManifest.xml | 4 +- demos/main/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +-- 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90b3d15e08..42bf526de0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,55 @@ # Release notes # +### r2.6.0 ### + +* New `Player.DefaultEventListener` abstract class can be extended to avoid + having to implement all methods defined by `Player.EventListener`. +* Added a reason to `EventListener.onPositionDiscontinuity` + ([#3252](https://github.com/google/ExoPlayer/issues/3252)). +* New `setShuffleModeEnabled` method for enabling shuffled playback. +* Support for `Renderer`s that don't consume any media + ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` + ([#3362](https://github.com/google/ExoPlayer/issues/3362)). +* Fix playbacks involving looping, concatenation and ads getting stuck when + media contains tracks with uneven durations + ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Better playback experience when the video decoder cannot keep up, by skipping + to key-frames. This is particularly relevant for variable speed playbacks. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Audio: New `AudioSink` interface allows customization of audio output path. +* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming + and progressive streams. +* Track selection: + * Fixed adaptive track selection logic for live playbacks + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). + * Added ability to select the lowest bitrate tracks. +* HLS: + * Support for Widevine protected FMP4 variants. +* DRM: + * Improved compatibility with ClearKey content + ([#3138](https://github.com/google/ExoPlayer/issues/3138)). + * Support multiple PSSH boxes of the same type. + * Retry initial provisioning and key requests if they fail + * Fix incorrect parsing of non-CENC sinf boxes. +* IMA extension: + * Expose `AdsLoader` via getter + ([#3322](https://github.com/google/ExoPlayer/issues/3322)). + * Handle `setPlayWhenReady` calls during ad playbacks + ([#3303](https://github.com/google/ExoPlayer/issues/3303)). + * Ignore seeks if an ad is playing + ([#3309](https://github.com/google/ExoPlayer/issues/3309)). +* UI: + * Allow specifying a `Drawable` for the `TimeBar` scrubber + ([#3337](https://github.com/google/ExoPlayer/issues/3337)). + * Allow multiple listeners on `TimeBar` + ([#3406](https://github.com/google/ExoPlayer/issues/3406)). +* New Leanback extension: Simplifies binding Exoplayer to Leanback UI + components. +* New Cast extension: Simplifies toggling between local and Cast playbacks. +* Unit tests moved to Robolectric. +* Misc bugfixes. + ### r2.5.4 ### * Remove unnecessary media playlist fetches during playback of live HLS streams. diff --git a/constants.gradle b/constants.gradle index c2209cbbe6..644d47b8aa 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.5.4' + releaseVersion = 'r2.6.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index cd2b51513c..11f8e39b53 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2600" + android:versionName="2.6.0"> diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5c6db02417..5252d2feeb 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2600" + android:versionName="2.6.0"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index f70d6152e8..d041e24d80 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2600" + android:versionName="2.6.0"> 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 62ee8c4873..f13a7de0ca 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 @@ -31,13 +31,13 @@ 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.5.4"; + public static final String VERSION = "2.6.0"; /** * 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.5.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,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 = 2005004; + public static final int VERSION_INT = 2006000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 1cfea625456e4ef66396d6fc94bbd00ec3f2302c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 3 Nov 2017 16:23:21 +0000 Subject: [PATCH 005/417] Remove DownloadManager test --- .../offline/DownloadManagerTest.java | 432 ------------------ 1 file changed, 432 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java deleted file mode 100644 index 791b4a9d35..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (C) 2017 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.offline; - -import android.os.ConditionVariable; -import android.support.annotation.Nullable; -import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask.State; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -/** Tests {@link DownloadManager}. */ -@ClosedSource(reason = "Not ready yet") -public class DownloadManagerTest extends InstrumentationTestCase { - - /* Used to check if condition becomes true in this time interval. */ - private static final int ASSERT_TRUE_TIMEOUT = 1000; - /* Used to check if condition stays false for this time interval. */ - private static final int ASSERT_FALSE_TIME = 1000; - - private DownloadManager downloadManager; - private File actionFile; - private TestDownloadListener testDownloadListener; - - @Override - public void setUp() throws Exception { - super.setUp(); - setUpMockito(this); - - actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); - downloadManager = new DownloadManager( - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY), - 100, actionFile.getAbsolutePath()); - - testDownloadListener = new TestDownloadListener(); - downloadManager.addListener(testDownloadListener); - } - - @Override - public void tearDown() throws Exception { - downloadManager.release(); - actionFile.delete(); - super.tearDown(); - } - - public void testDownloadActionRuns() throws Throwable { - doTestActionRuns(createDownloadAction("media 1")); - } - - public void testRemoveActionRuns() throws Throwable { - doTestActionRuns(createRemoveAction("media 1")); - } - - public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createDownloadAction("media 2")); - } - - public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createRemoveAction("media 2")); - } - - public void testSameMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createDownloadAction("media 1")); - } - - public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { - doTestActionsRunSequentially(createDownloadAction("media 1"), - createRemoveAction("media 1")); - } - - public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), - createDownloadAction("media 1")); - } - - public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), - createRemoveAction("media 1")); - } - - public void testSameMediaMultipleActions() throws Throwable { - FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction("media 1"); - FakeDownloadAction downloadAction3 = createDownloadAction("media 1"); - FakeDownloadAction removeAction2 = createRemoveAction("media 1"); - - // Two download actions run in parallel. - downloadAction1.post().assertStarted(); - downloadAction2.post().assertStarted(); - // removeAction1 is added. It interrupts the two download actions' threads but they are - // configured to ignore it so removeAction1 doesn't start. - removeAction1.post().assertDoesNotStart(); - - // downloadAction2 finishes but it isn't enough to start removeAction1. - downloadAction2.finish().assertCancelled(); - removeAction1.assertDoesNotStart(); - // downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. - downloadAction3.post().assertDoesNotStart(); - - // When downloadAction1 finishes, removeAction1 starts. - downloadAction1.finish().assertCancelled(); - removeAction1.assertStarted(); - // downloadAction3 still waits removeAction1 - downloadAction3.assertDoesNotStart(); - - // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 - // starts immediately. - removeAction2.post().assertStarted().finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { - FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction2 = createRemoveAction("media 1"); - FakeDownloadAction removeAction3 = createRemoveAction("media 1"); - - removeAction1.post().assertStarted(); - removeAction2.post().assertDoesNotStart(); - removeAction3.post().assertDoesNotStart(); - - removeAction2.assertCancelled(); - - removeAction1.finish().assertCancelled(); - removeAction3.assertStarted().finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1"); - - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); - - removeAction.finish().assertEnded(); - downloadAction1.assertStarted(); - downloadAction2.assertStarted(); - downloadAction1.finish().assertEnded(); - downloadAction2.finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 2"); - - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); - - removeAction.finish().assertEnded(); - downloadAction1.assertStarted(); - downloadAction2.assertStarted(); - downloadAction1.finish().assertEnded(); - downloadAction2.finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction("media 1"); - FakeDownloadAction removeAction2 = createRemoveAction("media 2"); - - downloadAction.post().assertStarted(); - removeAction1.post().assertDoesNotStart(); - removeAction2.post().assertStarted(); - - downloadAction.finish().assertCancelled(); - removeAction2.finish().assertEnded(); - - removeAction1.assertStarted(); - removeAction1.finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private void doTestActionRuns(FakeDownloadAction action) throws Throwable { - action.post().assertStarted().finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private void doTestActionsRunSequentially(FakeDownloadAction action1, - FakeDownloadAction action2) throws Throwable { - action1.ignoreInterrupts().post().assertStarted(); - action2.post().assertDoesNotStart(); - - action1.finish(); - action2.assertStarted(); - - action2.finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private void doTestActionsRunInParallel(FakeDownloadAction action1, - FakeDownloadAction action2) throws Throwable { - action1.post().assertStarted(); - action2.post().assertStarted(); - action1.finish().assertEnded(); - action2.finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private FakeDownloadAction createDownloadAction(String mediaId) { - return new FakeDownloadAction(downloadManager, mediaId, false); - } - - private FakeDownloadAction createRemoveAction(String mediaId) { - return new FakeDownloadAction(downloadManager, mediaId, true); - } - - private static class TestDownloadListener implements DownloadListener { - - private ConditionVariable downloadFinishedCondition; - private Throwable downloadError; - - private TestDownloadListener() { - downloadFinishedCondition = new ConditionVariable(); - } - - @Override - public void onStateChange(DownloadManager downloadManager, DownloadTask downloadTask, int state, - Throwable error) { - if (state == DownloadTask.STATE_ERROR && downloadError == null) { - downloadError = error; - } - ((FakeDownloadAction) downloadTask.getDownloadAction()).onStateChange(); - } - - @Override - public void onTasksFinished(DownloadManager downloadManager) { - downloadFinishedCondition.open(); - } - - private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { - assertTrue(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT)); - downloadFinishedCondition.close(); - if (downloadError != null) { - throw downloadError; - } - } - - } - - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - - private static class FakeDownloadAction extends DownloadAction { - - private final DownloadManager downloadManager; - private final String mediaId; - private final boolean removeAction; - private final FakeDownloader downloader; - private final ConditionVariable stateChanged; - private DownloadTask downloadTask; - - private FakeDownloadAction(DownloadManager downloadManager, String mediaId, - boolean removeAction) { - this.downloadManager = downloadManager; - this.mediaId = mediaId; - this.removeAction = removeAction; - this.downloader = new FakeDownloader(removeAction); - this.stateChanged = new ConditionVariable(); - } - - @Override - protected String getType() { - return "FakeDownloadAction"; - } - - @Override - protected void writeToStream(DataOutputStream output) throws IOException { - // do nothing. - } - - @Override - protected boolean isRemoveAction() { - return removeAction; - } - - @Override - protected boolean isSameMedia(DownloadAction other) { - return other instanceof FakeDownloadAction - && mediaId.equals(((FakeDownloadAction) other).mediaId); - } - - @Override - protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { - return downloader; - } - - private FakeDownloadAction post() { - downloadTask = downloadManager.handleAction(this); - return this; - } - - private FakeDownloadAction assertDoesNotStart() { - assertFalse(downloader.started.block(ASSERT_FALSE_TIME)); - return this; - } - - private FakeDownloadAction assertStarted() { - assertTrue(downloader.started.block(ASSERT_TRUE_TIMEOUT)); - assertState(DownloadTask.STATE_STARTED); - return this; - } - - private FakeDownloadAction assertEnded() { - return assertState(DownloadTask.STATE_ENDED); - } - - private FakeDownloadAction assertCancelled() { - return assertState(DownloadTask.STATE_CANCELED); - } - - private FakeDownloadAction assertState(@State int state) { - assertTrue(stateChanged.block(ASSERT_TRUE_TIMEOUT)); - stateChanged.close(); - assertEquals(state, downloadTask.getState()); - return this; - } - - private FakeDownloadAction finish() { - downloader.finish.open(); - return this; - } - - private FakeDownloadAction ignoreInterrupts() { - downloader.ignoreInterrupts = true; - return this; - } - - private void onStateChange() { - stateChanged.open(); - } - } - - private static class FakeDownloader implements Downloader { - private final ConditionVariable started; - private final com.google.android.exoplayer2.util.ConditionVariable finish; - private final boolean removeAction; - private boolean ignoreInterrupts; - - private FakeDownloader(boolean removeAction) { - this.removeAction = removeAction; - this.started = new ConditionVariable(); - this.finish = new com.google.android.exoplayer2.util.ConditionVariable(); - } - - @Override - public void init() throws InterruptedException, IOException { - // do nothing. - } - - @Override - public void download(@Nullable ProgressListener listener) - throws InterruptedException, IOException { - assertFalse(removeAction); - started.open(); - blockUntilFinish(); - } - - @Override - public void remove() throws InterruptedException { - assertTrue(removeAction); - started.open(); - blockUntilFinish(); - } - - private void blockUntilFinish() throws InterruptedException { - while (true){ - try { - finish.block(); - break; - } catch (InterruptedException e) { - if (!ignoreInterrupts) { - throw e; - } - } - } - } - - @Override - public long getDownloadedBytes() { - return 0; - } - - @Override - public float getDownloadPercentage() { - return 0; - } - } - -} From ecaaed96748d6ea70bbca2a9f8566df995dbbfac Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 09:36:43 -0700 Subject: [PATCH 006/417] Relax string comparison in DASH parseContentProtection ... by making it case insensitive and null-tolerant for schemeId (as was before adding playlist drm data merging). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174472123 --- .../dash/manifest/DashManifestParser.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) 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 0c35ef0d10..72df69f7e9 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 @@ -345,30 +345,32 @@ public class DashManifestParser extends DefaultHandler */ protected Pair parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { - String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); String schemeType = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; - switch (schemeIdUri) { - case "urn:mpeg:dash:mp4protection:2011": - schemeType = xpp.getAttributeValue(null, "value"); - String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); - if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { - UUID keyId = UUID.fromString(defaultKid); - data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); - uuid = C.COMMON_PSSH_UUID; - } - break; - case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": - uuid = C.PLAYREADY_UUID; - break; - case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": - uuid = C.WIDEVINE_UUID; - break; - default: - break; + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + if (schemeIdUri != null) { + switch (schemeIdUri.toLowerCase()) { + case "urn:mpeg:dash:mp4protection:2011": + schemeType = xpp.getAttributeValue(null, "value"); + String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); + if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + UUID keyId = UUID.fromString(defaultKid); + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + uuid = C.COMMON_PSSH_UUID; + } + break; + case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": + uuid = C.PLAYREADY_UUID; + break; + case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": + uuid = C.WIDEVINE_UUID; + break; + default: + break; + } } do { From 54a2a69b05d7119c82ec89c8c20c05a0e25fade9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 10:01:15 -0700 Subject: [PATCH 007/417] Allow playback to continue even after SingleSampleMediaPeriod load errors This prevents users from having to check sideloaded subtitles URLs before preparing a SingleSampleMediaSource with it. Issue:#3140 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174475274 --- .../source/SingleSampleMediaPeriod.java | 40 ++++++++++++------- .../source/SingleSampleMediaSource.java | 37 +++++++++++++++-- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index b19f398d86..6101c79b7f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -52,16 +51,20 @@ import java.util.Arrays; private final int eventSourceId; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; + /* package */ final boolean treatLoadErrorsAsEndOfStream; /* package */ boolean loadingFinished; + /* package */ boolean loadingSucceeded; /* package */ byte[] sampleData; /* package */ int sampleSize; + private int errorCount; public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId) { + int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.format = format; @@ -69,6 +72,7 @@ import java.util.Arrays; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); loader = new Loader("Loader:SingleSampleMediaPeriod"); @@ -85,7 +89,7 @@ import java.util.Arrays; @Override public void maybeThrowPrepareError() throws IOException { - loader.maybeThrowError(); + // Do nothing. } @Override @@ -157,6 +161,7 @@ import java.util.Arrays; sampleSize = loadable.sampleSize; sampleData = loadable.sampleData; loadingFinished = true; + loadingSucceeded = true; } @Override @@ -169,6 +174,11 @@ import java.util.Arrays; public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { notifyLoadError(error); + errorCount++; + if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) { + loadingFinished = true; + return Loader.DONT_RETRY; + } return Loader.RETRY; } @@ -206,7 +216,9 @@ import java.util.Arrays; @Override public void maybeThrowError() throws IOException { - loader.maybeThrowError(); + if (!treatLoadErrorsAsEndOfStream) { + loader.maybeThrowError(); + } } @Override @@ -219,19 +231,19 @@ import java.util.Arrays; formatHolder.format = format; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; - } - - Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); - if (!loadingFinished) { - return C.RESULT_NOTHING_READ; - } else { - buffer.timeUs = 0; - buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); - buffer.ensureSpaceForWrite(sampleSize); - buffer.data.put(sampleData, 0, sampleSize); + } else if (loadingFinished) { + if (loadingSucceeded) { + buffer.timeUs = 0; + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + buffer.ensureSpaceForWrite(sampleSize); + buffer.data.put(sampleData, 0, sampleSize); + } else { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + } streamState = STREAM_STATE_END_OF_STREAM; return C.RESULT_BUFFER_READ; } + return C.RESULT_NOTHING_READ; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 5b190078fd..dd901958fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -57,21 +57,51 @@ public final class SingleSampleMediaSource implements MediaSource { private final Handler eventHandler; private final EventListener eventListener; private final int eventSourceId; + private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + */ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + */ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0); + this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); } + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated normally + * by {@link SampleStream#maybeThrowError()}. + */ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId) { + int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.format = format; @@ -79,6 +109,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; timeline = new SinglePeriodTimeline(durationUs, true); } @@ -98,7 +129,7 @@ public final class SingleSampleMediaSource implements MediaSource { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId); + eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); } @Override From 475ea19ae7a859dcc2f1e20f0ce83fe44ba459dc Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 03:37:16 -0800 Subject: [PATCH 008/417] Simplify ContentDataSourceTest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174685374 --- .../upstream/ContentDataSourceTest.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 2b70c83ca5..6a85483dd1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,7 +23,6 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; @@ -38,9 +37,6 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - private static final int TEST_DATA_OFFSET = 1; - private static final int TEST_DATA_LENGTH = 1023; - public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() @@ -77,15 +73,11 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { .authority(AUTHORITY) .path(DATA_PATH).build(); try { - DataSpec dataSpec = new DataSpec(contentUri, TEST_DATA_OFFSET, C.LENGTH_UNSET, null); - long length = dataSource.open(dataSpec); - assertEquals(TEST_DATA_LENGTH, length); - byte[] expectedData = Arrays.copyOfRange( - TestUtil.getByteArray(getInstrumentation(), DATA_PATH), TEST_DATA_OFFSET, - TEST_DATA_OFFSET + TEST_DATA_LENGTH); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(expectedData, readData); - assertEquals(C.RESULT_END_OF_INPUT, dataSource.read(new byte[1], 0, 1)); + int testOffset = 1; + DataSpec dataSpec = new DataSpec(contentUri, testOffset, C.LENGTH_UNSET, null); + byte[] completeData = TestUtil.getByteArray(getInstrumentation(), DATA_PATH); + byte[] expectedData = Arrays.copyOfRange(completeData, testOffset, completeData.length); + TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData); } finally { dataSource.close(); } From decf437f17d10f53f58a2a0e8f243f0b5b1c2650 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 04:02:06 -0800 Subject: [PATCH 009/417] Broaden Samsung workaround to API level 25 + J7 Issue: #3257 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174686747 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index d965b662be..ef7d691c5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1141,8 +1141,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @return The mode specifying when the adaptation workaround should be enabled. */ private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { - if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name) - && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A520"))) { + if (Util.SDK_INT <= 25 && "OMX.Exynos.avc.dec.secure".equals(name) + && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A510") + || Util.MODEL.startsWith("SM-A520") || Util.MODEL.startsWith("SM-J700"))) { return ADAPTATION_WORKAROUND_MODE_ALWAYS; } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) From 2daa098a209bdf52fea41dbe7bc311d39e034551 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 07:03:09 -0800 Subject: [PATCH 010/417] Don't use InputStream.available in ContentDataSource Issue: #3426 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174700804 --- .../upstream/AssetDataSourceTest.java | 4 +- .../upstream/ContentDataSourceTest.java | 110 +++++++++++++----- .../upstream/ContentDataSource.java | 17 ++- .../android/exoplayer2/testutil/TestUtil.java | 19 ++- 4 files changed, 104 insertions(+), 46 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java index 102c89ec2b..d582d25ab1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -30,14 +30,14 @@ public final class AssetDataSourceTest extends InstrumentationTestCase { AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH)); TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), true); } public void testReadAssetUri() throws Exception { AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH)); TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), true); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 6a85483dd1..e19f7ad033 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -15,17 +15,22 @@ */ package com.google.android.exoplayer2.upstream; +import android.app.Instrumentation; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; @@ -37,23 +42,33 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - public void testReadValidUri() throws Exception { - ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); - Uri contentUri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY) - .path(DATA_PATH).build(); - DataSpec dataSpec = new DataSpec(contentUri); - TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); + public void testRead() throws Exception { + assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); + } + + public void testReadPipeMode() throws Exception { + assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); + } + + public void testReadFixedLength() throws Exception { + assertData(getInstrumentation(), 0, 100, false); + } + + public void testReadFromOffsetToEndOfInput() throws Exception { + assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); + } + + public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { + assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); + } + + public void testReadFromOffsetFixedLength() throws Exception { + assertData(getInstrumentation(), 1, 100, false); } public void testReadInvalidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); - Uri contentUri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY) - .build(); + Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); DataSpec dataSpec = new DataSpec(contentUri); try { dataSource.open(dataSpec); @@ -66,18 +81,16 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } - public void testReadFromOffsetToEndOfInput() throws Exception { - ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); - Uri contentUri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY) - .path(DATA_PATH).build(); + private static void assertData(Instrumentation instrumentation, int offset, int length, + boolean pipeMode) throws IOException { + Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); + ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); try { - int testOffset = 1; - DataSpec dataSpec = new DataSpec(contentUri, testOffset, C.LENGTH_UNSET, null); - byte[] completeData = TestUtil.getByteArray(getInstrumentation(), DATA_PATH); - byte[] expectedData = Arrays.copyOfRange(completeData, testOffset, completeData.length); - TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData); + DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); + byte[] completeData = TestUtil.getByteArray(instrumentation, DATA_PATH); + byte[] expectedData = Arrays.copyOfRange(completeData, offset, + length == C.LENGTH_UNSET ? completeData.length : offset + length); + TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); } finally { dataSource.close(); } @@ -86,7 +99,21 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { /** * A {@link ContentProvider} for the test. */ - public static final class TestContentProvider extends ContentProvider { + public static final class TestContentProvider extends ContentProvider + implements ContentProvider.PipeDataWriter { + + private static final String PARAM_PIPE_MODE = "pipe-mode"; + + public static Uri buildUri(String filePath, boolean pipeMode) { + Uri.Builder builder = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .path(filePath); + if (pipeMode) { + builder.appendQueryParameter(TestContentProvider.PARAM_PIPE_MODE, "1"); + } + return builder.build(); + } @Override public boolean onCreate() { @@ -106,7 +133,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { return null; } try { - return getContext().getAssets().openFd(uri.getPath().replaceFirst("/", "")); + String fileName = getFileName(uri); + boolean pipeMode = uri.getQueryParameter(PARAM_PIPE_MODE) != null; + if (pipeMode) { + ParcelFileDescriptor fileDescriptor = openPipeHelper(uri, null, null, null, this); + return new AssetFileDescriptor(fileDescriptor, 0, C.LENGTH_UNSET); + } else { + return getContext().getAssets().openFd(fileName); + } } catch (IOException e) { FileNotFoundException exception = new FileNotFoundException(e.getMessage()); exception.initCause(e); @@ -125,15 +159,31 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } @Override - public int delete(@NonNull Uri uri, String selection, + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override - public int update(@NonNull Uri uri, ContentValues values, - String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); + public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, + @NonNull String mimeType, @Nullable Bundle opts, @Nullable Object args) { + try { + byte[] data = TestUtil.getByteArray(getContext(), getFileName(uri)); + FileOutputStream outputStream = new FileOutputStream(output.getFileDescriptor()); + outputStream.write(data); + outputStream.close(); + } catch (IOException e) { + throw new RuntimeException("Error writing to pipe", e); + } + } + + private static String getFileName(Uri uri) { + return uri.getPath().replaceFirst("/", ""); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index c37599eccc..87642e0eba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -24,7 +24,7 @@ import java.io.EOFException; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; +import java.nio.channels.FileChannel; /** * A {@link DataSource} for reading from a content URI. @@ -47,7 +47,7 @@ public final class ContentDataSource implements DataSource { private Uri uri; private AssetFileDescriptor assetFileDescriptor; - private InputStream inputStream; + private FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -88,14 +88,11 @@ public final class ContentDataSource implements DataSource { } else { long assetFileDescriptorLength = assetFileDescriptor.getLength(); if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) { - // The asset must extend to the end of the file. - bytesRemaining = inputStream.available(); - if (bytesRemaining == 0) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, - // or if it's greater than Integer.MAX_VALUE. We don't know the true length in either - // case, so treat as unbounded. - bytesRemaining = C.LENGTH_UNSET; - } + // The asset must extend to the end of the file. If FileInputStream.getChannel().size() + // returns 0 then the remaining length cannot be determined. + FileChannel channel = inputStream.getChannel(); + long channelSize = channel.size(); + bytesRemaining = channelSize == 0 ? C.LENGTH_UNSET : channelSize - channel.position(); } else { bytesRemaining = assetFileDescriptorLength - skipped; } 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 b5b084fc7b..61d1ecaeea 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; +import android.content.Context; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; @@ -121,12 +122,20 @@ public class TestUtil { public static byte[] getByteArray(Instrumentation instrumentation, String fileName) throws IOException { - return Util.toByteArray(getInputStream(instrumentation, fileName)); + return getByteArray(instrumentation.getContext(), fileName); + } + + public static byte[] getByteArray(Context context, String fileName) throws IOException { + return Util.toByteArray(getInputStream(context, fileName)); } public static InputStream getInputStream(Instrumentation instrumentation, String fileName) throws IOException { - return instrumentation.getContext().getResources().getAssets().open(fileName); + return getInputStream(instrumentation.getContext(), fileName); + } + + public static InputStream getInputStream(Context context, String fileName) throws IOException { + return context.getResources().getAssets().open(fileName); } public static String getString(Instrumentation instrumentation, String fileName) @@ -167,13 +176,15 @@ public class TestUtil { * @param dataSource The {@link DataSource} through which to read. * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}. * @param expectedData The expected data. + * @param expectKnownLength Whether to assert that {@link DataSource#open} returns the expected + * data length. If false then it's asserted that {@link C#LENGTH_UNSET} is returned. * @throws IOException If an error occurs reading fom the {@link DataSource}. */ public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec, - byte[] expectedData) throws IOException { + byte[] expectedData, boolean expectKnownLength) throws IOException { try { long length = dataSource.open(dataSpec); - Assert.assertEquals(expectedData.length, length); + Assert.assertEquals(expectKnownLength ? expectedData.length : C.LENGTH_UNSET, length); byte[] readData = TestUtil.readToEnd(dataSource); MoreAsserts.assertEquals(expectedData, readData); } finally { From ea80c3e456a1f7596f13abd8e59e32ce05ab620a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 09:21:01 -0800 Subject: [PATCH 011/417] Relax parsing of ctts sample deltas Issue: #3384 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174715851 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 1c4ca995f6..588282bc9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -247,7 +247,13 @@ import java.util.List; remainingSamplesAtTimestampDelta--; if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) { remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); - timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); + // The BMFF spec (ISO 14496-12) states that sample deltas should be unsigned integers + // in stts boxes, however some streams violate the spec and use signed integers instead. + // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample + // deltas as signed integers here, because unsigned integers will still be parsed + // correctly (unless their top bit is set, which is never true in practice because sample + // deltas are always small). + timestampDeltaInTimeUnits = stts.readInt(); remainingTimestampDeltaChanges--; } From f014b67d3e48627dd2cf69f2620349ea39f620de Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 7 Nov 2017 02:51:43 -0800 Subject: [PATCH 012/417] Use helper method to disable Renderers. Removes duplicated code and starts cleaning up handling of media clocks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174829840 --- .../exoplayer2/ExoPlayerImplInternal.java | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) 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 b79f5c70a3..f00a5ce02d 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 @@ -718,11 +718,9 @@ import java.io.IOException; if (playingPeriodHolder != newPlayingPeriodHolder || playingPeriodHolder != readingPeriodHolder) { for (Renderer renderer : enabledRenderers) { - renderer.disable(); + disableRenderer(renderer); } enabledRenderers = new Renderer[0]; - rendererMediaClock = null; - rendererMediaClockSource = null; playingPeriodHolder = null; } @@ -801,13 +799,10 @@ import java.io.IOException; handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; standaloneMediaClock.stop(); - rendererMediaClock = null; - rendererMediaClockSource = null; rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } catch (ExoPlaybackException | RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Stop failed.", e); @@ -853,6 +848,15 @@ import java.io.IOException; } } + private void disableRenderer(Renderer renderer) throws ExoPlaybackException { + if (renderer == rendererMediaClockSource) { + rendererMediaClock = null; + rendererMediaClockSource = null; + } + ensureStopped(renderer); + renderer.disable(); + } + private void reselectTracksInternal() throws ExoPlaybackException { if (playingPeriodHolder == null) { // We don't have tracks yet, so we don't care. @@ -905,12 +909,7 @@ import java.io.IOException; if (rendererWasEnabledFlags[i]) { if (sampleStream != renderer.getStream()) { // We need to disable the renderer. - if (renderer == rendererMediaClockSource) { - rendererMediaClock = null; - rendererMediaClockSource = null; - } - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } else if (streamResetFlags[i]) { // The renderer will continue to consume from its current stream, but needs to be reset. renderer.resetPosition(rendererPositionUs); @@ -1453,12 +1452,7 @@ import java.io.IOException; // The renderer should be disabled before playing the next period, either because it's not // needed to play the next period, or because we need to re-enable it as its current stream // is final and it's not reading ahead. - if (renderer == rendererMediaClockSource) { - rendererMediaClock = null; - rendererMediaClockSource = null; - } - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } } From 7f135f2cda7c4108bcb105a7dfad12e1c13a307d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Nov 2017 04:30:48 -0800 Subject: [PATCH 013/417] Be more robust against load callback failures Issue: #2795 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174836960 --- .../google/android/exoplayer2/upstream/Loader.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 bd70150573..9e495f42bf 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 @@ -380,7 +380,13 @@ public final class Loader implements LoaderErrorThrower { callback.onLoadCanceled(loadable, nowMs, durationMs, false); break; case MSG_END_OF_SOURCE: - callback.onLoadCompleted(loadable, nowMs, durationMs); + try { + callback.onLoadCompleted(loadable, nowMs, durationMs); + } catch (RuntimeException e) { + // This should never happen, but handle it anyway. + Log.e(TAG, "Unexpected exception handling load completed", e); + fatalError = new UnexpectedLoaderException(e); + } break; case MSG_IO_EXCEPTION: currentError = (IOException) msg.obj; @@ -392,6 +398,9 @@ public final class Loader implements LoaderErrorThrower { start(getRetryDelayMillis()); } break; + default: + // Never happens. + break; } } From b60364600b468ad83089af82a3048a1238d358da Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 7 Nov 2017 05:22:48 -0800 Subject: [PATCH 014/417] Fix an issue with seeking that can lead to STATE_END not delivered. GitHub: #1897 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174841175 --- .../source/CompositeSequenceableLoader.java | 6 +- .../CompositeSequenceableLoaderTest.java | 274 ++++++++++++++++++ 2 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index 343d4f0bbe..a85d589762 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -63,7 +63,11 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { break; } for (SequenceableLoader loader : loaders) { - if (loader.getNextLoadPositionUs() == nextLoadPositionUs) { + long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); + boolean isLoaderBehind = + loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE + && loaderNextLoadPositionUs <= positionUs; + if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) { madeProgressThisIteration |= loader.continueLoading(positionUs); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java new file mode 100644 index 0000000000..e3ac104754 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2017 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; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link CompositeSequenceableLoader}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CompositeSequenceableLoaderTest { + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered + * position among all sub-loaders. + */ + @Test + public void testGetBufferedPositionUsReturnsMinimumLoaderBufferedPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered + * position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. + */ + @Test + public void testGetBufferedPositionUsReturnsMinimumNonEndOfSourceLoaderBufferedPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns + * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have buffered till end-of-source. + */ + @Test + public void testGetBufferedPositionUsReturnsEndOfSourceWhenAllLoaderBufferedTillEndOfSource() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next + * load position among all sub-loaders. + */ + @Test + public void testGetNextLoadPositionUsReturnMinimumLoaderNextLoadPositionUs() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next + * load position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. + */ + @Test + public void testGetNextLoadPositionUsReturnMinimumNonEndOfSourceLoaderNextLoadPositionUs() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns + * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have next load position at end-of-source. + */ + @Test + public void testGetNextLoadPositionUsReturnsEndOfSourceWhenAllLoaderLoadingLastChunk() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} only allows the loader + * with minimum next load position to continue loading if next load positions are not behind + * current playback position. + */ + @Test + public void testContinueLoadingOnlyAllowFurthestBehindLoaderToLoadIfNotBehindPlaybackPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + compositeSequenceableLoader.continueLoading(100); + + assertThat(loader1.numInvocations).isEqualTo(1); + assertThat(loader2.numInvocations).isEqualTo(0); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} allows all loaders + * with next load position behind current playback position to continue loading. + */ + @Test + public void testContinueLoadingReturnAllowAllLoadersBehindPlaybackPositionToLoad() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1002, /* nextLoadPositionUs */ 2002); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + compositeSequenceableLoader.continueLoading(3000); + + assertThat(loader1.numInvocations).isEqualTo(1); + assertThat(loader2.numInvocations).isEqualTo(1); + assertThat(loader3.numInvocations).isEqualTo(1); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} does not allow loader + * with next load position at end-of-source to continue loading. + */ + @Test + public void testContinueLoadingOnlyNotAllowEndOfSourceLoaderToLoad() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + compositeSequenceableLoader.continueLoading(3000); + + assertThat(loader1.numInvocations).isEqualTo(0); + assertThat(loader2.numInvocations).isEqualTo(0); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if the loader + * with minimum next load position can make progress if next load positions are not behind + * current playback position. + */ + @Test + public void testContinueLoadingReturnTrueIfFurthestBehindLoaderCanMakeProgress() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + loader1.setNextChunkDurationUs(1000); + + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + + assertThat(compositeSequenceableLoader.continueLoading(100)).isTrue(); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if any loader + * that are behind current playback position can make progress, even if it is not the one with + * minimum next load position. + */ + @Test + public void testContinueLoadingReturnTrueIfLoaderBehindPlaybackPositionCanMakeProgress() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + // loader2 is not the furthest behind, but it can make progress if allowed. + loader2.setNextChunkDurationUs(1000); + + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + + assertThat(compositeSequenceableLoader.continueLoading(3000)).isTrue(); + } + + private static class FakeSequenceableLoader implements SequenceableLoader { + + private long bufferedPositionUs; + private long nextLoadPositionUs; + private int numInvocations; + private int nextChunkDurationUs; + + private FakeSequenceableLoader(long bufferedPositionUs, long nextLoadPositionUs) { + this.bufferedPositionUs = bufferedPositionUs; + this.nextLoadPositionUs = nextLoadPositionUs; + } + + @Override + public long getBufferedPositionUs() { + return bufferedPositionUs; + } + + @Override + public long getNextLoadPositionUs() { + return nextLoadPositionUs; + } + + @Override + public boolean continueLoading(long positionUs) { + numInvocations++; + boolean loaded = nextChunkDurationUs != 0; + // The current chunk has been loaded, advance to next chunk. + bufferedPositionUs = nextLoadPositionUs; + nextLoadPositionUs += nextChunkDurationUs; + nextChunkDurationUs = 0; + return loaded; + } + + private void setNextChunkDurationUs(int nextChunkDurationUs) { + this.nextChunkDurationUs = nextChunkDurationUs; + } + + } + +} From 73851144ba3455af214bc3f5c9a0d8d9bd216eef Mon Sep 17 00:00:00 2001 From: yqritc Date: Wed, 8 Nov 2017 11:01:47 +0900 Subject: [PATCH 015/417] add bravia workaround to skip using setOutputSurface in MediaCodec --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 25e507d984..ac196da977 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1075,7 +1075,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/3355. return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name)) - || ("tcl_eu".equals(Util.DEVICE) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); + || (("tcl_eu".equals(Util.DEVICE) || Util.MODEL.startsWith("BRAVIA")) + && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); } /** From 6f729bdf97d5ebd496f55edc507223b9d5f85583 Mon Sep 17 00:00:00 2001 From: yqritc Date: Wed, 8 Nov 2017 13:41:04 +0900 Subject: [PATCH 016/417] add issue link --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index ac196da977..2d74dfba9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1071,8 +1071,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * If true is returned then we fall back to releasing and re-instantiating the codec instead. */ private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236 and - // https://github.com/google/ExoPlayer/issues/3355. + // Work around https://github.com/google/ExoPlayer/issues/3236, + // https://github.com/google/ExoPlayer/issues/3355 and + // https://github.com/google/ExoPlayer/issues/3439. return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name)) || (("tcl_eu".equals(Util.DEVICE) || Util.MODEL.startsWith("BRAVIA")) From d5600b52e17265109ec7e6921959696c5e75a648 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 9 Nov 2017 10:56:47 +0900 Subject: [PATCH 017/417] update bravia workaround more precisely --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 2d74dfba9e..41e3c970c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1076,7 +1076,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/3439. return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name)) - || (("tcl_eu".equals(Util.DEVICE) || Util.MODEL.startsWith("BRAVIA")) + || (("tcl_eu".equals(Util.DEVICE) || "SVP-DTV15".equals(Util.DEVICE) + || "BRAVIA_ATV2".equals(Util.DEVICE)) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); } From 580acb4e2f8e642883dfbd1f08a9ee9c1bbf2581 Mon Sep 17 00:00:00 2001 From: nvalletta Date: Sun, 12 Nov 2017 14:23:22 -0700 Subject: [PATCH 018/417] Add an easy way to set the shutter view background color --- .../exoplayer2/ui/SimpleExoPlayerView.java | 22 +++++++++++++++++++ library/ui/src/main/res/values/attrs.xml | 1 + 2 files changed, 23 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index fc41031756..86fe7a3f7f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -83,6 +83,12 @@ import java.util.List; *
  • Default: {@code true}
  • * * + *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} view. + *
      + *
    • Corresponding method: {@link #setShutterBackgroundColor(int)}
    • + *
    • Default: {@code 0}
    • + *
    + *
  • *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. *
      *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
    • @@ -249,6 +255,7 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } + int shutterColor = 0; int playerLayoutId = R.layout.exo_simple_player_view; boolean useArtwork = true; int defaultArtworkId = 0; @@ -262,6 +269,7 @@ public final class SimpleExoPlayerView extends FrameLayout { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { + shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); @@ -293,6 +301,9 @@ public final class SimpleExoPlayerView extends FrameLayout { // Shutter view. shutterView = findViewById(R.id.exo_shutter); + if (shutterView != null) { + shutterView.setBackgroundColor(shutterColor); + } // Create a surface view and insert it into the content frame, if there is one. if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { @@ -513,6 +524,17 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + /** + * Sets the background color of the {@code exo_shutter} view. + * + * @param color A resolved color (not a resource ID) for the background of the shutter view. + */ + public void setShutterBackgroundColor(int color) { + if (shutterView != null) { + shutterView.setBackgroundColor(color); + } + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { if (player != null && player.isPlayingAd()) { diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index d02d54ef23..eb9edaccdc 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -43,6 +43,7 @@ + From cb72d42e3ef658762f5e13f9ce7ea61b35d92618 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Nov 2017 07:36:45 -0800 Subject: [PATCH 019/417] Update 2.6.0 release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174853112 --- RELEASENOTES.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 42bf526de0..9bc7005ffc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,7 @@ * Added a reason to `EventListener.onPositionDiscontinuity` ([#3252](https://github.com/google/ExoPlayer/issues/3252)). * New `setShuffleModeEnabled` method for enabling shuffled playback. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. * Support for `Renderer`s that don't consume any media ([#3212](https://github.com/google/ExoPlayer/issues/3212)). * Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` @@ -14,9 +15,12 @@ * Fix playbacks involving looping, concatenation and ads getting stuck when media contains tracks with uneven durations ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Fix issue with `ContentDataSource` when reading from certain `ContentProvider` + implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)). * Better playback experience when the video decoder cannot keep up, by skipping to key-frames. This is particularly relevant for variable speed playbacks. -* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Allow `SingleSampleMediaSource` to suppress load errors + ([#3140](https://github.com/google/ExoPlayer/issues/3140)). * Audio: New `AudioSink` interface allows customization of audio output path. * Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming and progressive streams. @@ -24,8 +28,14 @@ * Fixed adaptive track selection logic for live playbacks ([#3017](https://github.com/google/ExoPlayer/issues/3017)). * Added ability to select the lowest bitrate tracks. +* DASH: + * Don't crash when a malformed or unexpected manifest update occurs + ([#2795](https://github.com/google/ExoPlayer/issues/2795)). * HLS: * Support for Widevine protected FMP4 variants. + * Support CEA-608 in FMP4 variants. + * Support extractor injection + ([#2748](https://github.com/google/ExoPlayer/issues/2748)). * DRM: * Improved compatibility with ClearKey content ([#3138](https://github.com/google/ExoPlayer/issues/3138)). From 98301467dc14e378aafcc5dc49f31a7f0da37b17 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 7 Nov 2017 07:50:35 -0800 Subject: [PATCH 020/417] Fix DefaultHlsExtractorFactory javadoc Issue:#2748 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174854541 --- .../exoplayer2/source/hls/DefaultHlsExtractorFactory.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 1aa7c47e04..c801520927 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -34,8 +34,6 @@ import java.util.List; /** * Default {@link HlsExtractorFactory} implementation. - * - *

      This class can be extended to override {@link TsExtractor} instantiation.

      */ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { From 15543f13b7d10c8280d9560d48346e28112899dc Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 7 Nov 2017 09:16:47 -0800 Subject: [PATCH 021/417] Decouple TrackGroups from SampleQueues in HlsSampleStreamWrapper This CL does not aim to introduce any functionality changes. Issue:#3149 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174864875 --- .../source/hls/HlsSampleStream.java | 14 +-- .../source/hls/HlsSampleStreamWrapper.java | 92 ++++++++++--------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index e423a682f3..0388f354ce 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -21,22 +21,22 @@ import com.google.android.exoplayer2.source.SampleStream; import java.io.IOException; /** - * {@link SampleStream} for a particular track group in HLS. + * {@link SampleStream} for a particular sample queue in HLS. */ /* package */ final class HlsSampleStream implements SampleStream { - public final int group; + public final int sampleQueueIndex; private final HlsSampleStreamWrapper sampleStreamWrapper; - public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int group) { + public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int sampleQueueIndex) { this.sampleStreamWrapper = sampleStreamWrapper; - this.group = group; + this.sampleQueueIndex = sampleQueueIndex; } @Override public boolean isReady() { - return sampleStreamWrapper.isReady(group); + return sampleStreamWrapper.isReady(sampleQueueIndex); } @Override @@ -46,12 +46,12 @@ import java.io.IOException; @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - return sampleStreamWrapper.readData(group, formatHolder, buffer, requireFormat); + return sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat); } @Override public int skipData(long positionUs) { - return sampleStreamWrapper.skipData(group, positionUs); + return sampleStreamWrapper.skipData(sampleQueueIndex, positionUs); } } 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 9816e4041c..0d2f758599 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 @@ -89,18 +89,19 @@ import java.util.LinkedList; private int[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; - private int enabledTrackCount; + private int enabledSampleQueueCount; private Format downstreamTrackFormat; private boolean released; // Tracks are complicated in HLS. See documentation of buildTracks for details. // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; - private int primaryTrackGroupIndex; - private boolean haveAudioVideoTrackGroups; // Indexed by track group. - private boolean[] trackGroupEnabledStates; - private boolean[] trackGroupIsAudioVideoFlags; + private int[] trackGroupToSampleQueueIndex; + private int primaryTrackGroupIndex; + private boolean haveAudioVideoSampleQueues; + private boolean[] sampleQueuesEnabledStates; + private boolean[] sampleQueueIsAudioVideoFlags; private long sampleOffsetUs; private long lastSeekPositionUs; @@ -134,6 +135,8 @@ import java.util.LinkedList; nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); sampleQueueTrackIds = new int[0]; sampleQueues = new SampleQueue[0]; + sampleQueueIsAudioVideoFlags = new boolean[0]; + sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new LinkedList<>(); maybeFinishPrepareRunnable = new Runnable() { @Override @@ -190,12 +193,11 @@ import java.util.LinkedList; public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) { Assertions.checkState(prepared); - int oldEnabledTrackCount = enabledTrackCount; + int oldEnabledSampleQueueCount = enabledSampleQueueCount; // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - int group = ((HlsSampleStream) streams[i]).group; - setTrackGroupEnabledState(group, false); + setSampleQueueEnabledState(((HlsSampleStream) streams[i]).sampleQueueIndex, false); streams[i] = null; } } @@ -203,7 +205,8 @@ import java.util.LinkedList; // a position other than the one we started preparing with, or if we're making a selection // having previously disabled all tracks. boolean seekRequired = forceReset - || (seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != lastSeekPositionUs); + || (seenFirstTrackSelection ? oldEnabledSampleQueueCount == 0 + : positionUs != lastSeekPositionUs); // Get the old (i.e. current before the loop below executes) primary track selection. The new // primary selection will equal the old one unless it's changed in the loop. TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection(); @@ -213,16 +216,17 @@ import java.util.LinkedList; if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - setTrackGroupEnabledState(trackGroupIndex, true); + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + setSampleQueueEnabledState(sampleQueueIndex, true); if (trackGroupIndex == primaryTrackGroupIndex) { primaryTrackSelection = selection; chunkSource.selectTracks(selection); } - streams[i] = new HlsSampleStream(this, trackGroupIndex); + streams[i] = new HlsSampleStream(this, sampleQueueIndex); streamResetFlags[i] = true; // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (!seekRequired) { - SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; + SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; sampleQueue.rewind(); // A seek can be avoided if we're able to advance to the current playback position in the // sample queue, or if we haven't read anything from the queue since the previous seek @@ -234,7 +238,7 @@ import java.util.LinkedList; } } - if (enabledTrackCount == 0) { + if (enabledSampleQueueCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; mediaChunks.clear(); @@ -290,7 +294,7 @@ import java.util.LinkedList; public void discardBuffer(long positionUs) { int sampleQueueCount = sampleQueues.length; for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues[i].discardTo(positionUs, false, trackGroupEnabledStates[i]); + sampleQueues[i].discardTo(positionUs, false, sampleQueuesEnabledStates[i]); } } @@ -370,8 +374,8 @@ import java.util.LinkedList; // SampleStream implementation. - public boolean isReady(int trackGroupIndex) { - return loadingFinished || (!isPendingReset() && sampleQueues[trackGroupIndex].hasNextSample()); + public boolean isReady(int sampleQueueIndex) { + return loadingFinished || (!isPendingReset() && sampleQueues[sampleQueueIndex].hasNextSample()); } public void maybeThrowError() throws IOException { @@ -379,8 +383,8 @@ import java.util.LinkedList; chunkSource.maybeThrowError(); } - public int readData(int trackGroupIndex, FormatHolder formatHolder, - DecoderInputBuffer buffer, boolean requireFormat) { + public int readData(int sampleQueueIndex, FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean requireFormat) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } @@ -399,12 +403,12 @@ import java.util.LinkedList; downstreamTrackFormat = trackFormat; } - return sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, loadingFinished, + return sampleQueues[sampleQueueIndex].read(formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); } - public int skipData(int trackGroupIndex, long positionUs) { - SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; + public int skipData(int sampleQueueIndex, long positionUs) { + SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); } else { @@ -415,8 +419,9 @@ import java.util.LinkedList; private boolean finishedReadingChunk(HlsMediaChunk chunk) { int chunkUid = chunk.uid; - for (int i = 0; i < sampleQueues.length; i++) { - if (trackGroupEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { return false; } } @@ -511,7 +516,7 @@ import java.util.LinkedList; loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { resetSampleQueues(); - if (enabledTrackCount > 0) { + if (enabledSampleQueueCount > 0) { callback.onContinueLoadingRequested(this); } } @@ -587,6 +592,11 @@ import java.util.LinkedList; sampleQueueTrackIds[trackCount] = id; sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; + sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); + sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO + || type == C.TRACK_TYPE_VIDEO; + haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; + sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1); return trackOutput; } @@ -605,7 +615,9 @@ import java.util.LinkedList; @Override public void onUpstreamFormatChanged(Format format) { - handler.post(maybeFinishPrepareRunnable); + if (!prepared) { + handler.post(maybeFinishPrepareRunnable); + } } // Called by the loading thread. @@ -696,17 +708,15 @@ import java.util.LinkedList; // Instantiate the necessary internal data-structures. primaryTrackGroupIndex = C.INDEX_UNSET; - trackGroupEnabledStates = new boolean[extractorTrackCount]; - trackGroupIsAudioVideoFlags = new boolean[extractorTrackCount]; + trackGroupToSampleQueueIndex = new int[extractorTrackCount]; + for (int i = 0; i < extractorTrackCount; i++) { + trackGroupToSampleQueueIndex[i] = i; + } // Construct the set of exposed track groups. TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; for (int i = 0; i < extractorTrackCount; i++) { Format sampleFormat = sampleQueues[i].getUpstreamFormat(); - String mimeType = sampleFormat.sampleMimeType; - boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType); - trackGroupIsAudioVideoFlags[i] = isAudioVideo; - haveAudioVideoTrackGroups |= isAudioVideo; if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; for (int j = 0; j < chunkSourceTrackCount; j++) { @@ -724,15 +734,15 @@ import java.util.LinkedList; } /** - * Enables or disables a specified track group. + * Enables or disables a specified sample queue. * - * @param trackGroupIndex The index of the track group. - * @param enabledState True if the group is being enabled, or false if it's being disabled. + * @param sampleQueueIndex The index of the sample queue. + * @param enabledState True if the sample queue is being enabled, or false if it's being disabled. */ - private void setTrackGroupEnabledState(int trackGroupIndex, boolean enabledState) { - Assertions.checkState(trackGroupEnabledStates[trackGroupIndex] != enabledState); - trackGroupEnabledStates[trackGroupIndex] = enabledState; - enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1); + private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) { + Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState); + sampleQueuesEnabledStates[sampleQueueIndex] = enabledState; + enabledSampleQueueCount = enabledSampleQueueCount + (enabledState ? 1 : -1); } /** @@ -769,8 +779,8 @@ import java.util.LinkedList; * @return Whether the in-buffer seek was successful. */ private boolean seekInsideBufferUs(long positionUs) { - int trackCount = sampleQueues.length; - for (int i = 0; i < trackCount; i++) { + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) @@ -779,7 +789,7 @@ import java.util.LinkedList; // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is // successful only if the seek into every queue succeeds. - if (!seekInsideQueue && (trackGroupIsAudioVideoFlags[i] || !haveAudioVideoTrackGroups)) { + if (!seekInsideQueue && (sampleQueueIsAudioVideoFlags[i] || !haveAudioVideoSampleQueues)) { return false; } sampleQueue.discardToRead(); From acf5247f6e61e552dcf12d29c216c0b8fbc594a2 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Nov 2017 07:56:16 -0800 Subject: [PATCH 022/417] Work around incorrect ClearKey encoding prior to O-MR1 Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175006223 --- .../android/exoplayer2/drm/ClearKeyUtil.java | 109 ++++++++++++++++++ .../exoplayer2/drm/DefaultDrmSession.java | 22 +++- .../exoplayer2/drm/FrameworkMediaDrm.java | 3 +- .../google/android/exoplayer2/util/Util.java | 10 ++ .../exoplayer2/drm/ClearKeyUtilTest.java | 64 ++++++++++ 5 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java new file mode 100644 index 0000000000..ee337dcc51 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017 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.drm; + +import android.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Utility methods for ClearKey. + */ +/* package */ final class ClearKeyUtil { + + private static final String TAG = "ClearKeyUtil"; + private static final Pattern REQUEST_KIDS_PATTERN = Pattern.compile("\"kids\":\\[\"(.*?)\"]"); + + private ClearKeyUtil() {} + + /** + * Adjusts ClearKey request data obtained from the Android ClearKey CDM to be spec compliant. + * + * @param request The request data. + * @return The adjusted request data. + */ + public static byte[] adjustRequestData(byte[] request) { + if (Util.SDK_INT >= 27) { + return request; + } + // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 rather + // than Base64Url. See [Internal: b/64388098]. Any "/" characters that ended up in the request + // as a result were not escaped as "\/". We know the exact request format from the platform's + // InitDataParser.cpp, so we can use a regexp rather than parsing the JSON. + String requestString = Util.fromUtf8Bytes(request); + Matcher requestKidsMatcher = REQUEST_KIDS_PATTERN.matcher(requestString); + if (!requestKidsMatcher.find()) { + Log.e(TAG, "Failed to adjust request data: " + requestString); + return request; + } + int kidsStartIndex = requestKidsMatcher.start(1); + int kidsEndIndex = requestKidsMatcher.end(1); + StringBuilder adjustedRequestBuilder = new StringBuilder(requestString); + base64ToBase64Url(adjustedRequestBuilder, kidsStartIndex, kidsEndIndex); + return Util.getUtf8Bytes(adjustedRequestBuilder.toString()); + } + + /** + * Adjusts ClearKey response data to be suitable for providing to the Android ClearKey CDM. + * + * @param response The response data. + * @return The adjusted response data. + */ + public static byte[] adjustResponseData(byte[] response) { + if (Util.SDK_INT >= 27) { + return response; + } + // Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for + // the "k" and "kid" strings. See [Internal: b/64388098]. + try { + JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response)); + JSONArray keysArray = responseJson.getJSONArray("keys"); + for (int i = 0; i < keysArray.length(); i++) { + JSONObject key = keysArray.getJSONObject(i); + key.put("k", base64UrlToBase64(key.getString("k"))); + key.put("kid", base64UrlToBase64(key.getString("kid"))); + } + return Util.getUtf8Bytes(responseJson.toString()); + } catch (JSONException e) { + Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e); + return response; + } + } + + private static void base64ToBase64Url(StringBuilder base64, int startIndex, int endIndex) { + for (int i = startIndex; i < endIndex; i++) { + switch (base64.charAt(i)) { + case '+': + base64.setCharAt(i, '-'); + break; + case '/': + base64.setCharAt(i, '_'); + break; + default: + break; + } + } + } + + private static String base64UrlToBase64(String base64) { + return base64.replace('-', '+').replace('_', '/'); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 688fff48fb..c391b7035d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -362,6 +362,20 @@ import java.util.UUID; try { KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); + if (C.CLEARKEY_UUID.equals(uuid)) { + final byte[] data = ClearKeyUtil.adjustRequestData(request.getData()); + final String defaultUrl = request.getDefaultUrl(); + request = new KeyRequest() { + @Override + public byte[] getData() { + return data; + } + @Override + public String getDefaultUrl() { + return defaultUrl; + } + }; + } postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); } catch (Exception e) { onKeysError(e); @@ -380,8 +394,12 @@ import java.util.UUID; } try { + byte[] responseData = (byte[]) response; + if (C.CLEARKEY_UUID.equals(uuid)) { + responseData = ClearKeyUtil.adjustResponseData(responseData); + } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { - mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); + mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override @@ -391,7 +409,7 @@ import java.util.UUID; }); } } else { - byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); + byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD || (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null)) && keySetId != null && keySetId.length != 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index c3ab3462d9..517ca9247c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -25,6 +25,7 @@ import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -74,7 +75,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { @Override - public void onEvent(@NonNull MediaDrm md, @NonNull byte[] sessionId, int event, int extra, + public void onEvent(@NonNull MediaDrm md, @Nullable byte[] sessionId, int event, int extra, byte[] data) { listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); } 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 6c61c221fd..6302563e74 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 @@ -246,6 +246,16 @@ public final class Util { return language == null ? null : new Locale(language).getLanguage(); } + /** + * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes. + * + * @param bytes The UTF-8 encoded bytes to decode. + * @return The string. + */ + public static String fromUtf8Bytes(byte[] bytes) { + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } + /** * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java new file mode 100644 index 0000000000..01ab9ea9aa --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 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.drm; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import java.nio.charset.Charset; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link ClearKeyUtil}. + */ +// TODO: When API level 27 is supported, add tests that check the adjust methods are no-ops. +@RunWith(RobolectricTestRunner.class) +public final class ClearKeyUtilTest { + + @Config(sdk = 26, manifest = Config.NONE) + @Test + public void testAdjustResponseDataV26() { + byte[] data = ("{\"keys\":[{" + + "\"k\":\"abc_def-\"," + + "\"kid\":\"ab_cde-f\"}]," + + "\"type\":\"abc_def-" + + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); + // We expect "-" and "_" to be replaced with "+" and "\/" (forward slashes need to be escaped in + // JSON respectively, for "k" and "kid" only. + byte[] expected = ("{\"keys\":[{" + + "\"k\":\"abc\\/def+\"," + + "\"kid\":\"ab\\/cde+f\"}]," + + "\"type\":\"abc_def-" + + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); + assertThat(Arrays.equals(expected, ClearKeyUtil.adjustResponseData(data))).isTrue(); + } + + @Config(sdk = 26, manifest = Config.NONE) + @Test + public void testAdjustRequestDataV26() { + byte[] data = "{\"kids\":[\"abc+def/\",\"ab+cde/f\"],\"type\":\"abc+def/\"}" + .getBytes(Charset.forName(C.UTF8_NAME)); + // We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids". + byte[] expected = "{\"kids\":[\"abc-def_\",\"ab-cde_f\"],\"type\":\"abc+def/\"}" + .getBytes(Charset.forName(C.UTF8_NAME)); + assertThat(Arrays.equals(expected, ClearKeyUtil.adjustRequestData(data))).isTrue(); + } + +} From 367bb64ba07eb8d50e3f8d7d44b2e4f198eb9fcd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Nov 2017 08:34:20 -0800 Subject: [PATCH 023/417] Add default implementations for ExoMediaDrm.* interfaces ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175010595 --- .../exoplayer2/drm/DefaultDrmSession.java | 15 +-- .../android/exoplayer2/drm/ExoMediaDrm.java | 95 +++++++++++++++++-- .../exoplayer2/drm/FrameworkMediaDrm.java | 54 ++--------- 3 files changed, 95 insertions(+), 69 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index c391b7035d..25fdaba5b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -25,6 +25,7 @@ import android.os.Message; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.Arrays; @@ -363,18 +364,8 @@ import java.util.UUID; KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); if (C.CLEARKEY_UUID.equals(uuid)) { - final byte[] data = ClearKeyUtil.adjustRequestData(request.getData()); - final String defaultUrl = request.getDefaultUrl(); - request = new KeyRequest() { - @Override - public byte[] getData() { - return data; - } - @Override - public String getDefaultUrl() { - return defaultUrl; - } - }; + request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), + request.getDefaultUrl()); } postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index e9ee1ce90b..cecc840511 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -82,14 +82,6 @@ public interface ExoMediaDrm { byte[] data); } - /** - * @see android.media.MediaDrm.KeyStatus - */ - interface ExoKeyStatus { - int getStatusCode(); - byte[] getKeyId(); - } - /** * @see android.media.MediaDrm.OnKeyStatusChangeListener */ @@ -100,11 +92,44 @@ public interface ExoMediaDrm { * * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred. * @param sessionId the DRM session ID on which the event occurred. - * @param exoKeyInfo a list of {@link ExoKeyStatus} that contains key ID and status. + * @param exoKeyInfo a list of {@link KeyStatus} that contains key ID and status. * @param hasNewUsableKey true if new key becomes usable. */ void onKeyStatusChange(ExoMediaDrm mediaDrm, byte[] sessionId, - List exoKeyInfo, boolean hasNewUsableKey); + List exoKeyInfo, boolean hasNewUsableKey); + } + + /** + * @see android.media.MediaDrm.KeyStatus + */ + interface KeyStatus { + int getStatusCode(); + byte[] getKeyId(); + } + + /** + * Default implementation of {@link KeyStatus}. + */ + final class DefaultKeyStatus implements KeyStatus { + + private final int statusCode; + private final byte[] keyId; + + DefaultKeyStatus(int statusCode, byte[] keyId) { + this.statusCode = statusCode; + this.keyId = keyId; + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public byte[] getKeyId() { + return keyId; + } + } /** @@ -115,6 +140,31 @@ public interface ExoMediaDrm { String getDefaultUrl(); } + /** + * Default implementation of {@link KeyRequest}. + */ + final class DefaultKeyRequest implements KeyRequest { + + private final byte[] data; + private final String defaultUrl; + + public DefaultKeyRequest(byte[] data, String defaultUrl) { + this.data = data; + this.defaultUrl = defaultUrl; + } + + @Override + public byte[] getData() { + return data; + } + + @Override + public String getDefaultUrl() { + return defaultUrl; + } + + } + /** * @see android.media.MediaDrm.ProvisionRequest */ @@ -123,6 +173,31 @@ public interface ExoMediaDrm { String getDefaultUrl(); } + /** + * Default implementation of {@link ProvisionRequest}. + */ + final class DefaultProvisionRequest implements ProvisionRequest { + + private final byte[] data; + private final String defaultUrl; + + public DefaultProvisionRequest(byte[] data, String defaultUrl) { + this.data = data; + this.defaultUrl = defaultUrl; + } + + @Override + public byte[] getData() { + return data; + } + + @Override + public String getDefaultUrl() { + return defaultUrl; + } + + } + /** * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener) */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 517ca9247c..f960cd637f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -20,7 +20,6 @@ import android.media.DeniedByServerException; import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; -import android.media.MediaDrm.KeyStatus; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; @@ -93,12 +92,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm keyInfo, boolean hasNewUsableKey) { - List exoKeyInfo = new ArrayList<>(); - for (KeyStatus keyStatus : keyInfo) { - exoKeyInfo.add(new FrameworkKeyStatus(keyStatus)); + @NonNull List keyInfo, boolean hasNewUsableKey) { + List exoKeyInfo = new ArrayList<>(); + for (MediaDrm.KeyStatus keyStatus : keyInfo) { + exoKeyInfo.add(new DefaultKeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); } - listener.onKeyStatusChange(FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey); } @@ -120,17 +118,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, optionalParameters); - return new KeyRequest() { - @Override - public byte[] getData() { - return request.getData(); - } - - @Override - public String getDefaultUrl() { - return request.getDefaultUrl(); - } - }; + return new DefaultKeyRequest(request.getData(), request.getDefaultUrl()); } @Override @@ -141,18 +129,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Wed, 8 Nov 2017 08:45:17 -0800 Subject: [PATCH 024/417] Don't allow cancelation of non-cancelable loads Issue: #3441 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175011804 --- .../source/chunk/ChunkSampleStream.java | 28 ++++++++++++------- .../exoplayer2/source/chunk/ChunkSource.java | 3 +- 2 files changed, 20 insertions(+), 11 deletions(-) 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 b64dec59bf..8a9be92d75 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,8 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.util.Log; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -38,6 +40,8 @@ import java.util.List; public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback, Loader.ReleaseCallback { + private static final String TAG = "ChunkSampleStream"; + private final int primaryTrackType; private final int[] embeddedTrackTypes; private final boolean[] embeddedTracksSelected; @@ -318,16 +322,20 @@ public class ChunkSampleStream implements SampleStream, S boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk(); boolean canceled = false; if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { - canceled = true; - if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.removeLast(); - Assertions.checkState(removed == loadable); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); - } - if (mediaChunks.isEmpty()) { - pendingResetPositionUs = lastSeekPositionUs; + if (!cancelable) { + Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); + } else { + canceled = true; + if (isMediaChunk) { + BaseMediaChunk removed = mediaChunks.removeLast(); + Assertions.checkState(removed == loadable); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index 6dffc457d6..b04dc7cbdb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -85,7 +85,8 @@ public interface ChunkSource { * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. * @param e The error. - * @return Whether the load should be canceled. + * @return Whether the load should be canceled. Should always be false if {@code cancelable} is + * false. */ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e); From 3171c86bdbd65970e3f5f5ce536f1dfc215b8a84 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 8 Nov 2017 09:35:11 -0800 Subject: [PATCH 025/417] Supports Out-of-band, in MPD EventStream. MPD file may include multiple EventStreams in its Periods, which contains Events that the application may need to handle/respond to. This change adds support for parsing the EventStream/Event nodes from MPD file, and exposing these EventStreams as a metadata sample stream that application can respond in a similar way to other metadata events. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175017697 --- .../metadata/emsg/EventMessageDecoder.java | 3 +- .../metadata/emsg/EventMessageEncoder.java | 90 +++++ .../emsg/EventMessageDecoderTest.java | 60 ++++ .../assets/sample_mpd_4_event_stream | 61 ++++ .../dash/manifest/DashManifestParserTest.java | 48 +++ .../source/dash/DashMediaPeriod.java | 308 ++++++++++++++---- .../source/dash/EventSampleStream.java | 130 ++++++++ .../source/dash/manifest/DashManifest.java | 4 +- .../dash/manifest/DashManifestParser.java | 153 ++++++++- .../source/dash/manifest/EventStream.java | 66 ++++ .../source/dash/manifest/Period.java | 22 +- 11 files changed, 871 insertions(+), 74 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java create mode 100644 library/dash/src/androidTest/assets/sample_mpd_4_event_stream create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index fd6996aa80..266988246d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.Arrays; @@ -40,7 +41,7 @@ public final class EventMessageDecoder implements MetadataDecoder { String value = emsgData.readNullTerminatedString(); long timescale = emsgData.readUnsignedInt(); emsgData.skipBytes(4); // presentation_time_delta - long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale; + long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java new file mode 100644 index 0000000000..2ddbfb4708 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2017 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.metadata.emsg; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Encodes data that can be decoded by {@link EventMessageDecoder}. This class isn't thread safe. + */ +public final class EventMessageEncoder { + + private final ByteArrayOutputStream byteArrayOutputStream; + private final DataOutputStream dataOutputStream; + + public EventMessageEncoder() { + byteArrayOutputStream = new ByteArrayOutputStream(512); + dataOutputStream = new DataOutputStream(byteArrayOutputStream); + } + + /** + * Encodes an {@link EventMessage} to a byte array that can be decoded by + * {@link EventMessageDecoder}. + * + * @param eventMessage The event message to be encoded. + * @param timescale Timescale of the event message, in units per second. + * @param presentationTimeUs The presentation time of the event message in microseconds. + * @return The serialized byte array. + */ + @Nullable + public byte[] encode(EventMessage eventMessage, long timescale, long presentationTimeUs) { + Assertions.checkArgument(timescale >= 0); + byteArrayOutputStream.reset(); + try { + writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri); + String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; + writeNullTerminatedString(dataOutputStream, nonNullValue); + writeUnsignedInt(dataOutputStream, timescale); + long presentationTime = Util.scaleLargeTimestamp(presentationTimeUs, timescale, + C.MICROS_PER_SECOND); + writeUnsignedInt(dataOutputStream, presentationTime); + long duration = Util.scaleLargeTimestamp(eventMessage.durationMs, timescale, 1000); + writeUnsignedInt(dataOutputStream, duration); + writeUnsignedInt(dataOutputStream, eventMessage.id); + dataOutputStream.write(eventMessage.messageData); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + dataOutputStream.close(); + } catch (IOException ignored) { + // ignored + } + } + } + + private static void writeNullTerminatedString(DataOutputStream dataOutputStream, String value) + throws IOException { + dataOutputStream.writeBytes(value); + dataOutputStream.writeByte(0); + } + + private static void writeUnsignedInt(DataOutputStream outputStream, long value) + throws IOException { + outputStream.writeByte((int) (value >>> 24) & 0xFF); + outputStream.writeByte((int) (value >>> 16) & 0xFF); + outputStream.writeByte((int) (value >>> 8) & 0xFF); + outputStream.writeByte((int) value & 0xFF); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index 1ce0ccb93d..f7f0c63300 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import java.io.IOException; import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,4 +56,63 @@ public final class EventMessageDecoderTest { assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); } + @Test + public void testEncodeEventStream() throws IOException { + EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, + new byte[] {0, 1, 2, 3, 4}); + byte[] expectedEmsgBody = new byte[] { + 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" + 49, 50, 51, 0, // value = "123" + 0, 0, -69, -128, // timescale = 48000 + 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 2, 50, -128, // event_duration = 144000 + 0, 15, 67, -45, // id = 1000403 + 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000, 1000000); + assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + } + + @Test + public void testEncodeDecodeEventStream() throws IOException { + EventMessage expectedEmsg = new EventMessage("urn:test", "123", 3000, 1000403, + new byte[] {0, 1, 2, 3, 4}); + byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000, 1); + MetadataInputBuffer buffer = new MetadataInputBuffer(); + buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); + + EventMessageDecoder decoder = new EventMessageDecoder(); + Metadata metadata = decoder.decode(buffer); + assertThat(metadata.length()).isEqualTo(1); + assertThat(metadata.get(0)).isEqualTo(expectedEmsg); + } + + @Test + public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { + EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, + new byte[] {0, 1, 2, 3, 4}); + byte[] expectedEmsgBody = new byte[] { + 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" + 49, 50, 51, 0, // value = "123" + 0, 0, -69, -128, // timescale = 48000 + 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 2, 50, -128, // event_duration = 144000 + 0, 15, 67, -45, // id = 1000403 + 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, + new byte[] {4, 3, 2, 1, 0}); + byte[] expectedEmsgBody1 = new byte[] { + 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" + 49, 50, 51, 0, // value = "123" + 0, 0, -69, -128, // timescale = 48000 + 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 2, 50, -128, // event_duration = 144000 + 0, 15, 67, -46, // id = 1000402 + 4, 3, 2, 1, 0}; // message_data = {4, 3, 2, 1, 0} + EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); + byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000, 1000000); + assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000, 1000000); + assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); + } + } diff --git a/library/dash/src/androidTest/assets/sample_mpd_4_event_stream b/library/dash/src/androidTest/assets/sample_mpd_4_event_stream new file mode 100644 index 0000000000..4352205d3f --- /dev/null +++ b/library/dash/src/androidTest/assets/sample_mpd_4_event_stream @@ -0,0 +1,61 @@ + + + + + + 1 800 10101010 + + + + + + The title + The description + + + GB + + + ]]> + + + + + /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA== + + + + + + + + + + + + + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + + + + + diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 3ce4b37ec6..b24a7da878 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import java.util.Collections; @@ -31,6 +32,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { private static final String SAMPLE_MPD_1 = "sample_mpd_1"; private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = "sample_mpd_2_unknown_mime_type"; private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE = "sample_mpd_3_segment_template"; + private static final String SAMPLE_MPD_4_EVENT_STREAM = "sample_mpd_4_event_stream"; /** * Simple test to ensure the sample manifests parse without any exceptions being thrown. @@ -69,6 +71,52 @@ public class DashManifestParserTest extends InstrumentationTestCase { } } + public void testParseMediaPresentationDescriptionCanParseEventStream() + throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_4_EVENT_STREAM)); + + Period period = mpd.getPeriod(0); + assertEquals(3, period.eventStreams.size()); + // assert text-only event stream + EventStream eventStream1 = period.eventStreams.get(0); + assertEquals(1, eventStream1.events.length); + EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0, + "+ 1 800 10101010".getBytes()); + assertEquals(expectedEvent1, eventStream1.events[0]); + + // assert CData-structured event stream + EventStream eventStream2 = period.eventStreams.get(1); + assertEquals(1, eventStream2.events.length); + assertEquals( + new EventMessage("urn:dvb:iptv:cpm:2014", "", 1500000, 1, + ("\n" + + " \n" + + " \n" + + " The title\n" + + " The description\n" + + " \n" + + " \n" + + " GB\n" + + " \n" + + " \n" + + " ]]>").getBytes()), + eventStream2.events[0]); + + // assert xml-structured event stream + EventStream eventStream3 = period.eventStreams.get(2); + assertEquals(1, eventStream3.events.length); + assertEquals( + new EventMessage("urn:scte:scte35:2014:xml+bin", "", 1000000, 2, + ("\n" + + " \n" + + " /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n" + + " \n" + + " ").getBytes()), + eventStream3.events[0]); + } + public void testParseCea608AccessibilityChannel() { assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel( buildCea608AccessibilityDescriptors("CC1=eng"))); 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 35f3c2e129..b5ce45b2f5 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import android.support.annotation.IntDef; import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; @@ -32,16 +33,21 @@ import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSamp import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Descriptor; +import com.google.android.exoplayer2.source.dash.manifest.EventStream; +import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A DASH {@link MediaPeriod}. @@ -61,9 +67,11 @@ import java.util.List; private Callback callback; private ChunkSampleStream[] sampleStreams; + private EventSampleStream[] eventSampleStreams; private CompositeSequenceableLoader sequenceableLoader; private DashManifest manifest; private int periodIndex; + private List eventStreams; public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, @@ -79,22 +87,41 @@ import java.util.List; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.allocator = allocator; sampleStreams = newSampleStreamArray(0); + eventSampleStreams = new EventSampleStream[0]; sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - Pair result = - buildTrackGroups(manifest.getPeriod(periodIndex).adaptationSets); + Period period = manifest.getPeriod(periodIndex); + eventStreams = period.eventStreams; + Pair result = buildTrackGroups(period.adaptationSets, + eventStreams); trackGroups = result.first; trackGroupInfos = result.second; } + /** + * Updates the {@link DashManifest} and the index of this period in the manifest. + *

      + * @param manifest The updated manifest. + * @param periodIndex the new index of this period in the updated manifest. + */ public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; this.periodIndex = periodIndex; + Period period = manifest.getPeriod(periodIndex); if (sampleStreams != null) { for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.getChunkSource().updateManifest(manifest, periodIndex); } callback.onContinueLoadingRequested(this); } + eventStreams = period.eventStreams; + for (EventSampleStream eventSampleStream : eventSampleStreams) { + for (EventStream eventStream : eventStreams) { + if (eventStream.id().equals(eventSampleStream.eventStreamId())) { + eventSampleStream.updateEventStream(eventStream, manifest.dynamic); + break; + } + } + } } public void release() { @@ -122,8 +149,27 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - HashMap> primarySampleStreams = new HashMap<>(); - // First pass for primary tracks. + Map> primarySampleStreams = new HashMap<>(); + List eventSampleStreamList = new ArrayList<>(); + + selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs, primarySampleStreams); + selectEventSampleStreams(selections, mayRetainStreamFlags, streams, + streamResetFlags, eventSampleStreamList); + selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs, primarySampleStreams); + + sampleStreams = newSampleStreamArray(primarySampleStreams.size()); + primarySampleStreams.values().toArray(sampleStreams); + eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; + eventSampleStreamList.toArray(eventSampleStreams); + sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + return positionUs; + } + + private void selectPrimarySampleStreams(TrackSelection[] selections, + boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, + long positionUs, Map> primarySampleStreams) { for (int i = 0; i < selections.length; i++) { if (streams[i] instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") @@ -136,10 +182,11 @@ import java.util.List; primarySampleStreams.put(trackGroupIndex, stream); } } + if (streams[i] == null && selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.isPrimary) { + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { ChunkSampleStream stream = buildSampleStream(trackGroupInfo, selections[i], positionUs); primarySampleStreams.put(trackGroupIndex, stream); @@ -148,7 +195,39 @@ import java.util.List; } } } - // Second pass for embedded tracks. + } + + private void selectEventSampleStreams(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, + List eventSampleStreamsList) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] instanceof EventSampleStream) { + EventSampleStream stream = (EventSampleStream) streams[i]; + if (selections[i] == null || !mayRetainStreamFlags[i]) { + streams[i] = null; + } else { + eventSampleStreamsList.add(stream); + } + } + + if (streams[i] == null && selections[i] != null) { + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { + EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); + Format format = selections[i].getTrackGroup().getFormat(0); + EventSampleStream stream = new EventSampleStream(eventStream, format, manifest.dynamic); + streams[i] = stream; + streamResetFlags[i] = true; + eventSampleStreamsList.add(stream); + } + } + } + } + + private void selectEmbeddedSampleStreams(TrackSelection[] selections, + boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, + long positionUs, Map> primarySampleStreams) { for (int i = 0; i < selections.length; i++) { if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) && (selections[i] == null || !mayRetainStreamFlags[i])) { @@ -161,7 +240,7 @@ import java.util.List; if (selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (!trackGroupInfo.isPrimary) { + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { ChunkSampleStream primaryStream = primarySampleStreams.get( trackGroupInfo.primaryTrackGroupIndex); SampleStream stream = streams[i]; @@ -177,10 +256,6 @@ import java.util.List; } } } - sampleStreams = newSampleStreamArray(primarySampleStreams.size()); - primarySampleStreams.values().toArray(sampleStreams); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - return positionUs; } @Override @@ -215,6 +290,9 @@ import java.util.List; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.seekToUs(positionUs); } + for (EventSampleStream sampleStream : eventSampleStreams) { + sampleStream.seekToUs(positionUs); + } return positionUs; } @@ -228,62 +306,25 @@ import java.util.List; // Internal methods. private static Pair buildTrackGroups( - List adaptationSets) { + List adaptationSets, List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); int primaryGroupCount = groupedAdaptationSetIndices.length; boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount]; boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount]; - int totalGroupCount = primaryGroupCount; - for (int i = 0; i < primaryGroupCount; i++) { - if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) { - primaryGroupHasEventMessageTrackFlags[i] = true; - totalGroupCount++; - } - if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) { - primaryGroupHasCea608TrackFlags[i] = true; - totalGroupCount++; - } - } + int totalEmbeddedTrackGroupCount = identifyEmbeddedTracks(primaryGroupCount, adaptationSets, + groupedAdaptationSetIndices, primaryGroupHasEventMessageTrackFlags, + primaryGroupHasCea608TrackFlags); + int totalGroupCount = primaryGroupCount + totalEmbeddedTrackGroupCount + eventStreams.size(); TrackGroup[] trackGroups = new TrackGroup[totalGroupCount]; TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount]; - int trackGroupCount = 0; - for (int i = 0; i < primaryGroupCount; i++) { - int[] adaptationSetIndices = groupedAdaptationSetIndices[i]; - List representations = new ArrayList<>(); - for (int adaptationSetIndex : adaptationSetIndices) { - representations.addAll(adaptationSets.get(adaptationSetIndex).representations); - } - Format[] formats = new Format[representations.size()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = representations.get(j).format; - } + int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos(adaptationSets, + groupedAdaptationSetIndices, primaryGroupCount, primaryGroupHasEventMessageTrackFlags, + primaryGroupHasCea608TrackFlags, trackGroups, trackGroupInfos); - AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); - int primaryTrackGroupIndex = trackGroupCount; - boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; - boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i]; - - trackGroups[trackGroupCount] = new TrackGroup(formats); - trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type, - adaptationSetIndices, primaryTrackGroupIndex, true, hasEventMessageTrack, hasCea608Track); - if (hasEventMessageTrack) { - Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", - MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); - trackGroups[trackGroupCount] = new TrackGroup(format); - trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_METADATA, - adaptationSetIndices, primaryTrackGroupIndex, false, false, false); - } - if (hasCea608Track) { - Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608", - MimeTypes.APPLICATION_CEA608, 0, null); - trackGroups[trackGroupCount] = new TrackGroup(format); - trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_TEXT, - adaptationSetIndices, primaryTrackGroupIndex, false, false, false); - } - } + buildManifestEventTrackGroupInfos(eventStreams, trackGroups, trackGroupInfos, trackGroupCount); return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos); } @@ -326,6 +367,90 @@ import java.util.List; ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices; } + /** + * Iterates through list of primary track groups and identifies embedded tracks. + *

      + * @param primaryGroupCount The number of primary track groups. + * @param adaptationSets The list of {@link AdaptationSet} of the current DASH period. + * @param groupedAdaptationSetIndices The indices of {@link AdaptationSet} that belongs to + * the same primary group, grouped in primary track groups order. + * @param primaryGroupHasEventMessageTrackFlags An output array containing boolean flag, each + * indicates whether the corresponding primary track group contains an embedded event message + * track. + * @param primaryGroupHasCea608TrackFlags An output array containing boolean flag, each + * indicates whether the corresponding primary track group contains an embedded Cea608 track. + * @return Total number of embedded tracks. + */ + private static int identifyEmbeddedTracks(int primaryGroupCount, + List adaptationSets, int[][] groupedAdaptationSetIndices, + boolean[] primaryGroupHasEventMessageTrackFlags, boolean[] primaryGroupHasCea608TrackFlags) { + int numEmbeddedTrack = 0; + for (int i = 0; i < primaryGroupCount; i++) { + if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) { + primaryGroupHasEventMessageTrackFlags[i] = true; + numEmbeddedTrack++; + } + if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) { + primaryGroupHasCea608TrackFlags[i] = true; + numEmbeddedTrack++; + } + } + return numEmbeddedTrack; + } + + private static int buildPrimaryAndEmbeddedTrackGroupInfos(List adaptationSets, + int[][] groupedAdaptationSetIndices, int primaryGroupCount, + boolean[] primaryGroupHasEventMessageTrackFlags, boolean[] primaryGroupHasCea608TrackFlags, + TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos) { + int trackGroupCount = 0; + for (int i = 0; i < primaryGroupCount; i++) { + int[] adaptationSetIndices = groupedAdaptationSetIndices[i]; + List representations = new ArrayList<>(); + for (int adaptationSetIndex : adaptationSetIndices) { + representations.addAll(adaptationSets.get(adaptationSetIndex).representations); + } + Format[] formats = new Format[representations.size()]; + for (int j = 0; j < formats.length; j++) { + formats[j] = representations.get(j).format; + } + + AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); + int primaryTrackGroupIndex = trackGroupCount; + boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; + boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i]; + + trackGroups[trackGroupCount] = new TrackGroup(formats); + trackGroupInfos[trackGroupCount++] = TrackGroupInfo.primaryTrack(firstAdaptationSet.type, + adaptationSetIndices, primaryTrackGroupIndex, hasEventMessageTrack, hasCea608Track); + if (hasEventMessageTrack) { + Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", + MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + trackGroups[trackGroupCount] = new TrackGroup(format); + trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, + primaryTrackGroupIndex); + } + if (hasCea608Track) { + Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608", + MimeTypes.APPLICATION_CEA608, 0, null); + trackGroups[trackGroupCount] = new TrackGroup(format); + trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedCea608Track( + adaptationSetIndices, primaryTrackGroupIndex); + } + } + return trackGroupCount; + } + + private static void buildManifestEventTrackGroupInfos(List eventStreams, + TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos, int existingTrackGroupCount) { + for (int i = 0; i < eventStreams.size(); i++) { + EventStream eventStream = eventStreams.get(i); + Format format = Format.createSampleFormat(eventStream.id(), MimeTypes.APPLICATION_EMSG, null, + Format.NO_VALUE, null); + trackGroups[existingTrackGroupCount] = new TrackGroup(format); + trackGroupInfos[existingTrackGroupCount++] = TrackGroupInfo.mpdEventTrack(i); + } + } + private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo, TrackSelection selection, long positionUs) { int embeddedTrackCount = 0; @@ -402,24 +527,75 @@ import java.util.List; private static final class TrackGroupInfo { + @Retention(RetentionPolicy.SOURCE) + @IntDef({CATEGORY_PRIMARY, CATEGORY_EMBEDDED, CATEGORY_MANIFEST_EVENTS}) + public @interface TrackGroupCategory {} + + /** + * A normal track group that has its samples drawn from the stream. + * For example: a video Track Group or an audio Track Group. + */ + private static final int CATEGORY_PRIMARY = 0; + + /** + * A track group whose samples are embedded within one of the primary streams. + * For example: an EMSG track has its sample embedded in `emsg' atoms in one of the primary + * streams. + */ + private static final int CATEGORY_EMBEDDED = 1; + + /** + * A track group that has its samples listed explicitly in the DASH manifest file. + * For example: an EventStream track has its sample (Events) included directly in the DASH + * manifest file. + */ + private static final int CATEGORY_MANIFEST_EVENTS = 2; + public final int[] adaptationSetIndices; public final int trackType; - public final boolean isPrimary; + public @TrackGroupCategory final int trackGroupCategory; + public final int eventStreamGroupIndex; public final int primaryTrackGroupIndex; public final boolean hasEmbeddedEventMessageTrack; public final boolean hasEmbeddedCea608Track; - public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex, - boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) { - this.trackType = trackType; - this.adaptationSetIndices = adaptationSetIndices; - this.primaryTrackGroupIndex = primaryTrackGroupIndex; - this.isPrimary = isPrimary; - this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack; - this.hasEmbeddedCea608Track = hasEmbeddedCea608Track; + public static TrackGroupInfo primaryTrack(int trackType, int[] adaptationSetIndices, + int primaryTrackGroupIndex, boolean hasEmbeddedEventMessageTrack, + boolean hasEmbeddedCea608Track) { + return new TrackGroupInfo(trackType, CATEGORY_PRIMARY, adaptationSetIndices, + primaryTrackGroupIndex, hasEmbeddedEventMessageTrack, hasEmbeddedCea608Track, -1); } + public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices, + int primaryTrackGroupIndex) { + return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_EMBEDDED, + adaptationSetIndices, primaryTrackGroupIndex, false, false, -1); + } + + public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices, + int primaryTrackGroupIndex) { + return new TrackGroupInfo(C.TRACK_TYPE_TEXT, CATEGORY_EMBEDDED, + adaptationSetIndices, primaryTrackGroupIndex, false, false, -1); + } + + public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) { + return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_MANIFEST_EVENTS, + null, -1, false, false, eventStreamIndex); + } + + private TrackGroupInfo(int trackType, @TrackGroupCategory int trackGroupCategory, + int[] adaptationSetIndices, int primaryTrackGroupIndex, + boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track, + int eventStreamGroupIndex) { + this.trackType = trackType; + this.adaptationSetIndices = adaptationSetIndices; + this.trackGroupCategory = trackGroupCategory; + this.primaryTrackGroupIndex = primaryTrackGroupIndex; + this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack; + this.hasEmbeddedCea608Track = hasEmbeddedCea608Track; + this.eventStreamGroupIndex = eventStreamGroupIndex; + } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java new file mode 100644 index 0000000000..549bfdef7b --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2017 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.dash; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.dash.manifest.EventStream; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link SampleStream} consisting of serialized {@link EventMessage}s read from an + * {@link EventStream}. + */ +/* package */ final class EventSampleStream implements SampleStream { + + private final Format upstreamFormat; + private final EventMessageEncoder eventMessageEncoder; + + private long[] eventTimesUs; + private boolean eventStreamUpdatable; + private EventStream eventStream; + + private boolean isFormatSentDownstream; + private int currentIndex; + private long pendingSeekPositionUs; + + EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) { + this.upstreamFormat = upstreamFormat; + eventMessageEncoder = new EventMessageEncoder(); + pendingSeekPositionUs = C.TIME_UNSET; + updateEventStream(eventStream, eventStreamUpdatable); + } + + void updateEventStream(EventStream eventStream, boolean eventStreamUpdatable) { + long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1]; + + this.eventStreamUpdatable = eventStreamUpdatable; + this.eventStream = eventStream; + this.eventTimesUs = eventStream.presentationTimesUs; + if (pendingSeekPositionUs != C.TIME_UNSET) { + seekToUs(pendingSeekPositionUs); + } else if (lastReadPositionUs != C.TIME_UNSET) { + currentIndex = Util.binarySearchCeil(eventTimesUs, lastReadPositionUs, false, false); + } + } + + String eventStreamId() { + return eventStream.id(); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired) { + if (formatRequired || !isFormatSentDownstream) { + formatHolder.format = upstreamFormat; + isFormatSentDownstream = true; + return C.RESULT_FORMAT_READ; + } + if (currentIndex == eventTimesUs.length) { + if (!eventStreamUpdatable) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + int sampleIndex = currentIndex++; + byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex], + eventStream.timescale, eventTimesUs[sampleIndex]); + if (serializedEvent != null) { + buffer.ensureSpaceForWrite(serializedEvent.length); + buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); + buffer.data.put(serializedEvent); + buffer.timeUs = eventTimesUs[sampleIndex]; + return C.RESULT_BUFFER_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + @Override + public int skipData(long positionUs) { + int newIndex = + Math.max(currentIndex, Util.binarySearchCeil(eventTimesUs, positionUs, true, false)); + int skipped = newIndex - currentIndex; + currentIndex = newIndex; + return skipped; + } + + /** + * Seeks to the specified position in microseconds. + * + * @param positionUs The seek position in microseconds. + */ + public void seekToUs(long positionUs) { + currentIndex = Util.binarySearchCeil(eventTimesUs, positionUs, true, false); + boolean isPendingSeek = eventStreamUpdatable && currentIndex == eventTimesUs.length; + pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET; + } + +} 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 cd02e27fce..1ab94ccd30 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 @@ -107,7 +107,9 @@ public class DashManifest { Period period = getPeriod(periodIndex); ArrayList copyAdaptationSets = copyAdaptationSets(period.adaptationSets, keys); - copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); + Period copiedPeriod = new Period(period.id, period.startMs - shiftMs, copyAdaptationSets, + period.eventStreams); + copyPeriods.add(copiedPeriod); } } long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; 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 72df69f7e9..97ea07e065 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 @@ -20,12 +20,14 @@ import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.util.Pair; +import android.util.Xml; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement; @@ -36,6 +38,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.XmlPullParserUtil; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -47,6 +50,7 @@ import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; /** * A parser of media presentation description files. @@ -197,6 +201,7 @@ public class DashManifestParser extends DefaultHandler long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); SegmentBase segmentBase = null; List adaptationSets = new ArrayList<>(); + List eventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { xpp.next(); @@ -207,6 +212,8 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase)); + } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { + eventStreams.add(parseEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { @@ -216,11 +223,12 @@ public class DashManifestParser extends DefaultHandler } } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); - return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs); + return Pair.create(buildPeriod(id, startMs, adaptationSets, eventStreams), durationMs); } - protected Period buildPeriod(String id, long startMs, List adaptationSets) { - return new Period(id, startMs, adaptationSets); + protected Period buildPeriod(String id, long startMs, List adaptationSets, + List eventStreams) { + return new Period(id, startMs, adaptationSets, eventStreams); } // AdaptationSet parsing. @@ -387,7 +395,7 @@ public class DashManifestParser extends DefaultHandler Log.w(TAG, "Skipping malformed cenc:pssh data"); data = null; } - } else if (uuid == C.PLAYREADY_UUID && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + } else if (C.PLAYREADY_UUID.equals(uuid) && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") && xpp.next() == XmlPullParser.TEXT) { // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, @@ -660,6 +668,143 @@ public class DashManifestParser extends DefaultHandler startNumber, duration, timeline, initializationTemplate, mediaTemplate); } + /** + * /** + * Parses a single EventStream node in the manifest. + *

      + * @param xpp The current xml parser. + * @return The {@link EventStream} parsed from this EventStream node. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected EventStream parseEventStream(XmlPullParser xpp) + throws XmlPullParserException, IOException { + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); + String value = parseString(xpp, "value", ""); + long timescale = parseLong(xpp, "timescale", 1); + List> timedEvents = new ArrayList<>(); + ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512); + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Event")) { + Pair timedEvent = parseEvent(xpp, schemeIdUri, value, timescale, + scratchOutputStream); + timedEvents.add(timedEvent); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream")); + + long[] presentationTimesUs = new long[timedEvents.size()]; + EventMessage[] events = new EventMessage[timedEvents.size()]; + for (int i = 0; i < timedEvents.size(); i++) { + Pair timedEvent = timedEvents.get(i); + presentationTimesUs[i] = timedEvent.first; + events[i] = timedEvent.second; + } + return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events); + } + + protected EventStream buildEventStream(String schemeIdUri, String value, long timescale, + long[] presentationTimesUs, EventMessage[] events) { + return new EventStream(schemeIdUri, value, timescale, presentationTimesUs, events); + } + + /** + * Parses a single Event node in the manifest. + *

      + * @param xpp The current xml parser. + * @param schemeIdUri The schemeIdUri of the parent EventStream. + * @param value The schemeIdUri of the parent EventStream. + * @param timescale The timescale of the parent EventStream. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize data + * in between and tags into. + * @return The {@link EventStream} parsed from this EventStream node. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected Pair parseEvent(XmlPullParser xpp, String schemeIdUri, String value, + long timescale, ByteArrayOutputStream scratchOutputStream) + throws IOException, XmlPullParserException { + long id = parseLong(xpp, "id", 0); + long duration = parseLong(xpp, "duration", C.TIME_UNSET); + long presentationTime = parseLong(xpp, "presentationTime", 0); + long durationMs = Util.scaleLargeTimestamp(duration, 1000, timescale); + long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, + timescale); + byte[] eventObject = parseEventObject(xpp, scratchOutputStream); + return new Pair<>(presentationTimesUs, buildEvent(schemeIdUri, value, id, durationMs, + eventObject)); + } + + /** + * Parses everything between as a byte array string. + * + * @param xpp The current xml parser. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize byte + * array data into. + * @return The serialized byte array. + * @throws XmlPullParserException If there is any error parsing this node. + * @throws IOException If there is any error reading from the underlying input stream. + */ + protected byte[] parseEventObject(XmlPullParser xpp, ByteArrayOutputStream scratchOutputStream) + throws XmlPullParserException, IOException { + scratchOutputStream.reset(); + XmlSerializer xmlSerializer = Xml.newSerializer(); + xmlSerializer.setOutput(scratchOutputStream, null); + // Start reading everything between and , and serialize them into an Xml + // byte array. + xpp.nextToken(); + while (!XmlPullParserUtil.isEndTag(xpp, "Event")) { + switch (xpp.getEventType()) { + case (XmlPullParser.START_DOCUMENT): + xmlSerializer.startDocument(null, false); + break; + case (XmlPullParser.END_DOCUMENT): + xmlSerializer.endDocument(); + break; + case (XmlPullParser.START_TAG): + xmlSerializer.startTag(xpp.getNamespace(), xpp.getName()); + for (int i = 0; i < xpp.getAttributeCount(); i++) { + xmlSerializer.attribute(xpp.getAttributeNamespace(i), xpp.getAttributeName(i), + xpp.getAttributeValue(i)); + } + break; + case (XmlPullParser.END_TAG): + xmlSerializer.endTag(xpp.getNamespace(), xpp.getName()); + break; + case (XmlPullParser.TEXT): + xmlSerializer.text(xpp.getText()); + break; + case (XmlPullParser.CDSECT): + xmlSerializer.cdsect(xpp.getText()); + break; + case (XmlPullParser.ENTITY_REF): + xmlSerializer.entityRef(xpp.getText()); + break; + case (XmlPullParser.IGNORABLE_WHITESPACE): + xmlSerializer.ignorableWhitespace(xpp.getText()); + break; + case (XmlPullParser.PROCESSING_INSTRUCTION): + xmlSerializer.processingInstruction(xpp.getText()); + break; + case (XmlPullParser.COMMENT): + xmlSerializer.comment(xpp.getText()); + break; + case (XmlPullParser.DOCDECL): + xmlSerializer.docdecl(xpp.getText()); + break; + default: // fall out + } + xpp.nextToken(); + } + xmlSerializer.flush(); + return scratchOutputStream.toByteArray(); + } + + protected EventMessage buildEvent(String schemeIdUri, String value, long id, + long durationMs, byte[] messageData) { + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + } + protected List parseSegmentTimeline(XmlPullParser xpp) throws XmlPullParserException, IOException { List segmentTimeline = new ArrayList<>(); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java new file mode 100644 index 0000000000..8a4e1ad058 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/EventStream.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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.dash.manifest; + +import com.google.android.exoplayer2.metadata.emsg.EventMessage; + +/** + * A DASH in-MPD EventStream element, as defined by ISO/IEC 23009-1, 2nd edition, section 5.10. + */ +public final class EventStream { + + /** + * {@link EventMessage}s in the event stream. + */ + public final EventMessage[] events; + + /** + * Presentation time of the events in microsecond, sorted in ascending order. + */ + public final long[] presentationTimesUs; + + /** + * The scheme URI. + */ + public final String schemeIdUri; + + /** + * The value of the event stream. Use empty string if not defined in manifest. + */ + public final String value; + + /** + * The timescale in units per seconds, as defined in the manifest. + */ + public final long timescale; + + public EventStream(String schemeIdUri, String value, long timescale, long[] presentationTimesUs, + EventMessage[] events) { + this.schemeIdUri = schemeIdUri; + this.value = value; + this.timescale = timescale; + this.presentationTimesUs = presentationTimesUs; + this.events = events; + } + + /** + * A constructed id of this {@link EventStream}. Equal to {@code schemeIdUri + "/" + value}. + */ + public String id() { + return schemeIdUri + "/" + value; + } + +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index 269a63b7a9..bb1dbdac5d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.Collections; import java.util.List; @@ -27,7 +28,7 @@ public class Period { /** * The period identifier, if one exists. */ - public final String id; + @Nullable public final String id; /** * The start time of the period in milliseconds. @@ -39,15 +40,32 @@ public class Period { */ public final List adaptationSets; + /** + * The event stream belonging to the period. + */ + public final List eventStreams; + /** * @param id The period identifier. May be null. * @param startMs The start time of the period in milliseconds. * @param adaptationSets The adaptation sets belonging to the period. */ - public Period(String id, long startMs, List adaptationSets) { + public Period(@Nullable String id, long startMs, List adaptationSets) { + this(id, startMs, adaptationSets, Collections.emptyList()); + } + + /** + * @param id The period identifier. May be null. + * @param startMs The start time of the period in milliseconds. + * @param adaptationSets The adaptation sets belonging to the period. + * @param eventStreams The {@link EventStream}s belonging to the period. + */ + public Period(@Nullable String id, long startMs, List adaptationSets, + List eventStreams) { this.id = id; this.startMs = startMs; this.adaptationSets = Collections.unmodifiableList(adaptationSets); + this.eventStreams = Collections.unmodifiableList(eventStreams); } /** From 6616e6c1a9cf1d96f863b2417fc509b3582d340e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 Nov 2017 03:01:21 -0800 Subject: [PATCH 026/417] Don't check if next period is prepared when ignoring renderer media clock. When the renderer media clock source read its stream to end but is not ready, this means one of two things. Either the next period is not prepared yet and we need to stop the renderers and buffer until it's prepared, or we are waiting for another track in the current period with a uneven (longer) duration. The second case was already covered by this if condition and uses the standalone clock instead to continue. The first case now also uses the standalone clock, but it doesn't make a difference, because both clocks are stopped and still synchronized. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175134975 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 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 f00a5ce02d..4f3ec508c9 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 @@ -510,7 +510,7 @@ import java.io.IOException; // durations. See: https://github.com/google/ExoPlayer/issues/1874. if (rendererMediaClockSource == null || rendererMediaClockSource.isEnded() || (!rendererMediaClockSource.isReady() - && rendererWaitingForNextStream(rendererMediaClockSource))) { + && rendererMediaClockSource.hasReadStreamToEnd())) { rendererPositionUs = standaloneMediaClock.getPositionUs(); } else { rendererPositionUs = rendererMediaClock.getPositionUs(); From 04baa42349a88d6d3c10be6cf9e8a8152ccc39e2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 Nov 2017 03:14:22 -0800 Subject: [PATCH 027/417] Add custom callbacks to allows seeks after dynamic playlist modifications. These callbacks are executed on the app thread after the corresponding timeline update was triggered. This ensures that seek operations see the updated timelines and are therefore valid, even if the seek is performed into a window which didn't exist before. GitHub:#3407 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175136187 --- .../DynamicConcatenatingMediaSourceTest.java | 226 ++++++++++++++++- .../DynamicConcatenatingMediaSource.java | 228 ++++++++++++++++-- 2 files changed, 428 insertions(+), 26 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 61eebbc15a..c0c5252751 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.source; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; @@ -40,6 +44,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; +import org.mockito.Mockito; /** * Unit tests for {@link DynamicConcatenatingMediaSource} @@ -50,6 +55,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private Timeline timeline; private boolean timelineUpdated; + private boolean customRunnableCalled; public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; @@ -371,6 +377,180 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + public void testCustomCallbackBeforePreparationAddSingle() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddMultiple() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddSingleWithIndex() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources(/* index */ 0, Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationRemove() throws InterruptedException { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.addMediaSource(createFakeMediaSource()); + + mediaSource.removeMediaSource(/* index */ 0, runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationMove() throws InterruptedException { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + verify(runnable).run(); + } + + public void testCustomCallbackAfterPreparationAddSingle() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), + runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationRemove() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + waitForTimelineUpdate(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationMove() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + } + }); + waitForTimelineUpdate(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, + runnable); + } + }); + waitForCustomRunnable(); + } + + private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() + throws InterruptedException { + HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); + handlerThread.start(); + Handler.Callback handlerCallback = Mockito.mock(Handler.Callback.class); + when(handlerCallback.handleMessage(any(Message.class))).thenReturn(false); + Handler handler = new Handler(handlerThread.getLooper(), handlerCallback); + final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + handler.post(new Runnable() { + @Override + public void run() { + prepareAndListenToTimelineUpdates(mediaSource); + } + }); + waitForTimelineUpdate(); + return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); + } + private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { @Override @@ -385,16 +565,41 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } private synchronized void waitForTimelineUpdate() throws InterruptedException { - long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS; + long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; while (!timelineUpdated) { wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= timeoutMs) { + if (System.currentTimeMillis() >= deadlineMs) { fail("No timeline update occurred within timeout."); } } timelineUpdated = false; } + private Runnable createCustomRunnable() { + return new Runnable() { + @Override + public void run() { + synchronized (DynamicConcatenatingMediaSourceTest.this) { + assertTrue(timelineUpdated); + timelineUpdated = false; + customRunnableCalled = true; + DynamicConcatenatingMediaSourceTest.this.notify(); + } + } + }; + } + + private synchronized void waitForCustomRunnable() throws InterruptedException { + long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; + while (!customRunnableCalled) { + wait(TIMEOUT_MS); + if (System.currentTimeMillis() >= deadlineMs) { + fail("No custom runnable call occurred within timeout."); + } + } + customRunnableCalled = false; + } + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { @@ -403,6 +608,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return sources; } + private static FakeMediaSource createFakeMediaSource() { + return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + } + private static FakeTimeline createFakeTimeline(int index) { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } @@ -429,6 +638,19 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + private static class DynamicConcatenatingMediaSourceAndHandler { + + public final DynamicConcatenatingMediaSource mediaSource; + public final Handler handler; + + public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, + Handler handler) { + this.mediaSource = mediaSource; + this.handler = handler; + } + + } + private static class LazyMediaSource implements MediaSource { private Listener listener; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 92a117ce4e..6bfa4047a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.NonNull; -import android.util.Pair; +import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -47,6 +49,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private static final int MSG_ADD_MULTIPLE = 1; private static final int MSG_REMOVE = 2; private static final int MSG_MOVE = 3; + private static final int MSG_ON_COMPLETION = 4; // Accessed on the app thread. private final List mediaSourcesPublic; @@ -95,7 +98,22 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(MediaSource mediaSource) { - addMediaSource(mediaSourcesPublic.size(), mediaSource); + addMediaSource(mediaSourcesPublic.size(), mediaSource, null); + } + + /** + * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. + *

      + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource(MediaSource mediaSource, + @Nullable Runnable actionOnCompletion) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); } /** @@ -109,11 +127,31 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(int index, MediaSource mediaSource) { + addMediaSource(index, mediaSource, null); + } + + /** + * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. + *

      + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource(int index, MediaSource mediaSource, + @Nullable Runnable actionOnCompletion) { Assertions.checkNotNull(mediaSource); Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource))); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, + new MessageData<>(index, mediaSource, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -127,7 +165,24 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * sources are added in the order in which they appear in this collection. */ public synchronized void addMediaSources(Collection mediaSources) { - addMediaSources(mediaSourcesPublic.size(), mediaSources); + addMediaSources(mediaSourcesPublic.size(), mediaSources, null); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on + * completion. + *

      + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources(Collection mediaSources, + @Nullable Runnable actionOnCompletion) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); } /** @@ -142,6 +197,24 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * sources are added in the order in which they appear in this collection. */ public synchronized void addMediaSources(int index, Collection mediaSources) { + addMediaSources(index, mediaSources, null); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. + *

      + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources(int index, Collection mediaSources, + @Nullable Runnable actionOnCompletion) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); @@ -149,7 +222,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - Pair.create(index, mediaSources))); + new MessageData<>(index, mediaSources, actionOnCompletion))); + } else if (actionOnCompletion != null){ + actionOnCompletion.run(); } } @@ -164,9 +239,28 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * range of 0 <= index < {@link #getSize()}. */ public synchronized void removeMediaSource(int index) { + removeMediaSource(index, null); + } + + /** + * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. + *

      + * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being + * removed should not be re-added. If you want to move the instance use + * {@link #moveMediaSource(int, int)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been removed from the playlist. + */ + public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index)); + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, + new MessageData<>(index, null, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -179,13 +273,31 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * range of 0 <= index < {@link #getSize()}. */ public synchronized void moveMediaSource(int currentIndex, int newIndex) { + moveMediaSource(currentIndex, newIndex, null); + } + + /** + * Moves an existing {@link MediaSource} within the playlist and executes a custom action on + * completion. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been moved. + */ + public synchronized void moveMediaSource(int currentIndex, int newIndex, + @Nullable Runnable actionOnCompletion) { if (currentIndex == newIndex) { return; } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - Pair.create(currentIndex, newIndex))); + new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -215,7 +327,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); preventListenerNotification = false; - maybeNotifyListener(); + maybeNotifyListener(null); } @Override @@ -263,31 +375,42 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override @SuppressWarnings("unchecked") public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_ON_COMPLETION) { + ((EventDispatcher) message).dispatchEvent(); + return; + } preventListenerNotification = true; + EventDispatcher actionOnCompletion; switch (messageType) { case MSG_ADD: { - Pair messageData = (Pair) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, 1); - addMediaSourceInternal(messageData.first, messageData.second); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, 1); + addMediaSourceInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_ADD_MULTIPLE: { - Pair> messageData = - (Pair>) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, messageData.second.size()); - addMediaSourcesInternal(messageData.first, messageData.second); + MessageData> messageData = + (MessageData>) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, + messageData.customData.size()); + addMediaSourcesInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_REMOVE: { - shuffleOrder = shuffleOrder.cloneAndRemove((Integer) message); - removeMediaSourceInternal((Integer) message); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); + removeMediaSourceInternal(messageData.index); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_MOVE: { - Pair messageData = (Pair) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.first); - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.second, 1); - moveMediaSourceInternal(messageData.first, messageData.second); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.customData, 1); + moveMediaSourceInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } default: { @@ -295,14 +418,18 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } preventListenerNotification = false; - maybeNotifyListener(); + maybeNotifyListener(actionOnCompletion); } - private void maybeNotifyListener() { + private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { if (!preventListenerNotification) { listener.onSourceInfoRefreshed(this, new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); + if (actionOnCompletion != null) { + player.sendMessages( + new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + } } } @@ -359,7 +486,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } mediaSourceHolder.isPrepared = true; - maybeNotifyListener(); + maybeNotifyListener(null); } private void removeMediaSourceInternal(int index) { @@ -407,6 +534,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl return index; } + /** + * Data class to hold playlist media sources together with meta data needed to process them. + */ private static final class MediaSourceHolder implements Comparable { public final MediaSource mediaSource; @@ -432,6 +562,47 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } + /** + * Can be used to dispatch a runnable on the thread the object was created on. + */ + private static final class EventDispatcher { + + public final Handler eventHandler; + public final Runnable runnable; + + public EventDispatcher(Runnable runnable) { + this.runnable = runnable; + this.eventHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() + : Looper.getMainLooper()); + } + + public void dispatchEvent() { + eventHandler.post(runnable); + } + + } + + /** + * Message used to post actions from app thread to playback thread. + */ + private static final class MessageData { + + public final int index; + public final CustomType customData; + public final @Nullable EventDispatcher actionOnCompletion; + + public MessageData(int index, CustomType customData, @Nullable Runnable actionOnCompletion) { + this.index = index; + this.actionOnCompletion = actionOnCompletion != null + ? new EventDispatcher(actionOnCompletion) : null; + this.customData = customData; + } + + } + + /** + * Timeline exposing concatenated timelines of playlist media sources. + */ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { private final int windowCount; @@ -514,6 +685,10 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the + * DeferredTimeline is used to keep the originally assigned first period ID. + */ private static final class DeferredTimeline extends Timeline { private static final Object DUMMY_ID = new Object(); @@ -582,6 +757,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } + /** + * Media period used for periods created from unprepared media sources exposed through + * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes + * available. + */ private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { public final MediaSource mediaSource; From 694bd997cb7fe64379fb02e9cc090ae9833697c1 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 9 Nov 2017 07:14:21 -0800 Subject: [PATCH 028/417] Re-enable index file store at the end of the SimpleCache.initialize() In the case converting cache files from an earlier version of SimpleCache, there is no previous version of the index file. If the app doesn't call any SimpleCache methods which would make the index file stored before it exists whole data gets lost. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175153650 --- .../android/exoplayer2/upstream/cache/SimpleCache.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 9739f21923..599474d6c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.File; @@ -31,6 +32,8 @@ import java.util.TreeSet; */ public final class SimpleCache implements Cache { + private static final String TAG = "SimpleCache"; + private final File cacheDir; private final CacheEvictor evictor; private final HashMap lockedSpans; @@ -288,7 +291,11 @@ public final class SimpleCache implements Cache { } index.removeEmpty(); - // Don't call index.store() here so initialization doesn't fail because of write issues. + try { + index.store(); + } catch (CacheException e) { + Log.e(TAG, "Storing index file failed", e); + } } /** From eb4e05199fa5d8a417916c9e3e2ca0638cfe0bbf Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 9 Nov 2017 09:58:18 -0800 Subject: [PATCH 029/417] Expose first index in SampleQueue This will be needed when retaining a back-buffer. Being able to query the first index allows us to work out when we've discarded all samples that were obtained from a particular chunk, which we'll use to determine when to remove chunks from ChunkSampleStream.mediaChunks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175172085 --- .../source/SampleMetadataQueue.java | 61 +++++++++++-------- .../exoplayer2/source/SampleQueue.java | 7 +++ .../exoplayer2/source/SampleQueueTest.java | 19 ++++++ 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index d70c59b195..65c443d425 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -51,8 +51,8 @@ import com.google.android.exoplayer2.util.Util; private Format[] formats; private int length; - private int absoluteStartIndex; - private int relativeStartIndex; + private int absoluteFirstIndex; + private int relativeFirstIndex; private int readPosition; private long largestDiscardedTimestampUs; @@ -87,8 +87,8 @@ import com.google.android.exoplayer2.util.Util; */ public void reset(boolean resetUpstreamFormat) { length = 0; - absoluteStartIndex = 0; - relativeStartIndex = 0; + absoluteFirstIndex = 0; + relativeFirstIndex = 0; readPosition = 0; upstreamKeyframeRequired = true; largestDiscardedTimestampUs = Long.MIN_VALUE; @@ -103,7 +103,7 @@ import com.google.android.exoplayer2.util.Util; * Returns the current absolute write index. */ public int getWriteIndex() { - return absoluteStartIndex + length; + return absoluteFirstIndex + length; } /** @@ -132,11 +132,18 @@ import com.google.android.exoplayer2.util.Util; // Called by the consuming thread. + /** + * Returns the current absolute start index. + */ + public int getFirstIndex() { + return absoluteFirstIndex; + } + /** * Returns the current absolute read index. */ public int getReadIndex() { - return absoluteStartIndex + readPosition; + return absoluteFirstIndex + readPosition; } /** @@ -297,11 +304,11 @@ import com.google.android.exoplayer2.util.Util; * {@link C#POSITION_UNSET} if no discarding of data is necessary. */ public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - if (length == 0 || timeUs < timesUs[relativeStartIndex]) { + if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { return C.POSITION_UNSET; } int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; - int discardCount = findSampleBefore(relativeStartIndex, searchLength, timeUs, toKeyframe); + int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); if (discardCount == -1) { return C.POSITION_UNSET; } @@ -382,15 +389,15 @@ import com.google.android.exoplayer2.util.Util; int[] newSizes = new int[newCapacity]; CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeStartIndex; - System.arraycopy(offsets, relativeStartIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeStartIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeStartIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeStartIndex, newSizes, 0, beforeWrap); - System.arraycopy(cryptoDatas, relativeStartIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeStartIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeStartIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeStartIndex; + int beforeWrap = capacity - relativeFirstIndex; + System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeFirstIndex; System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); @@ -405,7 +412,7 @@ import com.google.android.exoplayer2.util.Util; cryptoDatas = newCryptoDatas; formats = newFormats; sourceIds = newSourceIds; - relativeStartIndex = 0; + relativeFirstIndex = 0; length = capacity; capacity = newCapacity; } @@ -440,7 +447,7 @@ import com.google.android.exoplayer2.util.Util; relativeSampleIndex = capacity - 1; } } - discardUpstreamSamples(absoluteStartIndex + retainCount); + discardUpstreamSamples(absoluteFirstIndex + retainCount); return true; } @@ -454,7 +461,7 @@ import com.google.android.exoplayer2.util.Util; * @param length The length of the range being searched. * @param timeUs The specified time. * @param keyframe Whether only keyframes should be considered. - * @return The offset from {@code relativeStartIndex} to the found sample, or -1 if no matching + * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching * sample was found. */ private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { @@ -487,20 +494,20 @@ import com.google.android.exoplayer2.util.Util; largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount)); length -= discardCount; - absoluteStartIndex += discardCount; - relativeStartIndex += discardCount; - if (relativeStartIndex >= capacity) { - relativeStartIndex -= capacity; + absoluteFirstIndex += discardCount; + relativeFirstIndex += discardCount; + if (relativeFirstIndex >= capacity) { + relativeFirstIndex -= capacity; } readPosition -= discardCount; if (readPosition < 0) { readPosition = 0; } if (length == 0) { - int relativeLastDiscardIndex = (relativeStartIndex == 0 ? capacity : relativeStartIndex) - 1; + int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; } else { - return offsets[relativeStartIndex]; + return offsets[relativeFirstIndex]; } } @@ -537,7 +544,7 @@ import com.google.android.exoplayer2.util.Util; * @param offset The offset, which must be in the range [0, length]. */ private int getRelativeIndex(int offset) { - int relativeIndex = relativeStartIndex + offset; + int relativeIndex = relativeFirstIndex + offset; return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; } 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 b83cf7df5b..78b16bf377 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 @@ -181,6 +181,13 @@ public final class SampleQueue implements TrackOutput { return metadataQueue.hasNextSample(); } + /** + * Returns the absolute index of the first sample. + */ + public int getFirstIndex() { + return metadataQueue.getFirstIndex(); + } + /** * Returns the current absolute read index. */ 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 49983fae30..6030238131 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 @@ -215,12 +215,14 @@ public final class SampleQueueTest { public void testReadMultiWithRewind() { writeTestData(); assertReadTestData(); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(10); // Rewind. sampleQueue.rewind(); assertAllocationCount(10); // Read again. + assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertReadTestData(); } @@ -230,11 +232,14 @@ public final class SampleQueueTest { writeTestData(); assertReadTestData(); sampleQueue.discardToRead(); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(8); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(0); // Rewind. sampleQueue.rewind(); assertAllocationCount(0); // Can't read again. + assertThat(sampleQueue.getFirstIndex()).isEqualTo(8); assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertReadEndOfStream(false); } @@ -332,6 +337,7 @@ public final class SampleQueueTest { writeTestData(); // Should discard everything. sampleQueue.discardToEnd(); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(8); assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(0); // We should still be able to read the upstream format. @@ -346,28 +352,39 @@ public final class SampleQueueTest { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertAllocationCount(10); // Read the first sample. assertReadTestData(null, 0, 1); // Shouldn't discard anything. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(10); // Should discard the read sample. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(1); + assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(9); // Shouldn't discard anything. sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(1); + assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(9); // Should be able to read the remaining samples. assertReadTestData(TEST_FORMAT_1, 1, 7); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(1); assertThat(sampleQueue.getReadIndex()).isEqualTo(8); // Should discard up to the second last sample sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(6); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(3); // Should discard up the last sample sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(7); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(1); } @@ -376,10 +393,12 @@ public final class SampleQueueTest { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertAllocationCount(10); // Should discard the first sample. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false); + assertThat(sampleQueue.getFirstIndex()).isEqualTo(1); assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(9); // Should be able to read the remaining samples. From 735af5c0b0ad9466c040ad222cf69dadca63ff43 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Thu, 9 Nov 2017 10:47:16 -0800 Subject: [PATCH 030/417] Update several minor code/test style issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175180089 --- .../metadata/emsg/EventMessageEncoder.java | 8 +- .../emsg/EventMessageDecoderTest.java | 60 ------------ .../emsg/EventMessageEncoderTest.java | 95 +++++++++++++++++++ .../assets/sample_mpd_4_event_stream | 18 +--- .../dash/manifest/DashManifestParserTest.java | 1 + 5 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java index 2ddbfb4708..2bd54367e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -61,15 +61,11 @@ public final class EventMessageEncoder { writeUnsignedInt(dataOutputStream, duration); writeUnsignedInt(dataOutputStream, eventMessage.id); dataOutputStream.write(eventMessage.messageData); + dataOutputStream.flush(); return byteArrayOutputStream.toByteArray(); } catch (IOException e) { + // Should never happen. throw new RuntimeException(e); - } finally { - try { - dataOutputStream.close(); - } catch (IOException ignored) { - // ignored - } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index f7f0c63300..1ce0ccb93d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import java.io.IOException; import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,63 +55,4 @@ public final class EventMessageDecoderTest { assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); } - @Test - public void testEncodeEventStream() throws IOException { - EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 - 0, 2, 50, -128, // event_duration = 144000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} - byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000, 1000000); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); - } - - @Test - public void testEncodeDecodeEventStream() throws IOException { - EventMessage expectedEmsg = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); - byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000, 1); - MetadataInputBuffer buffer = new MetadataInputBuffer(); - buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); - - EventMessageDecoder decoder = new EventMessageDecoder(); - Metadata metadata = decoder.decode(buffer); - assertThat(metadata.length()).isEqualTo(1); - assertThat(metadata.get(0)).isEqualTo(expectedEmsg); - } - - @Test - public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { - EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); - byte[] expectedEmsgBody = new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 - 0, 2, 50, -128, // event_duration = 144000 - 0, 15, 67, -45, // id = 1000403 - 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} - EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, - new byte[] {4, 3, 2, 1, 0}); - byte[] expectedEmsgBody1 = new byte[] { - 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" - 49, 50, 51, 0, // value = "123" - 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 - 0, 2, 50, -128, // event_duration = 144000 - 0, 15, 67, -46, // id = 1000402 - 4, 3, 2, 1, 0}; // message_data = {4, 3, 2, 1, 0} - EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); - byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000, 1000000); - assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); - byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000, 1000000); - assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java new file mode 100644 index 0000000000..f526fc3451 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 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.metadata.emsg; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link EventMessageEncoder}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class EventMessageEncoderTest { + + @Test + public void testEncodeEventStream() throws IOException { + EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, + new byte[] {0, 1, 2, 3, 4}); + byte[] expectedEmsgBody = new byte[] { + 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" + 49, 50, 51, 0, // value = "123" + 0, 0, -69, -128, // timescale = 48000 + 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 2, 50, -128, // event_duration = 144000 + 0, 15, 67, -45, // id = 1000403 + 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000, 1000000); + assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + } + + @Test + public void testEncodeDecodeEventStream() throws IOException { + EventMessage expectedEmsg = new EventMessage("urn:test", "123", 3000, 1000403, + new byte[] {0, 1, 2, 3, 4}); + byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000, 1); + MetadataInputBuffer buffer = new MetadataInputBuffer(); + buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); + + EventMessageDecoder decoder = new EventMessageDecoder(); + Metadata metadata = decoder.decode(buffer); + assertThat(metadata.length()).isEqualTo(1); + assertThat(metadata.get(0)).isEqualTo(expectedEmsg); + } + + @Test + public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { + EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, + new byte[] {0, 1, 2, 3, 4}); + byte[] expectedEmsgBody = new byte[] { + 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" + 49, 50, 51, 0, // value = "123" + 0, 0, -69, -128, // timescale = 48000 + 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 2, 50, -128, // event_duration = 144000 + 0, 15, 67, -45, // id = 1000403 + 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} + EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, + new byte[] {4, 3, 2, 1, 0}); + byte[] expectedEmsgBody1 = new byte[] { + 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" + 49, 50, 51, 0, // value = "123" + 0, 0, -69, -128, // timescale = 48000 + 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 2, 50, -128, // event_duration = 144000 + 0, 15, 67, -46, // id = 1000402 + 4, 3, 2, 1, 0}; // message_data = {4, 3, 2, 1, 0} + EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); + byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000, 1000000); + assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); + byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000, 1000000); + assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); + } + +} diff --git a/library/dash/src/androidTest/assets/sample_mpd_4_event_stream b/library/dash/src/androidTest/assets/sample_mpd_4_event_stream index 4352205d3f..cf96541ca0 100644 --- a/library/dash/src/androidTest/assets/sample_mpd_4_event_stream +++ b/library/dash/src/androidTest/assets/sample_mpd_4_event_stream @@ -27,33 +27,19 @@ - - - http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + http://www.dummy.url/ - http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ - - - http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ - - - http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ - - - http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ - - - http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/ + http://www.dummy.url/ diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index b24a7da878..37dc6a748e 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -79,6 +79,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { Period period = mpd.getPeriod(0); assertEquals(3, period.eventStreams.size()); + // assert text-only event stream EventStream eventStream1 = period.eventStreams.get(0); assertEquals(1, eventStream1.events.length); From d2517b258841c32a7b7b2cda9b3bc2324e805946 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 Nov 2017 08:55:50 -0800 Subject: [PATCH 031/417] Add javadoc to ExoPlayerTestRunner. Someone must have forgotten to do this when rewriting this class. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175296249 --- .../testutil/ExoPlayerTestRunner.java | 167 +++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index a87066415d..a1f8fc7861 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -62,14 +62,30 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public interface PlayerFactory { + /** + * Creates a new {@link SimpleExoPlayer} using the provided renderers factory, track selector, + * and load control. + * + * @param renderersFactory A {@link RenderersFactory} to be used for the new player. + * @param trackSelector A {@link MappingTrackSelector} to be used for the new player. + * @param loadControl A {@link LoadControl} to be used for the new player. + * @return A new {@link SimpleExoPlayer}. + */ SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, MappingTrackSelector trackSelector, LoadControl loadControl); } + /** + * A generic video {@link Format} which can be used to set up media sources and renderers. + */ public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); + + /** + * A generic audio {@link Format} which can be used to set up media sources and renderers. + */ public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -85,19 +101,45 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ActionSchedule actionSchedule; private Player.EventListener eventListener; + /** + * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The + * default value is a non-seekable, non-dynamic {@link FakeTimeline} with zero duration. Setting + * the timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * + * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test + * runner. + * @return This builder. + */ public Builder setTimeline(Timeline timeline) { Assert.assertNull(mediaSource); this.timeline = timeline; return this; } + /** + * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value + * is null. Setting the manifest is not allowed after a call to + * {@link #setMediaSource(MediaSource)}. + * + * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. + * @return This builder. + */ public Builder setManifest(Object manifest) { Assert.assertNull(mediaSource); this.manifest = manifest; return this; } - /** Replaces {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. */ + /** + * Sets a {@link MediaSource} to be used by the test runner. The default value is a + * {@link FakeMediaSource} with the timeline and manifest provided by + * {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. Setting the media source is + * not allowed after calls to {@link #setTimeline(Timeline)} and/or + * {@link #setManifest(Object)}. + * + * @param mediaSource A {@link MediaSource} to be used by the test runner. + * @return This builder. + */ public Builder setMediaSource(MediaSource mediaSource) { Assert.assertNull(timeline); Assert.assertNull(manifest); @@ -105,49 +147,118 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Sets a {@link MappingTrackSelector} to be used by the test runner. The default value is a + * {@link DefaultTrackSelector}. + * + * @param trackSelector A {@link MappingTrackSelector} to be used by the test runner. + * @return This builder. + */ public Builder setTrackSelector(MappingTrackSelector trackSelector) { this.trackSelector = trackSelector; return this; } + /** + * Sets a {@link LoadControl} to be used by the test runner. The default value is a + * {@link DefaultLoadControl}. + * + * @param loadControl A {@link LoadControl} to be used by the test runner. + * @return This builder. + */ public Builder setLoadControl(LoadControl loadControl) { this.loadControl = loadControl; return this; } + /** + * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media + * periods and for setting up a {@link FakeRenderer}. The default value is a single + * {@link #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media + * source with {@link #setMediaSource(MediaSource)} and renderers with + * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. + * + * @param supportedFormats A list of supported {@link Format}s. + * @return This builder. + */ public Builder setSupportedFormats(Format... supportedFormats) { this.supportedFormats = supportedFormats; return this; } + /** + * Sets the {@link Renderer}s to be used by the test runner. The default value is a single + * {@link FakeRenderer} supporting the formats set by {@link #setSupportedFormats(Format...)}. + * Setting the renderers is not allowed after a call to + * {@link #setRenderersFactory(RenderersFactory)}. + * + * @param renderers A list of {@link Renderer}s to be used by the test runner. + * @return This builder. + */ public Builder setRenderers(Renderer... renderers) { Assert.assertNull(renderersFactory); this.renderers = renderers; return this; } - /** Replaces {@link #setRenderers(Renderer...)}. */ + /** + * Sets the {@link RenderersFactory} to be used by the test runner. The default factory creates + * all renderers set by {@link #setRenderers(Renderer...)}. Setting the renderer factory is not + * allowed after a call to {@link #setRenderers(Renderer...)}. + * + * @param renderersFactory A {@link RenderersFactory} to be used by the test runner. + * @return This builder. + */ public Builder setRenderersFactory(RenderersFactory renderersFactory) { Assert.assertNull(renderers); this.renderersFactory = renderersFactory; return this; } + /** + * Sets the {@link PlayerFactory} which creates the {@link SimpleExoPlayer} to be used by the + * test runner. The default value is a {@link SimpleExoPlayer} with the renderers provided by + * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)}, the + * track selector provided by {@link #setTrackSelector(MappingTrackSelector)} and the load + * control provided by {@link #setLoadControl(LoadControl)}. + * + * @param playerFactory A {@link PlayerFactory} to create the player. + * @return This builder. + */ public Builder setExoPlayer(PlayerFactory playerFactory) { this.playerFactory = playerFactory; return this; } + /** + * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be + * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. + * + * @param actionSchedule An {@link ActionSchedule} to be used by the test runner. + * @return This builder. + */ public Builder setActionSchedule(ActionSchedule actionSchedule) { this.actionSchedule = actionSchedule; return this; } + /** + * Sets an {@link Player.EventListener} to be registered to listen to player events. + * + * @param eventListener A {@link Player.EventListener} to be registered by the test runner to + * listen to player events. + * @return This builder. + */ public Builder setEventListener(Player.EventListener eventListener) { this.eventListener = eventListener; return this; } + /** + * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults. + * + * @return The built {@link ExoPlayerTestRunner}. + */ public ExoPlayerTestRunner build() { if (supportedFormats == null) { supportedFormats = new Format[] { VIDEO_FORMAT }; @@ -234,6 +345,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Called on the test thread to run the test. + /** + * Starts the test runner on its own thread. This will trigger the creation of the player, the + * listener registration, the start of the action schedule, and the preparation of the player + * with the provided media source. + * + * @return This test runner. + */ public ExoPlayerTestRunner start() { handler.post(new Runnable() { @Override @@ -257,6 +375,16 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Blocks the current thread until the test runner finishes. A test is deemed to be finished when + * the playback state transitions to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE}, or + * when am {@link ExoPlaybackException} is thrown. + * + * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed + * the method will throw a {@link TimeoutException}. + * @return This test runner. + * @throws Exception If any exception occurred during playback, release, or due to a timeout. + */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); @@ -271,6 +399,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Assertions called on the test thread after test finished. + /** + * Asserts that the timelines reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * timelines. + * + * @param timelines A list of expected {@link Timeline}s. + */ public void assertTimelinesEqual(Timeline... timelines) { Assert.assertEquals(timelines.length, this.timelines.size()); for (Timeline timeline : timelines) { @@ -278,6 +413,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the manifests reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * manifest. + * + * @param manifests A list of expected manifests. + */ public void assertManifestsEqual(Object... manifests) { Assert.assertEquals(manifests.length, this.manifests.size()); for (Object manifest : manifests) { @@ -285,14 +427,35 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the last track group array reported by + * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to + * the provided track group array. + * + * @param trackGroupArray The expected {@link TrackGroupArray}. + */ public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { Assert.assertEquals(trackGroupArray, this.trackGroups); } + /** + * Asserts that the number of reported discontinuities by + * {@link Player.EventListener#onPositionDiscontinuity(int)} is equal to the provided number. + * + * @param expectedCount The expected number of position discontinuities. + */ public void assertPositionDiscontinuityCount(int expectedCount) { Assert.assertEquals(expectedCount, positionDiscontinuityCount); } + /** + * Asserts that the indices of played periods is equal to the provided list of periods. A period + * is considered to be played if it was the current period after a position discontinuity or a + * media source preparation. When the same period is repeated automatically due to enabled repeat + * modes, it is reported twice. Seeks within the current period are not reported. + * + * @param periodIndices A list of expected period indices. + */ public void assertPlayedPeriodIndices(int... periodIndices) { Assert.assertEquals(periodIndices.length, this.periodIndices.size()); for (int periodIndex : periodIndices) { From 0be4b46bb486443a3bddd8a9c7e772cc0053ff0f Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 07:28:57 -0800 Subject: [PATCH 032/417] Introduce Builder pattern to create MediaSource. Start with DASH MediaSource. The number of injected arguments is getting out-of-control. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175529031 --- .../exoplayer2/castdemo/PlayerManager.java | 6 +- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 147 ++++++++++++++++++ .../playbacktests/gts/DashTestRunner.java | 7 +- 4 files changed, 161 insertions(+), 6 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 77dc018a73..ae0fdadfc9 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -160,8 +160,10 @@ import com.google.android.gms.cast.framework.CastContext; return new SsMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource(uri, DATA_SOURCE_FACTORY, - new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); + return DashMediaSource.Builder + .forManifestUri(uri, DATA_SOURCE_FACTORY, + new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY)) + .build(); case DemoUtil.MIME_TYPE_HLS: return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null); case DemoUtil.MIME_TYPE_VIDEO_MP4: 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 08c5bddb09..614626077a 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 @@ -365,8 +365,11 @@ public class PlayerActivity extends Activity implements OnClickListener, return new SsMediaSource(uri, buildDataSourceFactory(false), new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); case C.TYPE_DASH: - return new DashMediaSource(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return DashMediaSource.Builder + .forManifestUri(uri, buildDataSourceFactory(false), + new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_HLS: return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); case C.TYPE_OTHER: 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 c529fcab4b..3d5a9c393d 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -114,6 +115,142 @@ public final class DashMediaSource implements MediaSource { private int firstPeriodId; + /** + * Builder for {@link DashMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final DashManifest manifest; + private final Uri manifestUri; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; + + private ParsingLoadable.Parser manifestParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + + private int minLoadableRetryCount; + private long livePresentationDelayMs; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @return A new builder. + */ + public static Builder forSideLoadedManifest(DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory) { + return new Builder(manifest, null, null, chunkSourceFactory); + } + + /** + * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @return A new builder. + */ + public static Builder forManifestUri(Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { + return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); + } + + private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory) { + this.manifest = manifest; + this.manifestUri = manifestUri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the duration in milliseconds by which the default start position should precede the end + * of the live window for live playbacks. The default value is + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. Use + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by + * the manifest, if present. + * @return This builder. + */ + public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + this.livePresentationDelayMs = livePresentationDelayMs; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the manifest parser to parse loaded manifest data. The default is + * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * + * @param manifestParser A parser for loaded manifest data. + * @return This builder. + */ + public Builder setManifestParser( + ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + + /** + * Builds a new {@link DashMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link DashMediaSource}. + */ + public DashMediaSource build() { + Assertions.checkArgument(manifest == null || !manifest.dynamic); + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + boolean loadableManifestUri = manifestUri != null; + if (loadableManifestUri && manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, + chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + eventListener); + } + + } + /** * Constructs an instance to play a given {@link DashManifest}, which must be static. * @@ -121,7 +258,9 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, @@ -136,7 +275,9 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -154,7 +295,9 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -178,7 +321,9 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, @@ -203,7 +348,9 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 85cefbc2f6..215d8a0518 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,8 +316,11 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + return DashMediaSource.Builder + .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) + .setLivePresentationDelayMs(0) + .build(); } @Override From e92667ba5b58cbda9695c8168d3f8fa810da8288 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 13 Nov 2017 17:28:25 +0000 Subject: [PATCH 033/417] Only set shutter color if attr is declared --- .../exoplayer2/ui/SimpleExoPlayerView.java | 22 +++++++++++-------- library/ui/src/main/res/values/attrs.xml | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 86fe7a3f7f..b09e80c591 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -83,12 +83,6 @@ import java.util.List; *

    • Default: {@code true}
    • *
    *
  • - *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} view. - *
      - *
    • Corresponding method: {@link #setShutterBackgroundColor(int)}
    • - *
    • Default: {@code 0}
    • - *
    - *
  • *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. *
      *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
    • @@ -119,6 +113,13 @@ import java.util.List; *
    • Default: {@code surface_view}
    • *
    *
  • + *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} + * view. + *
      + *
    • Corresponding method: {@link #setShutterBackgroundColor(int)}
    • + *
    • Default: {@code unset}
    • + *
    + *
  • *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
      @@ -255,6 +256,7 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } + boolean shutterColorSet = false; int shutterColor = 0; int playerLayoutId = R.layout.exo_simple_player_view; boolean useArtwork = true; @@ -269,7 +271,9 @@ public final class SimpleExoPlayerView extends FrameLayout { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { - shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); + shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); + shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, + shutterColor); playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); @@ -301,7 +305,7 @@ public final class SimpleExoPlayerView extends FrameLayout { // Shutter view. shutterView = findViewById(R.id.exo_shutter); - if (shutterView != null) { + if (shutterView != null && shutterColorSet) { shutterView.setBackgroundColor(shutterColor); } @@ -527,7 +531,7 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Sets the background color of the {@code exo_shutter} view. * - * @param color A resolved color (not a resource ID) for the background of the shutter view. + * @param color The background color. */ public void setShutterBackgroundColor(int color) { if (shutterView != null) { diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index eb9edaccdc..525f95768c 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -43,7 +43,7 @@ - + From 4787ffac9398672cd211e0d7575ac98af8cccb95 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 09:31:52 -0800 Subject: [PATCH 034/417] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175542973 --- extensions/tv/README.md | 41 -------------------------------------- extensions/tv/build.gradle | 39 ------------------------------------ 2 files changed, 80 deletions(-) delete mode 100644 extensions/tv/README.md delete mode 100644 extensions/tv/build.gradle diff --git a/extensions/tv/README.md b/extensions/tv/README.md deleted file mode 100644 index 0deb33794f..0000000000 --- a/extensions/tv/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# ExoPlayer TV tuner extension # - -Provides components for broadcast TV playback with ExoPlayer. - -## Links ## - -* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.tv.*` - belong to this extension. - -[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html - -## Build Instructions ## - -* Checkout ExoPlayer: - -``` -git clone https://github.com/google/ExoPlayer.git -``` - -* Set the following environment variables: - -``` -cd "" -EXOPLAYER_ROOT="$(pwd)" -TV_MODULE_PATH="${EXOPLAYER_ROOT}/extensions/tv/src/main" -``` - -* Download the [Android NDK][] and set its location in an environment variable: - -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - -``` -NDK_PATH="" -``` - -* Build the JNI native libraries from the command line: - -``` -cd "${TV_MODULE_PATH}"/jni && \ -${NDK_PATH}/ndk-build APP_ABI=all -j -``` diff --git a/extensions/tv/build.gradle b/extensions/tv/build.gradle deleted file mode 100644 index ee54926650..0000000000 --- a/extensions/tv/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2017 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. -apply from: '../../constants.gradle' -apply plugin: 'com.android.library' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion 21 - targetSdkVersion project.ext.targetSdkVersion - } - - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. - } -} - -dependencies { - compile project(modulePrefix + 'library-core') -} - -ext { - javadocTitle = 'TV tuner extension' -} -apply from: '../../javadoc_library.gradle' From 96a856c0b54ea5c578234fef4fa09b1eb83d80b9 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 09:35:49 -0800 Subject: [PATCH 035/417] Drop the 'r' from release version. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175543465 --- RELEASENOTES.md | 3 ++- constants.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9bc7005ffc..6e6ed28077 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,8 @@ # Release notes # -### r2.6.0 ### +### 2.6.0 ### +* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". * New `Player.DefaultEventListener` abstract class can be extended to avoid having to implement all methods defined by `Player.EventListener`. * Added a reason to `EventListener.onPositionDiscontinuity` diff --git a/constants.gradle b/constants.gradle index 644d47b8aa..2a7754d65c 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.6.0' + releaseVersion = '2.6.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix From d50f7317a282d4f13dd8832c2aa924e6e7516e91 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 10:01:30 -0800 Subject: [PATCH 036/417] Update 2.6.0 release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175546817 --- RELEASENOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6e6ed28077..d6b20be4e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,9 @@ to key-frames. This is particularly relevant for variable speed playbacks. * Allow `SingleSampleMediaSource` to suppress load errors ([#3140](https://github.com/google/ExoPlayer/issues/3140)). +* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked + after a dynamic playlist modification has been applied + ([#3407](https://github.com/google/ExoPlayer/issues/3407)). * Audio: New `AudioSink` interface allows customization of audio output path. * Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming and progressive streams. From db7e9a548c019828302a17fb363c01fc0b07a6f4 Mon Sep 17 00:00:00 2001 From: arnaudberry Date: Mon, 13 Nov 2017 10:49:09 -0800 Subject: [PATCH 037/417] Make it possible to extend DashManifestParser to parse revision-id. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175554723 --- .../source/dash/manifest/DashManifestParser.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 97ea07e065..eb9a849563 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 @@ -496,7 +496,7 @@ public class DashManifestParser extends DefaultHandler segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, - inbandEventStreams); + inbandEventStreams, Representation.REVISION_ID_DEFAULT); } protected Format buildFormat(String id, String containerMimeType, int width, int height, @@ -543,7 +543,7 @@ public class DashManifestParser extends DefaultHandler } ArrayList inbandEventStreams = representationInfo.inbandEventStreams; inbandEventStreams.addAll(extraInbandEventStreams); - return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, + return Representation.newInstance(contentId, representationInfo.revisionId, format, representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStreams); } @@ -1131,7 +1131,8 @@ public class DashManifestParser extends DefaultHandler } } - private static final class RepresentationInfo { + /** A parsed Representation element. */ + protected static final class RepresentationInfo { public final Format format; public final String baseUrl; @@ -1139,16 +1140,18 @@ public class DashManifestParser extends DefaultHandler public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; + public final long revisionId; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams) { + ArrayList inbandEventStreams, long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; this.inbandEventStreams = inbandEventStreams; + this.revisionId = revisionId; } } From 67bbbed9fec6860fd267b1c13ad7ed244915b21d Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 11:10:54 -0800 Subject: [PATCH 038/417] Notify TrackSelection when it's enabled and disabled. Add onEnable() and onDisable() call-backs to TrackSelection. This allows TrackSelection to perform interesting operations (like subscribe to NetworkStatus) and clean up after itself. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175558485 --- .../android/exoplayer2/ExoPlayerTest.java | 141 ++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 33 +++- .../trackselection/BaseTrackSelection.java | 10 ++ .../trackselection/TrackSelection.java | 20 ++- .../testutil/FakeTrackSelection.java | 132 ++++++++++++++++ .../testutil/FakeTrackSelector.java | 86 +++++++++++ 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 56d5f05d00..0edd19bc09 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.FakeTrackSelection; +import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -311,4 +313,143 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made once (1 period). + // Track selections are not reused, so there are 2 track selections made. + assertEquals(2, createdTrackSelections.size()); + // There should be 2 track selections enabled in total. + assertEquals(2, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), + new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice (2 periods). + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // There should be 4 track selections enabled in total. + assertEquals(4, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // TrackSelections are reused, so there are only 2 track selections made for 2 renderers. + assertEquals(2, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + } 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 4f3ec508c9..bcbeed437f 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 @@ -1652,11 +1652,11 @@ import java.io.IOException; // Undo the effect of previous call to associate no-sample renderers with empty tracks // so the mediaPeriod receives back whatever it sent us before. disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); + updatePeriodTrackSelectorResult(trackSelectorResult); // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); associateNoSampleRenderersWithEmptySampleStream(sampleStreams); - periodTrackSelectorResult = trackSelectorResult; // Update whether we have enabled tracks and sanity check the expected streams are non-null. hasEnabledTracks = false; @@ -1678,6 +1678,7 @@ import java.io.IOException; } public void release() { + updatePeriodTrackSelectorResult(null); try { if (info.endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); @@ -1690,6 +1691,36 @@ import java.io.IOException; } } + private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + if (periodTrackSelectorResult != null) { + disableTrackSelectionsInResult(periodTrackSelectorResult); + } + periodTrackSelectorResult = trackSelectorResult; + if (periodTrackSelectorResult != null) { + enableTrackSelectionsInResult(periodTrackSelectorResult); + } + } + + private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.enable(); + } + } + } + + private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.disable(); + } + } + } + /** * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy * {@link EmptySampleStream} that was associated with it. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 054ee7973f..6bc6afb88b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -78,6 +78,16 @@ public abstract class BaseTrackSelection implements TrackSelection { blacklistUntilTimes = new long[length]; } + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + @Override public final TrackGroup getTrackGroup() { return group; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index ad02b6c775..027b2abde9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -47,6 +47,20 @@ public interface TrackSelection { } + /** + * Enables the track selection. + *

      + * This method may not be called when the track selection is already enabled. + */ + void enable(); + + /** + * Disables this track selection. + *

      + * This method may only be called when the track selection is already enabled. + */ + void disable(); + /** * Returns the {@link TrackGroup} to which the selected tracks belong. */ @@ -124,6 +138,8 @@ public interface TrackSelection { /** * Updates the selected track. + *

      + * This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -150,7 +166,7 @@ public interface TrackSelection { * An example of a case where a smaller value may be returned is if network conditions have * improved dramatically, allowing chunks to be discarded and re-buffered in a track of * significantly higher quality. Discarding chunks may allow faster switching to a higher quality - * track in this case. + * track in this case. This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -167,6 +183,8 @@ public interface TrackSelection { * period of time. Blacklisting will fail if all other tracks are currently blacklisted. If * blacklisting the currently selected track, note that it will remain selected until the next * call to {@link #updateSelectedTrack(long, long, long)}. + *

      + * This method may only be called when the selection is enabled. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java new file mode 100644 index 0000000000..20346a0355 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.List; +import junit.framework.Assert; + +/** + * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number + * of calls to its methods. + */ +public final class FakeTrackSelection implements TrackSelection { + + private final TrackGroup rendererTrackGroup; + + public int enableCount; + public int releaseCount; + public boolean isEnabled; + + public FakeTrackSelection(TrackGroup rendererTrackGroup) { + this.rendererTrackGroup = rendererTrackGroup; + } + + @Override + public void enable() { + // assert that track selection is in disabled state before this call. + Assert.assertFalse(isEnabled); + enableCount++; + isEnabled = true; + } + + @Override + public void disable() { + // assert that track selection is in enabled state before this call. + Assert.assertTrue(isEnabled); + releaseCount++; + isEnabled = false; + } + + @Override + public TrackGroup getTrackGroup() { + return rendererTrackGroup; + } + + @Override + public int length() { + return rendererTrackGroup.length; + } + + @Override + public Format getFormat(int index) { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getIndexInTrackGroup(int index) { + return 0; + } + + @Override + public int indexOf(Format format) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public int indexOf(int indexInTrackGroup) { + return 0; + } + + @Override + public Format getSelectedFormat() { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getSelectedIndexInTrackGroup() { + return 0; + } + + @Override + public int getSelectedIndex() { + return 0; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_UNKNOWN; + } + + @Override + public Object getSelectionData() { + return null; + } + + @Override + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { + Assert.assertTrue(isEnabled); + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public boolean blacklist(int index, long blacklistDurationMs) { + Assert.assertTrue(isEnabled); + return false; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java new file mode 100644 index 0000000000..da9a1a18ad --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 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 android.support.annotation.NonNull; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.ArrayList; +import java.util.List; + +/** + * A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. + */ +public class FakeTrackSelector extends MappingTrackSelector { + + private final List selectedTrackSelections = new ArrayList<>(); + private final boolean mayReuseTrackSelection; + + public FakeTrackSelector() { + this(false); + } + + /** + * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse + * {@link TrackSelection}s during track selection, when it finds previously-selected track + * selection using the same {@link TrackGroup}. + */ + public FakeTrackSelector(boolean mayReuseTrackSelection) { + this.mayReuseTrackSelection = mayReuseTrackSelection; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + List resultList = new ArrayList<>(); + for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { + TrackGroup trackGroup = trackGroupArray.get(0); + FakeTrackSelection trackSelectionForRenderer = reuseOrCreateTrackSelection(trackGroup); + resultList.add(trackSelectionForRenderer); + } + return resultList.toArray(new TrackSelection[resultList.size()]); + } + + @NonNull + private FakeTrackSelection reuseOrCreateTrackSelection(TrackGroup trackGroup) { + FakeTrackSelection trackSelectionForRenderer = null; + if (mayReuseTrackSelection) { + for (FakeTrackSelection selectedTrackSelection : selectedTrackSelections) { + if (selectedTrackSelection.getTrackGroup().equals(trackGroup)) { + trackSelectionForRenderer = selectedTrackSelection; + } + } + } + if (trackSelectionForRenderer == null) { + trackSelectionForRenderer = new FakeTrackSelection(trackGroup); + selectedTrackSelections.add(trackSelectionForRenderer); + } + return trackSelectionForRenderer; + } + + /** + * Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. + */ + public List getSelectedTrackSelections() { + return selectedTrackSelections; + } + +} From afe6f667b280ede0ef9f25c4727fcfc915017501 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 11:51:51 -0800 Subject: [PATCH 039/417] Move all buffer discard to MediaPeriod.discardBuffer This is a step toward retaining a back-buffer in a way that works for all MediaSource implementations. It's not possible to adjust the discardBuffer calls in ExoPlayerImplInternal to discard up to (position - backBufferDurationUs). Next steps are to: 1. Find an appropriate place to specify the back buffer value, to be passed to the discardBuffer calls. I guess the LoadControl is the appropriate place to define such values. 2. Enhance discardBuffer to support a toKeyframe argument to pass through to discardTo. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175565363 --- .../exoplayer2/ExoPlayerImplInternal.java | 1 + .../source/ExtractorMediaPeriod.java | 1 - .../source/chunk/ChunkSampleStream.java | 25 ++++++------------- .../source/dash/DashMediaPeriod.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 1 - 5 files changed, 9 insertions(+), 21 deletions(-) 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 bcbeed437f..e21e15fa58 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 @@ -732,6 +732,7 @@ import java.io.IOException; setPlayingPeriodHolder(newPlayingPeriodHolder); if (playingPeriodHolder.hasEnabledTracks) { periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); + playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index c418c427f7..d112d5eaf1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -569,7 +569,6 @@ import java.util.Arrays; if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) { return false; } - sampleQueue.discardToRead(); } return true; } 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 8a9be92d75..ca83f67c90 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 @@ -110,21 +110,17 @@ public class ChunkSampleStream implements SampleStream, S lastSeekPositionUs = positionUs; } - // TODO: Generalize this method to also discard from the primary sample queue and stop discarding - // from this queue in readData and skipData. This will cause samples to be kept in the queue until - // they've been rendered, rather than being discarded as soon as they're read by the renderer. - // This will make in-buffer seeks more likely when seeking slightly forward from the current - // position. This change will need handling with care, in particular when considering removal of - // chunks from the front of the mediaChunks list. /** - * Discards buffered media for embedded tracks, up to the specified position. + * Discards buffered media up to the specified position. * * @param positionUs The position to discard up to, in microseconds. */ - public void discardEmbeddedTracksTo(long positionUs) { + public void discardBuffer(long positionUs) { + primarySampleQueue.discardTo(positionUs, false, true); for (int i = 0; i < embeddedSampleQueues.length; i++) { embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]); } + discardDownstreamMediaChunks(primarySampleQueue.getFirstIndex()); } /** @@ -189,16 +185,15 @@ public class ChunkSampleStream implements SampleStream, S */ public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; + primarySampleQueue.rewind(); // If we're not pending a reset, see if we can seek within the primary sample queue. boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); if (seekInsideBuffer) { // We succeeded. Discard samples and corresponding chunks prior to the seek position. - discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); - primarySampleQueue.discardToRead(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.rewind(); - embeddedSampleQueue.discardTo(positionUs, true, false); + embeddedSampleQueue.advanceTo(positionUs, true, false); } } else { // We failed, and need to restart. @@ -261,13 +256,8 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); - int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, + return primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); - if (result == C.RESULT_BUFFER_READ) { - primarySampleQueue.discardToRead(); - } - return result; } @Override @@ -282,7 +272,6 @@ public class ChunkSampleStream implements SampleStream, S skipCount = 0; } } - primarySampleQueue.discardToRead(); return skipCount; } 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 b5ce45b2f5..3680dac821 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 @@ -261,7 +261,7 @@ import java.util.Map; @Override public void discardBuffer(long positionUs) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.discardEmbeddedTracksTo(positionUs); + sampleStream.discardBuffer(positionUs); } } 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 0d2f758599..07b60f05b0 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 @@ -792,7 +792,6 @@ import java.util.LinkedList; if (!seekInsideQueue && (sampleQueueIsAudioVideoFlags[i] || !haveAudioVideoSampleQueues)) { return false; } - sampleQueue.discardToRead(); } return true; } From b17ae80679a4ae8c4d1276a47a67019d75b54438 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 13 Nov 2017 13:18:42 -0800 Subject: [PATCH 040/417] Fix cenc mode support and add support for the .mp4a extension. Also add encrypted HLS internal sample streams. Issue:#1661 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175577648 --- .../exoplayer2/source/hls/DefaultHlsExtractorFactory.java | 4 +++- .../exoplayer2/source/hls/playlist/HlsPlaylistParser.java | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index c801520927..dc838c9506 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -43,6 +43,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { public static final String MP3_FILE_EXTENSION = ".mp3"; public static final String MP4_FILE_EXTENSION = ".mp4"; public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; + public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4"; public static final String VTT_FILE_EXTENSION = ".vtt"; public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -71,7 +72,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // Only reuse TS and fMP4 extractors. extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) - || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) + || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 6536be3ffe..9bdb01c2e4 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -109,7 +109,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Tue, 14 Nov 2017 03:01:58 -0800 Subject: [PATCH 041/417] Forward ad group and ad index when creating period from concatanted media sources. Also added tests which verify the intended behaviour. GitHub:#3452 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175656478 --- .../source/ConcatenatingMediaSourceTest.java | 26 ++++++++ .../DynamicConcatenatingMediaSourceTest.java | 63 +++++++++++-------- .../source/ConcatenatingMediaSource.java | 4 +- .../DynamicConcatenatingMediaSource.java | 3 +- .../exoplayer2/source/LoopingMediaSource.java | 3 +- .../exoplayer2/testutil/FakeMediaSource.java | 21 +++++-- .../exoplayer2/testutil/FakeTimeline.java | 35 ++++++++++- .../exoplayer2/testutil/TimelineAsserts.java | 61 +++++++++++++++++- 8 files changed, 180 insertions(+), 36 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 53111e83ac..6f6556225e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -196,6 +197,31 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } + public void testPeriodCreationWithAds() throws InterruptedException { + // Create media source with ad child source. + Timeline timelineContentOnly = new FakeTimeline( + new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); + Timeline timelineWithAds = new FakeTimeline( + new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); + FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); + FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, + mediaSourceWithAds); + + // Prepare and assert timeline contains ad groups. + Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + + // Create all periods and assert period creation of child media sources has been called. + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } + /** * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns * the concatenated timeline. diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index c0c5252751..e506d0a4b3 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -154,7 +154,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(0, timeline.getLastWindowIndex(true)); // Assert all periods can be prepared. - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); // Remove at front of queue. mediaSource.removeMediaSource(0); @@ -205,7 +206,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); @@ -239,7 +241,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); //Add lazy sources after preparation (and also try to prepare media period from lazy source). mediaSource.addMediaSource(1, lazySources[2]); @@ -335,7 +338,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(2, timeline.getLastWindowIndex(false)); assertEquals(2, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); } public void testIllegalArguments() { @@ -533,6 +537,35 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { waitForCustomRunnable(); } + public void testPeriodCreationWithAds() throws InterruptedException { + // Create dynamic media source with ad child source. + Timeline timelineContentOnly = new FakeTimeline( + new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); + Timeline timelineWithAds = new FakeTimeline( + new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); + FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); + FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + mediaSource.addMediaSource(mediaSourceContentOnly); + mediaSource.addMediaSource(mediaSourceWithAds); + assertNull(timeline); + + // Prepare and assert timeline contains ad groups. + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + + // Create all periods and assert period creation of child media sources has been called. + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } + private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() throws InterruptedException { HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); @@ -616,28 +649,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } - private static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - int periodCount) { - for (int i = 0; i < periodCount; i++) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); - assertNotNull(mediaPeriod); - final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); - mediaPeriod.prepare(new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - mediaPeriodPrepared.open(); - } - @Override - public void onContinueLoadingRequested(MediaPeriod source) {} - }, 0); - assertTrue(mediaPeriodPrepared.block(TIMEOUT_MS)); - MediaPeriod secondMediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); - assertNotNull(secondMediaPeriod); - mediaSource.releasePeriod(secondMediaPeriod); - mediaSource.releasePeriod(mediaPeriod); - } - } - private static class DynamicConcatenatingMediaSourceAndHandler { public final DynamicConcatenatingMediaSource mediaSource; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index fe8f23c4c0..058471f31f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -120,8 +120,8 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); - MediaPeriodId periodIdInSource = - new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); + MediaPeriodId periodIdInSource = id.copyWithPeriodIndex( + id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 6bfa4047a5..e80abad3ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -341,7 +341,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); - MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild); + MediaPeriodId idInSource = id.copyWithPeriodIndex( + id.periodIndex - holder.firstPeriodIndexInChild); MediaPeriod mediaPeriod; if (!holder.isPrepared) { mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 583c1ed68c..984820cc6a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -80,7 +80,8 @@ public final class LoopingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return loopCount != Integer.MAX_VALUE - ? childSource.createPeriod(new MediaPeriodId(id.periodIndex % childPeriodCount), allocator) + ? childSource.createPeriod(id.copyWithPeriodIndex(id.periodIndex % childPeriodCount), + allocator) : childSource.createPeriod(id, allocator); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index cef48285ca..1f2524110a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -38,6 +38,7 @@ public class FakeMediaSource implements MediaSource { private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; + private final ArrayList createdMediaPeriods; private boolean preparedSource; private boolean releasedSource; @@ -58,13 +59,10 @@ public class FakeMediaSource implements MediaSource { this.timeline = timeline; this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); + this.createdMediaPeriods = new ArrayList<>(); this.trackGroupArray = trackGroupArray; } - public void assertReleased() { - Assert.assertTrue(releasedSource); - } - @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assert.assertFalse(preparedSource); @@ -84,6 +82,7 @@ public class FakeMediaSource implements MediaSource { Assert.assertFalse(releasedSource); FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); + createdMediaPeriods.add(id); return mediaPeriod; } @@ -104,6 +103,20 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + /** + * Assert that the source and all periods have been released. + */ + public void assertReleased() { + Assert.assertTrue(releasedSource); + } + + /** + * Assert that a media period for the given id has been created. + */ + public void assertMediaPeriodCreated(MediaPeriodId mediaPeriodId) { + Assert.assertTrue(createdMediaPeriods.contains(mediaPeriodId)); + } + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { return new FakeMediaPeriod(trackGroupArray); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 040782264b..2937ee2770 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; /** * Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. @@ -36,6 +37,8 @@ public final class FakeTimeline extends Timeline { public final boolean isSeekable; public final boolean isDynamic; public final long durationUs; + public final int adGroupsPerPeriodCount; + public final int adsPerAdGroupCount; public TimelineWindowDefinition(int periodCount, Object id) { this(periodCount, id, true, false, WINDOW_DURATION_US); @@ -47,15 +50,24 @@ public final class FakeTimeline extends Timeline { public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) { + this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); + } + + public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, + boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { this.periodCount = periodCount; this.id = id; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.durationUs = durationUs; + this.adGroupsPerPeriodCount = adGroupsCountPerPeriod; + this.adsPerAdGroupCount = adsPerAdGroupCount; } } + private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; + private final TimelineWindowDefinition[] windowDefinitions; private final int[] periodOffsets; @@ -96,7 +108,28 @@ public final class FakeTimeline extends Timeline { Object id = setIds ? windowPeriodIndex : null; Object uid = setIds ? periodIndex : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; - return period.set(id, uid, windowIndex, periodDurationUs, periodDurationUs * windowPeriodIndex); + long positionInWindowUs = periodDurationUs * windowPeriodIndex; + if (windowDefinition.adGroupsPerPeriodCount == 0) { + return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs); + } else { + int adGroups = windowDefinition.adGroupsPerPeriodCount; + long[] adGroupTimesUs = new long[adGroups]; + int[] adCounts = new int[adGroups]; + int[] adLoadedAndPlayedCounts = new int[adGroups]; + long[][] adDurationsUs = new long[adGroups][]; + long adResumePositionUs = 0; + long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0; + for (int i = 0; i < adGroups; i++) { + adGroupTimesUs[i] = i * adGroupOffset; + adCounts[i] = windowDefinition.adsPerAdGroupCount; + adLoadedAndPlayedCounts[i] = 0; + adDurationsUs[i] = new long[adCounts[i]]; + Arrays.fill(adDurationsUs[i], AD_DURATION_US); + } + return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs, adGroupTimesUs, + adCounts, adLoadedAndPlayedCounts, adLoadedAndPlayedCounts, adDurationsUs, + adResumePositionUs); + } } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index c61aac708c..b1df8f62e1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -16,12 +16,19 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; /** * Unit test for {@link Timeline}. @@ -60,7 +67,7 @@ public final class TimelineAsserts { } /** - * Asserts that window properties {@link Window}.isDynamic are set correctly.. + * Asserts that window properties {@link Window}.isDynamic are set correctly. */ public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { Window window = new Window(); @@ -139,5 +146,57 @@ public final class TimelineAsserts { } } + /** + * Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. + */ + public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) { + Period period = new Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + timeline.getPeriod(i, period); + assertEquals(expectedAdGroupCounts[i], period.getAdGroupCount()); + } + } + + /** + * Asserts that all period (including ad periods) can be created from the source, prepared, and + * released without exception and within timeout. + */ + public static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + Timeline timeline, long timeoutMs) { + Period period = new Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, new MediaPeriodId(i), timeoutMs); + timeline.getPeriod(i, period); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, + new MediaPeriodId(i, adGroupIndex, adIndex), timeoutMs); + } + } + } + } + + private static void assertPeriodCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + MediaPeriodId mediaPeriodId, long timeoutMs) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); + assertNotNull(mediaPeriod); + final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); + mediaPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + mediaPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, /* positionUs= */ 0); + assertTrue(mediaPeriodPrepared.block(timeoutMs)); + // MediaSource is supposed to support multiple calls to createPeriod with the same id without an + // intervening call to releasePeriod. + MediaPeriod secondMediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); + assertNotNull(secondMediaPeriod); + mediaSource.releasePeriod(secondMediaPeriod); + mediaSource.releasePeriod(mediaPeriod); + } + } From fee6cf5cd8272168200e6e238237fd29bc5bcc81 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 14 Nov 2017 03:48:47 -0800 Subject: [PATCH 042/417] Continue adding Builder to MediaSource. Add Builder pattern to SsMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175659618 --- .../exoplayer2/castdemo/PlayerManager.java | 6 +- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 117 +++++++------- .../source/smoothstreaming/SsMediaSource.java | 143 ++++++++++++++++++ 4 files changed, 210 insertions(+), 63 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index ae0fdadfc9..a10692bca7 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -157,8 +157,10 @@ import com.google.android.gms.cast.framework.CastContext; Uri uri = Uri.parse(sample.uri); switch (sample.mimeType) { case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource(uri, DATA_SOURCE_FACTORY, - new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); + return SsMediaSource.Builder + .forManifestUri(uri, DATA_SOURCE_FACTORY, + new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY)) + .build(); case DemoUtil.MIME_TYPE_DASH: return DashMediaSource.Builder .forManifestUri(uri, DATA_SOURCE_FACTORY, 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 614626077a..65e1c0e083 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 @@ -362,8 +362,11 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return new SsMediaSource(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return SsMediaSource.Builder + .forManifestUri(uri, buildDataSourceFactory(false), + new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_DASH: return DashMediaSource.Builder .forManifestUri(uri, buildDataSourceFactory(false), 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 3d5a9c393d..54a5086d3b 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 @@ -58,63 +58,6 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - /** - * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the - * duration by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; - /** - * A fixed default presentation delay for live streams. The presentation delay is the duration - * by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; - - /** - * The interval in milliseconds between invocations of - * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). - */ - private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; - /** - * The minimum default start position for live streams, relative to the start of the live window. - */ - private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; - - private static final String TAG = "DashMediaSource"; - - private final boolean sideloadedManifest; - private final DataSource.Factory manifestDataSourceFactory; - private final DashChunkSource.Factory chunkSourceFactory; - private final int minLoadableRetryCount; - private final long livePresentationDelayMs; - private final EventDispatcher eventDispatcher; - private final ParsingLoadable.Parser manifestParser; - private final ManifestCallback manifestCallback; - private final Object manifestUriLock; - private final SparseArray periodsById; - private final Runnable refreshManifestRunnable; - private final Runnable simulateManifestRefreshRunnable; - - private Listener sourceListener; - private DataSource dataSource; - private Loader loader; - private LoaderErrorThrower loaderErrorThrower; - - private Uri manifestUri; - private long manifestLoadStartTimestamp; - private long manifestLoadEndTimestamp; - private DashManifest manifest; - private Handler handler; - private long elapsedRealtimeOffsetMs; - - private int firstPeriodId; - /** * Builder for {@link DashMediaSource}. Each builder instance can only be used once. */ @@ -142,6 +85,7 @@ public final class DashMediaSource implements MediaSource { */ public static Builder forSideLoadedManifest(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory) { + Assertions.checkArgument(!manifest.dynamic); return new Builder(manifest, null, null, chunkSourceFactory); } @@ -227,7 +171,6 @@ public final class DashMediaSource implements MediaSource { return this; } - /** * Builds a new {@link DashMediaSource} using the current parameters. *

      @@ -236,7 +179,6 @@ public final class DashMediaSource implements MediaSource { * @return The newly built {@link DashMediaSource}. */ public DashMediaSource build() { - Assertions.checkArgument(manifest == null || !manifest.dynamic); Assertions.checkArgument((eventListener == null) == (eventHandler == null)); Assertions.checkState(!isBuildCalled); isBuildCalled = true; @@ -251,6 +193,63 @@ public final class DashMediaSource implements MediaSource { } + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** + * A constant indicating that the presentation delay for live streams should be set to + * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the + * duration by which the default start position precedes the end of the live window. + */ + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; + /** + * A fixed default presentation delay for live streams. The presentation delay is the duration + * by which the default start position precedes the end of the live window. + */ + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; + + /** + * The interval in milliseconds between invocations of + * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the + * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). + */ + private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; + /** + * The minimum default start position for live streams, relative to the start of the live window. + */ + private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; + + private static final String TAG = "DashMediaSource"; + + private final boolean sideloadedManifest; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; + private final int minLoadableRetryCount; + private final long livePresentationDelayMs; + private final EventDispatcher eventDispatcher; + private final ParsingLoadable.Parser manifestParser; + private final ManifestCallback manifestCallback; + private final Object manifestUriLock; + private final SparseArray periodsById; + private final Runnable refreshManifestRunnable; + private final Runnable simulateManifestRefreshRunnable; + + private Listener sourceListener; + private DataSource dataSource; + private Loader loader; + private LoaderErrorThrower loaderErrorThrower; + + private Uri manifestUri; + private long manifestLoadStartTimestamp; + private long manifestLoadEndTimestamp; + private DashManifest manifest; + private Handler handler; + private long elapsedRealtimeOffsetMs; + + private int firstPeriodId; + /** * Constructs an instance to play a given {@link DashManifest}, which must be static. * 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 548f787741..5a93847428 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -51,6 +52,138 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } + /** + * Builder for {@link SsMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final SsManifest manifest; + private final Uri manifestUri; + private final DataSource.Factory manifestDataSourceFactory; + private final SsChunkSource.Factory chunkSourceFactory; + + private ParsingLoadable.Parser manifestParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + + private int minLoadableRetryCount; + private long livePresentationDelayMs; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @return A new builder. + */ + public static Builder forSideLoadedManifest(SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory) { + Assertions.checkArgument(!manifest.isLive); + return new Builder(manifest, null, null, chunkSourceFactory); + } + + /** + * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @return A new builder. + */ + public static Builder forManifestUri(Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { + return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); + } + + private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory) { + this.manifest = manifest; + this.manifestUri = manifestUri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the duration in milliseconds by which the default start position should precede the end + * of the live window for live playbacks. The default value is + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. + * @return This builder. + */ + public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + this.livePresentationDelayMs = livePresentationDelayMs; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the manifest parser to parse loaded manifest data. The default is an instance of + * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * + * @param manifestParser A parser for loaded manifest data. + * @return This builder. + */ + public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + /** + * Builds a new {@link SsMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link SsMediaSource}. + */ + public SsMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + boolean loadableManifestUri = manifestUri != null; + if (loadableManifestUri && manifestParser == null) { + manifestParser = new SsManifestParser(); + } + return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, + chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + eventListener); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -96,7 +229,9 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, @@ -111,7 +246,9 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -129,7 +266,9 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -151,7 +290,9 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, @@ -174,7 +315,9 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, From 244f7a1e332a62b9715218c40a9cf77c779b5af1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 04:08:38 -0800 Subject: [PATCH 043/417] Suppress reference equality warning in EventLogger. We deliberately compare the track group returned by the track selection with the track group in the parameter to check if the track selection is referring to this particular track group. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175660909 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 68f7ddfd21..d72f747940 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -467,6 +467,9 @@ import java.util.Locale; } } + // Suppressing reference equality warning because the track group stored in the track selection + // must point to the exact track group object to be considered part of it. + @SuppressWarnings("ReferenceEquality") private static String getTrackStatusString(TrackSelection selection, TrackGroup group, int trackIndex) { return getTrackStatusString(selection != null && selection.getTrackGroup() == group From b30b7863503cc739c4acf9d1840f29bf460c21c2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 06:17:02 -0800 Subject: [PATCH 044/417] Avoid track group array error-prone reference quality check in CastPlayer. Also replaced the duplicated EMPTY track group array with the one already defined in TrackGroupArray. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175670266 --- .../google/android/exoplayer2/ext/cast/CastPlayer.java | 7 +++---- .../android/exoplayer2/source/TrackGroupArray.java | 10 ++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index ffb06ed232..9a8986409a 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -86,7 +86,6 @@ public final class CastPlayer implements Player { private static final int RENDERER_INDEX_AUDIO = 1; private static final int RENDERER_INDEX_TEXT = 2; private static final long PROGRESS_REPORT_PERIOD_MS = 1000; - private static final TrackGroupArray EMPTY_TRACK_GROUP_ARRAY = new TrackGroupArray(); private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY = new TrackSelectionArray(null, null, null); private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; @@ -137,7 +136,7 @@ public final class CastPlayer implements Player { playbackState = STATE_IDLE; repeatMode = REPEAT_MODE_OFF; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; - currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + currentTrackGroups = TrackGroupArray.EMPTY; currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; @@ -596,8 +595,8 @@ public final class CastPlayer implements Player { MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null; List castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null; if (castMediaTracks == null || castMediaTracks.isEmpty()) { - boolean hasChanged = currentTrackGroups != EMPTY_TRACK_GROUP_ARRAY; - currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + boolean hasChanged = !currentTrackGroups.isEmpty(); + currentTrackGroups = TrackGroupArray.EMPTY; currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; return hasChanged; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index 394cec891b..fb28da581c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -62,8 +62,11 @@ public final class TrackGroupArray { * @param group The group. * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(TrackGroup group) { for (int i = 0; i < length; i++) { + // Suppressed reference equality warning because this is looking for the index of a specific + // TrackGroup object, not the index of a potential equal TrackGroup. if (trackGroups[i] == group) { return i; } @@ -71,6 +74,13 @@ public final class TrackGroupArray { return C.INDEX_UNSET; } + /** + * Returns whether this track group array is empty. + */ + public boolean isEmpty() { + return length == 0; + } + @Override public int hashCode() { if (hashCode == 0) { From 22b95032b38613826fe253f0d037b5fd4efad26b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 14 Nov 2017 07:14:47 -0800 Subject: [PATCH 045/417] Reinstate buffer discard for SmoothStreaming Now we have a ChunkSampleStream.discardBuffer, we need to call it from SsMediaPeriod as well as for the DASH case. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175675427 --- .../exoplayer2/source/smoothstreaming/SsMediaPeriod.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 1cc2a6833d..05fe22226f 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -139,7 +139,9 @@ import java.util.ArrayList; @Override public void discardBuffer(long positionUs) { - // Do nothing. + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardBuffer(positionUs); + } } @Override From 5aa053d91d7c8c9d06cc5cd103538472a9d4f85c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 08:06:50 -0800 Subject: [PATCH 046/417] Add method to FakeMediaSource to trigger source info refresh. This allows to remove the LazyMediaSource used within DynamicConcatenatingMediaSourceTest and also allows to write test which simulates dynamic timeline or manifest updates. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175680371 --- .../DynamicConcatenatingMediaSourceTest.java | 77 ++++++++----------- .../exoplayer2/testutil/FakeMediaSource.java | 38 +++++++-- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index e506d0a4b3..e7b2a8d963 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -33,15 +33,12 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.Allocator; -import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -216,19 +213,26 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistWithLazyMediaSource() throws InterruptedException { timeline = null; - FakeMediaSource[] childSources = createMediaSources(2); - LazyMediaSource[] lazySources = new LazyMediaSource[4]; + + // Create some normal (immediately preparing) sources and some lazy sources whose timeline + // updates need to be triggered. + FakeMediaSource[] fastSources = createMediaSources(2); + FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { - lazySources[i] = new LazyMediaSource(); + lazySources[i] = new FakeMediaSource(null, null); } - //Add lazy sources before preparation + // Add lazy sources and normal sources before preparation. Also remove one lazy source again + // before preparation to check it doesn't throw or change the result. DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(lazySources[0]); - mediaSource.addMediaSource(0, childSources[0]); + mediaSource.addMediaSource(0, fastSources[0]); mediaSource.removeMediaSource(1); mediaSource.addMediaSource(1, lazySources[1]); assertNull(timeline); + + // Prepare and assert that the timeline contains all information for normal sources while having + // placeholder information for lazy sources. prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); assertNotNull(timeline); @@ -236,7 +240,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); - lazySources[1].triggerTimelineUpdate(createFakeTimeline(8)); + // Trigger source info refresh for lazy source and check that the timeline now contains all + // information for all windows. + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); @@ -244,10 +250,11 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, TIMEOUT_MS); - //Add lazy sources after preparation (and also try to prepare media period from lazy source). + // Add further lazy and normal sources after preparation. Also remove one lazy source again to + // check it doesn't throw or change the result. mediaSource.addMediaSource(1, lazySources[2]); waitForTimelineUpdate(); - mediaSource.addMediaSource(2, childSources[1]); + mediaSource.addMediaSource(2, fastSources[1]); waitForTimelineUpdate(); mediaSource.addMediaSource(0, lazySources[3]); waitForTimelineUpdate(); @@ -257,6 +264,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not + // called yet. MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); assertNotNull(lazyPeriod); final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); @@ -269,11 +278,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void onContinueLoadingRequested(MediaPeriod source) {} }, 0); assertFalse(lazyPeriodPrepared.block(1)); + // Assert that a second period can also be created and released without problems. MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); assertNotNull(secondLazyPeriod); mediaSource.releasePeriod(secondLazyPeriod); - lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); + // Trigger source info refresh for lazy media source. Assert that now all information is + // available again and the previously created period now also finished preparing. + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); @@ -281,9 +293,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); mediaSource.releasePeriod(lazyPeriod); + // Release media source and assert all normal and lazy media sources are fully released as well. mediaSource.releaseSource(); - childSources[0].assertReleased(); - childSources[1].assertReleased(); + for (FakeMediaSource fastSource : fastSources) { + fastSource.assertReleased(); + } + for (FakeMediaSource lazySource : lazySources) { + lazySource.assertReleased(); + } } public void testEmptyTimelineMediaSource() throws InterruptedException { @@ -662,38 +679,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } - private static class LazyMediaSource implements MediaSource { - - private Listener listener; - - public void triggerTimelineUpdate(Timeline timeline) { - listener.onSourceInfoRefreshed(this, timeline, null); - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.listener = listener; - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return new FakeMediaPeriod(TrackGroupArray.EMPTY); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - } - - @Override - public void releaseSource() { - } - - } - /** * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 1f2524110a..f4c8435801 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; @@ -34,28 +35,34 @@ import junit.framework.Assert; */ public class FakeMediaSource implements MediaSource { - protected final Timeline timeline; private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; private final ArrayList createdMediaPeriods; + protected Timeline timeline; private boolean preparedSource; private boolean releasedSource; + private Listener listener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a - * {@link TrackGroupArray} using the given {@link Format}s. + * {@link TrackGroupArray} using the given {@link Format}s. The provided {@link Timeline} may be + * null to prevent an immediate source info refresh message when preparing the media source. It + * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { + public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) { this(timeline, manifest, buildTrackGroupArray(formats)); } /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with the - * given {@link TrackGroupArray}. + * given {@link TrackGroupArray}. The provided {@link Timeline} may be null to prevent an + * immediate source info refresh message when preparing the media source. It can be manually set + * later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(Timeline timeline, Object manifest, TrackGroupArray trackGroupArray) { + public FakeMediaSource(@Nullable Timeline timeline, Object manifest, + TrackGroupArray trackGroupArray) { this.timeline = timeline; this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); @@ -67,7 +74,10 @@ public class FakeMediaSource implements MediaSource { public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assert.assertFalse(preparedSource); preparedSource = true; - listener.onSourceInfoRefreshed(this, timeline, manifest); + this.listener = listener; + if (timeline != null) { + listener.onSourceInfoRefreshed(this, timeline, manifest); + } } @Override @@ -77,9 +87,9 @@ public class FakeMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); Assert.assertTrue(preparedSource); Assert.assertFalse(releasedSource); + Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); createdMediaPeriods.add(id); @@ -103,11 +113,23 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + /** + * Sets a new timeline and manifest. If the source is already prepared, this triggers a source + * info refresh message being sent to the listener. + */ + public void setNewSourceInfo(Timeline newTimeline, Object manifest) { + Assert.assertFalse(releasedSource); + this.timeline = newTimeline; + if (preparedSource) { + listener.onSourceInfoRefreshed(this, timeline, manifest); + } + } + /** * Assert that the source and all periods have been released. */ public void assertReleased() { - Assert.assertTrue(releasedSource); + Assert.assertTrue(releasedSource || !preparedSource); } /** From 5301d3813608ae137d94e511129337619e59fa49 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 02:19:48 -0800 Subject: [PATCH 047/417] Some test cleanup The purpose of this change isn't to fix anything. It's just to simplify things a little bit. There will be following CLs that make some changes to get things onto correct threads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175800354 --- .../DynamicConcatenatingMediaSourceTest.java | 263 ++---------------- .../testutil/FakeSimpleExoPlayer.java | 47 +--- .../exoplayer2/testutil/OggTestData.java | 1 - .../exoplayer2/testutil/StubExoPlayer.java | 248 +++++++++++++++++ 4 files changed, 270 insertions(+), 289 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index e7b2a8d963..96d11678c9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -15,19 +15,14 @@ */ package com.google.android.exoplayer2.source; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import android.os.Looper; import android.os.Message; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod.Callback; @@ -37,8 +32,8 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.StubExoPlayer; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -456,7 +451,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -470,7 +465,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( @@ -485,7 +480,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), @@ -500,7 +495,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( @@ -514,7 +509,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); @@ -522,7 +517,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { }); waitForTimelineUpdate(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); @@ -535,7 +530,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( @@ -544,7 +539,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { }); waitForTimelineUpdate(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, @@ -585,24 +580,17 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() throws InterruptedException { + final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); handlerThread.start(); - Handler.Callback handlerCallback = Mockito.mock(Handler.Callback.class); - when(handlerCallback.handleMessage(any(Message.class))).thenReturn(false); - Handler handler = new Handler(handlerThread.getLooper(), handlerCallback); - final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - handler.post(new Runnable() { - @Override - public void run() { - prepareAndListenToTimelineUpdates(mediaSource); - } - }); - waitForTimelineUpdate(); + Handler handler = new Handler(handlerThread.getLooper()); return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); } private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { - mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { + mediaSource.prepareSource(new MessageHandlingExoPlayer(), true, new Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) { timeline = newTimeline; @@ -669,244 +657,34 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private static class DynamicConcatenatingMediaSourceAndHandler { public final DynamicConcatenatingMediaSource mediaSource; - public final Handler handler; + public final Handler mainHandler; public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler handler) { + Handler mainHandler) { this.mediaSource = mediaSource; - this.handler = handler; + this.mainHandler = mainHandler; } } /** - * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. + * ExoPlayer that only accepts custom messages and runs them on a separate handler thread. */ - private static class StubExoPlayer implements ExoPlayer, Handler.Callback { + private static class MessageHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { private final Handler handler; - public StubExoPlayer() { + public MessageHandlingExoPlayer() { HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); handlerThread.start(); handler = new Handler(handlerThread.getLooper(), this); } - @Override - public Looper getPlaybackLooper() { - throw new UnsupportedOperationException(); - } - - @Override - public void addListener(Player.EventListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeListener(Player.EventListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public int getPlaybackState() { - throw new UnsupportedOperationException(); - } - - @Override - public void prepare(MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getPlayWhenReady() { - throw new UnsupportedOperationException(); - } - - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - throw new UnsupportedOperationException(); - } - - @Override - public int getRepeatMode() { - throw new UnsupportedOperationException(); - } - - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getShuffleModeEnabled() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isLoading() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - throw new UnsupportedOperationException(); - } - - @Override - public PlaybackParameters getPlaybackParameters() { - throw new UnsupportedOperationException(); - } - - @Override - public void stop() { - throw new UnsupportedOperationException(); - } - - @Override - public void release() { - throw new UnsupportedOperationException(); - } - @Override public void sendMessages(ExoPlayerMessage... messages) { handler.obtainMessage(0, messages).sendToTarget(); } - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - public int getRendererCount() { - throw new UnsupportedOperationException(); - } - - @Override - public int getRendererType(int index) { - throw new UnsupportedOperationException(); - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - throw new UnsupportedOperationException(); - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - throw new UnsupportedOperationException(); - } - - @Override - public Object getCurrentManifest() { - throw new UnsupportedOperationException(); - } - - @Override - public Timeline getCurrentTimeline() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentPeriodIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getNextWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getPreviousWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public long getDuration() { - throw new UnsupportedOperationException(); - } - - @Override - public long getCurrentPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public long getBufferedPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public int getBufferedPercentage() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCurrentWindowDynamic() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCurrentWindowSeekable() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isPlayingAd() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentAdGroupIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentAdIndexInAdGroup() { - throw new UnsupportedOperationException(); - } - - @Override - public long getContentPosition() { - throw new UnsupportedOperationException(); - } - @Override public boolean handleMessage(Message msg) { ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; @@ -919,6 +697,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } return true; } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 01f984b212..f0e69dfc7e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -69,7 +69,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return player; } - private static class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, + private static class FakeExoPlayer extends StubExoPlayer implements MediaSource.Listener, MediaPeriod.Callback, Runnable { private final Renderer[] renderers; @@ -144,21 +144,11 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return true; } - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - throw new UnsupportedOperationException(); - } - @Override public int getRepeatMode() { return Player.REPEAT_MODE_OFF; } - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new UnsupportedOperationException(); - } - @Override public boolean getShuffleModeEnabled() { return false; @@ -169,31 +159,6 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return isLoading; } - @Override - public void seekToDefaultPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - throw new UnsupportedOperationException(); - } - @Override public PlaybackParameters getPlaybackParameters() { return PlaybackParameters.DEFAULT; @@ -351,16 +316,6 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { }); } - @Override - public void sendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - // MediaSource.Listener @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java index 88b5de7f65..7cae709438 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; - /** * Provides ogg/vorbis test data in bytes for unit tests. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java new file mode 100644 index 0000000000..e03f6fbad9 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2017 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 android.os.Looper; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; + +/** + * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} + * from every method. + */ +public abstract class StubExoPlayer implements ExoPlayer { + + @Override + public Looper getPlaybackLooper() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeListener(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public int getPlaybackState() { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getPlayWhenReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getShuffleModeEnabled() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLoading() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererCount() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererType(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + throw new UnsupportedOperationException(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getCurrentManifest() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getNextWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCurrentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public int getBufferedPercentage() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowSeekable() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayingAd() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdGroupIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentPosition() { + throw new UnsupportedOperationException(); + } + +} From 28df0e133b909832feca142e13f63bcd5fa21121 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 15 Nov 2017 03:08:11 -0800 Subject: [PATCH 048/417] Add Builder pattern to HlsMediaSource. Add Builder pattern to HlsMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175803853 --- .../exoplayer2/castdemo/PlayerManager.java | 4 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/source/hls/HlsMediaSource.java | 136 +++++++++++++++++- 3 files changed, 141 insertions(+), 4 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index a10692bca7..1dfe153c97 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -167,7 +167,9 @@ import com.google.android.gms.cast.framework.CastContext; new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY)) .build(); case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null); + return HlsMediaSource.Builder + .forDataSource(uri, DATA_SOURCE_FACTORY) + .build(); case DemoUtil.MIME_TYPE_VIDEO_MP4: return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), null, null); 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 65e1c0e083..3d669c9477 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 @@ -374,7 +374,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_HLS: - return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); + return HlsMediaSource.Builder + .forDataSource(uri, mediaDataSourceFactory) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_OTHER: return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, eventLogger); 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 21b27e655d..3f28981f0e 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 @@ -47,6 +47,132 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } + /** + * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri manifestUri; + private final HlsDataSourceFactory hlsDataSourceFactory; + + private HlsExtractorFactory extractorFactory; + private ParsingLoadable.Parser playlistParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + private int minLoadableRetryCount; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and + * a {@link DataSource.Factory}. + * + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory A data source factory that will be wrapped by a + * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, + * segments and keys. + * @return A new builder. + */ + public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { + return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + } + + /** + * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and + * a {@link HlsDataSourceFactory}. + * + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * manifests, segments and keys. + * @return A new builder. + */ + public static Builder forHlsDataSource(Uri manifestUri, + HlsDataSourceFactory dataSourceFactory) { + return new Builder(manifestUri, dataSourceFactory); + } + + private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { + this.manifestUri = manifestUri; + this.hlsDataSourceFactory = hlsDataSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } + + /** + * Sets the factory for {@link Extractor}s for the segments. Default value is + * {@link HlsExtractorFactory#DEFAULT}. + * + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the + * segments. + * @return This builder. + */ + public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { + this.extractorFactory = extractorFactory; + return this; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times loads must be retried before + * errors are propagated. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the parser to parse HLS playlists. The default is an instance of + * {@link HlsPlaylistParser}. + * + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @return This builder. + */ + public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { + this.playlistParser = playlistParser; + return this; + } + + /** + * Builds a new {@link HlsMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link HlsMediaSource}. + */ + public HlsMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + if (extractorFactory == null) { + extractorFactory = HlsExtractorFactory.DEFAULT; + } + if (playlistParser == null) { + playlistParser = new HlsPlaylistParser(); + } + return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, + minLoadableRetryCount, eventHandler, eventListener, playlistParser); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -69,7 +195,9 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, @@ -85,7 +213,9 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -105,10 +235,12 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; From bd0bc03f643eb95603b8ba97f9439f194891c0ca Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 03:27:35 -0800 Subject: [PATCH 049/417] Inject toKeyframe discard parameter from ExoPlayerImplInternal ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175805139 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++-- .../android/exoplayer2/source/ClippingMediaPeriod.java | 4 ++-- .../source/DynamicConcatenatingMediaSource.java | 4 ++-- .../android/exoplayer2/source/ExtractorMediaPeriod.java | 4 ++-- .../com/google/android/exoplayer2/source/MediaPeriod.java | 4 +++- .../android/exoplayer2/source/MergingMediaPeriod.java | 4 ++-- .../exoplayer2/source/SingleSampleMediaPeriod.java | 2 +- .../exoplayer2/source/chunk/ChunkSampleStream.java | 8 +++++--- .../android/exoplayer2/source/dash/DashMediaPeriod.java | 4 ++-- .../android/exoplayer2/source/hls/HlsMediaPeriod.java | 4 ++-- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 4 ++-- .../exoplayer2/source/smoothstreaming/SsMediaPeriod.java | 4 ++-- .../android/exoplayer2/testutil/FakeMediaPeriod.java | 2 +- 13 files changed, 28 insertions(+), 24 deletions(-) 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 e21e15fa58..caf64d4e90 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 @@ -541,7 +541,7 @@ import java.io.IOException; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); - playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs); + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs, false); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; @@ -732,7 +732,7 @@ import java.io.IOException; setPlayingPeriodHolder(newPlayingPeriodHolder); if (playingPeriodHolder.hasEnabledTracks) { periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); - playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs); + playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs, false); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 89af07a3f0..7742444323 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -121,8 +121,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void discardBuffer(long positionUs) { - mediaPeriod.discardBuffer(positionUs + startUs); + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs + startUs, toKeyframe); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index e80abad3ef..6b5c8b2637 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -824,8 +824,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } @Override - public void discardBuffer(long positionUs) { - mediaPeriod.discardBuffer(positionUs); + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index d112d5eaf1..086d62e41d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -276,10 +276,10 @@ import java.util.Arrays; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { - sampleQueues[i].discardTo(positionUs, false, trackEnabledStates[i]); + sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index c297229d78..439562e0ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -116,8 +116,10 @@ public interface MediaPeriod extends SequenceableLoader { * This method should only be called after the period has been prepared. * * @param positionUs The position in microseconds. + * @param toKeyframe If true then for each track discards samples up to the keyframe before or at + * the specified position, rather than any sample before or at that position. */ - void discardBuffer(long positionUs); + void discardBuffer(long positionUs, boolean toKeyframe); /** * Attempts to read a discontinuity. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index e6a4d4e603..786a4693d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -129,9 +129,9 @@ import java.util.IdentityHashMap; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { for (MediaPeriod period : enabledPeriods) { - period.discardBuffer(positionUs); + period.discardBuffer(positionUs, toKeyframe); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 6101c79b7f..0cea0fad66 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -116,7 +116,7 @@ import java.util.Arrays; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { // Do nothing. } 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 ca83f67c90..53742238ef 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 @@ -114,11 +114,13 @@ public class ChunkSampleStream implements SampleStream, S * Discards buffered media up to the specified position. * * @param positionUs The position to discard up to, in microseconds. + * @param toKeyframe If true then for each track discards samples up to the keyframe before or at + * the specified position, rather than any sample before or at that position. */ - public void discardBuffer(long positionUs) { - primarySampleQueue.discardTo(positionUs, false, true); + public void discardBuffer(long positionUs, boolean toKeyframe) { + primarySampleQueue.discardTo(positionUs, toKeyframe, true); for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]); + embeddedSampleQueues[i].discardTo(positionUs, toKeyframe, embeddedTracksSelected[i]); } discardDownstreamMediaChunks(primarySampleQueue.getFirstIndex()); } 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 3680dac821..5a60ee46ae 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 @@ -259,9 +259,9 @@ import java.util.Map; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.discardBuffer(positionUs); + sampleStream.discardBuffer(positionUs, toKeyframe); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index ea9e52e62e..bc2b92cfe8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -183,9 +183,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - sampleStreamWrapper.discardBuffer(positionUs); + sampleStreamWrapper.discardBuffer(positionUs, toKeyframe); } } 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 07b60f05b0..adedee7e83 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 @@ -291,10 +291,10 @@ import java.util.LinkedList; return seekRequired; } - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { int sampleQueueCount = sampleQueues.length; for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues[i].discardTo(positionUs, false, sampleQueuesEnabledStates[i]); + sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]); } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 05fe22226f..3c51abcd49 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -138,9 +138,9 @@ import java.util.ArrayList; } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.discardBuffer(positionUs); + sampleStream.discardBuffer(positionUs, toKeyframe); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 38a5e37fa5..153a427bbd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -85,7 +85,7 @@ public class FakeMediaPeriod implements MediaPeriod { } @Override - public void discardBuffer(long positionUs) { + public void discardBuffer(long positionUs, boolean toKeyframe) { // Do nothing. } From 28693ac7d613fedb5f66e97297aaa71f7ce53c92 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 09:08:32 -0800 Subject: [PATCH 050/417] Allow LoadControl to configure the back-buffer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175833398 --- .../exoplayer2/DefaultLoadControl.java | 12 ++++++- .../exoplayer2/ExoPlayerImplInternal.java | 11 +++++-- .../android/exoplayer2/LoadControl.java | 32 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index d8bc042ad7..4cbcc00886 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Util; /** * The default {@link LoadControl} implementation. */ -public final class DefaultLoadControl implements LoadControl { +public class DefaultLoadControl implements LoadControl { /** * The default minimum duration of media that the player will attempt to ensure is buffered at all @@ -163,6 +163,16 @@ public final class DefaultLoadControl implements LoadControl { return allocator; } + @Override + public long getBackBufferDurationUs() { + return 0; + } + + @Override + public boolean retainBackBufferFromKeyframe() { + return false; + } + @Override public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; 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 caf64d4e90..6acab54ba0 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 @@ -107,6 +107,8 @@ import java.io.IOException; private final Timeline.Window window; private final Timeline.Period period; private final MediaPeriodInfoSequence mediaPeriodInfoSequence; + private final long backBufferDurationUs; + private final boolean retainBackBufferFromKeyframe; private PlaybackInfo playbackInfo; private PlaybackParameters playbackParameters; @@ -147,6 +149,9 @@ import java.io.IOException; this.state = Player.STATE_IDLE; this.player = player; + backBufferDurationUs = loadControl.getBackBufferDurationUs(); + retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); + playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -541,7 +546,8 @@ import java.io.IOException; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); - playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs, false); + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, + retainBackBufferFromKeyframe); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; @@ -732,7 +738,8 @@ import java.io.IOException; setPlayingPeriodHolder(newPlayingPeriodHolder); if (playingPeriodHolder.hasEnabledTracks) { periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); - playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs, false); + playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs - backBufferDurationUs, + retainBackBufferFromKeyframe); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java index c092480222..44b16b0cf6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -55,6 +55,38 @@ public interface LoadControl { */ Allocator getAllocator(); + /** + * Returns the duration of media to retain in the buffer prior to the current playback position, + * for fast backward seeking. + *

      + * Note: If {@link #retainBackBufferFromKeyframe()} is false then seeking in the back-buffer will + * only be fast if the back-buffer contains a keyframe prior to the seek position. + *

      + * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not + * currently supported. + * + * @return The duration of media to retain in the buffer prior to the current playback position, + * in microseconds. + */ + long getBackBufferDurationUs(); + + /** + * Returns whether media should be retained from the keyframe before the current playback position + * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position. + *

      + * Warning: Returning true will cause the back-buffer size to depend on the spacing of keyframes + * in the media being played. Returning true is not recommended unless you control the media and + * are comfortable with the back-buffer size exceeding {@link #getBackBufferDurationUs()} by as + * much as the maximum duration between adjacent keyframes in the media. + *

      + * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not + * currently supported. + * + * @return Whether media should be retained from the keyframe before the current playback position + * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position. + */ + boolean retainBackBufferFromKeyframe(); + /** * Called by the player to determine whether sufficient media is buffered for playback to be * started or resumed. From 6db895bd7b4a69fa9208aaa446440e65251d0ce4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Nov 2017 09:46:28 -0800 Subject: [PATCH 051/417] Use ArrayDeque for playback parameters checkpoints ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175837754 --- .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 73c0bc20be..2180601481 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 @@ -36,8 +36,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.LinkedList; /** * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback @@ -174,7 +174,7 @@ public final class DefaultAudioSink implements AudioSink { private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; - private final LinkedList playbackParametersCheckpoints; + private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; /** @@ -277,7 +277,7 @@ public final class DefaultAudioSink implements AudioSink { drainingAudioProcessorIndex = C.INDEX_UNSET; this.audioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; - playbackParametersCheckpoints = new LinkedList<>(); + playbackParametersCheckpoints = new ArrayDeque<>(); } @Override From 8537376a2c6d7a95a2bdeb4d2b523273109dad3d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 11:13:35 -0800 Subject: [PATCH 052/417] Update release notes with dev-v2 section ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175852052 --- RELEASENOTES.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d6b20be4e1..8bc960f1a1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,12 @@ # Release notes # +### dev-v2 (not yet released) ### + +* Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured by + custom `LoadControl` implementations. +* New Cast extension: Simplifies toggling between local and Cast playbacks. + ### 2.6.0 ### * Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". @@ -60,7 +67,6 @@ ([#3406](https://github.com/google/ExoPlayer/issues/3406)). * New Leanback extension: Simplifies binding Exoplayer to Leanback UI components. -* New Cast extension: Simplifies toggling between local and Cast playbacks. * Unit tests moved to Robolectric. * Misc bugfixes. From 15a2f47f316d0dc8807af91dacd89faed5ef1cdb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 01:07:51 -0800 Subject: [PATCH 053/417] Add support for float output in DefaultAudioSink Also switch from using MIME types to C.ENCODING_* encodings in DefaultAudioSink. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175936872 --- RELEASENOTES.md | 1 + .../java/com/google/android/exoplayer2/C.java | 10 +- .../android/exoplayer2/audio/AudioSink.java | 30 ++-- .../exoplayer2/audio/DefaultAudioSink.java | 134 ++++++++---------- .../audio/MediaCodecAudioRenderer.java | 20 ++- .../audio/SimpleDecoderAudioRenderer.java | 4 +- .../android/exoplayer2/util/MimeTypes.java | 30 +++- .../google/android/exoplayer2/util/Util.java | 1 + 8 files changed, 121 insertions(+), 109 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8bc960f1a1..f5a694e194 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,7 @@ position, for fast backward seeking. The back-buffer can be configured by custom `LoadControl` implementations. * New Cast extension: Simplifies toggling between local and Cast playbacks. +* Support 32-bit PCM float output from `DefaultAudioSink`. ### 2.6.0 ### 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 9d4049ada9..592589e221 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 @@ -127,8 +127,8 @@ public final class C { */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS, - ENCODING_DTS_HD}) + ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_DTS, ENCODING_DTS_HD}) public @interface Encoding {} /** @@ -136,7 +136,7 @@ public final class C { */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT}) + ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) public @interface PcmEncoding {} /** * @see AudioFormat#ENCODING_INVALID @@ -158,6 +158,10 @@ public final class C { * PCM encoding with 32 bits per sample. */ public static final int ENCODING_PCM_32BIT = 0x40000000; + /** + * @see AudioFormat#ENCODING_PCM_FLOAT + */ + public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; /** * @see AudioFormat#ENCODING_AC3 */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 5408032907..faf3160018 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -25,14 +25,13 @@ import java.nio.ByteBuffer; * A sink that consumes audio data. *

      * Before starting playback, specify the input audio format by calling - * {@link #configure(String, int, int, int, int, int[], int, int)}. + * {@link #configure(int, int, int, int, int[], int, int)}. *

      * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

      - * Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format - * changes. The sink will be reinitialized on the next call to - * {@link #handleBuffer(ByteBuffer, long)}. + * Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format changes. + * The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}. *

      * Call {@link #reset()} to prepare the sink to receive audio data from a new playback position. *

      @@ -166,13 +165,12 @@ public interface AudioSink { void setListener(Listener listener); /** - * Returns whether it's possible to play audio in the specified format using encoded audio - * passthrough. + * Returns whether it's possible to play audio in the specified encoding using passthrough. * - * @param mimeType The format mime type. - * @return Whether it's possible to play audio in the format using encoded audio passthrough. + * @param encoding The audio encoding. + * @return Whether it's possible to play audio in the specified encoding using passthrough. */ - boolean isPassthroughSupported(String mimeType); + boolean isPassthroughSupported(@C.Encoding int encoding); /** * Returns the playback position in the stream starting at zero, in microseconds, or @@ -186,12 +184,9 @@ public interface AudioSink { /** * Configures (or reconfigures) the sink. * - * @param inputMimeType The MIME type of audio data provided in the input buffers. + * @param inputEncoding The encoding of audio data provided in the input buffers. * @param inputChannelCount The number of channels. * @param inputSampleRate The sample rate in Hz. - * @param inputPcmEncoding For PCM formats, the encoding used. One of - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} - * and {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size. * @param outputChannels A mapping from input to output channels that is applied to this sink's @@ -205,9 +200,9 @@ public interface AudioSink { * immediately preceding the next call to {@link #reset()} or this method. * @throws ConfigurationException If an error occurs configuring the sink. */ - void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, - @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, - int trimStartSamples, int trimEndSamples) throws ConfigurationException; + void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException; /** * Starts or resumes consuming audio if initialized. @@ -228,8 +223,7 @@ public interface AudioSink { * Returns whether the data was handled in full. If the data was not handled in full then the same * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, * except in the case of an intervening call to {@link #reset()} (or to - * {@link #configure(String, int, int, int, int, int[], int, int)} that causes the sink to be - * reset). + * {@link #configure(int, int, int, int, int[], int, int)} that causes the sink to be reset). * * @param buffer The buffer containing audio data. * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. 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 2180601481..0d3365b5d8 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 @@ -29,7 +29,6 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -182,13 +181,13 @@ public final class DefaultAudioSink implements AudioSink { */ private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; + private boolean isInputPcm; private int inputSampleRate; private int sampleRate; private int channelConfig; - private @C.Encoding int encoding; private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; - private boolean passthrough; + private boolean processingEnabled; private int bufferSize; private long bufferSizeUs; @@ -286,9 +285,8 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public boolean isPassthroughSupported(String mimeType) { - return audioCapabilities != null - && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); + public boolean isPassthroughSupported(@C.Encoding int encoding) { + return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); } @Override @@ -331,18 +329,20 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, - @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, - int trimStartSamples, int trimEndSamples) throws ConfigurationException { + public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException { + boolean flush = false; this.inputSampleRate = inputSampleRate; int channelCount = inputChannelCount; int sampleRate = inputSampleRate; - @C.Encoding int encoding; - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType); - boolean flush = false; - if (!passthrough) { - encoding = inputPcmEncoding; - pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount); + isInputPcm = isEncodingPcm(inputEncoding); + if (isInputPcm) { + pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); + } + @C.Encoding int encoding = inputEncoding; + boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + if (processingEnabled) { trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { @@ -360,8 +360,6 @@ public final class DefaultAudioSink implements AudioSink { if (flush) { resetAudioProcessors(); } - } else { - encoding = getEncodingForMimeType(inputMimeType); } int channelConfig; @@ -411,11 +409,11 @@ public final class DefaultAudioSink implements AudioSink { // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) - if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { + if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate + if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -423,16 +421,24 @@ public final class DefaultAudioSink implements AudioSink { reset(); - this.encoding = encoding; - this.passthrough = passthrough; + this.processingEnabled = processingEnabled; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; - outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); - + outputEncoding = encoding; + if (isInputPcm) { + outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount); + } if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; - } else if (passthrough) { + } else if (isInputPcm) { + int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); + Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); + int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; + int maxAppBufferSize = (int) Math.max(minBufferSize, + durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); + bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); + } else { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { @@ -442,21 +448,9 @@ public final class DefaultAudioSink implements AudioSink { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } - } else { - int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); - Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); - int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; - int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); - bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize - : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize - : multipliedBufferSize; } - bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); - - // The old playback parameters may no longer be applicable so try to reset them now. - setPlaybackParameters(playbackParameters); + bufferSizeUs = + isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; } private void resetAudioProcessors() { @@ -487,6 +481,10 @@ public final class DefaultAudioSink implements AudioSink { releasingConditionVariable.block(); audioTrack = initializeAudioTrack(); + + // The old playback parameters may no longer be applicable so try to reset them now. + setPlaybackParameters(playbackParameters); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { @@ -574,7 +572,7 @@ public final class DefaultAudioSink implements AudioSink { return true; } - if (passthrough && framesPerEncodedSample == 0) { + if (!isInputPcm && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } @@ -618,20 +616,19 @@ public final class DefaultAudioSink implements AudioSink { } } - if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; - } else { + if (isInputPcm) { submittedPcmBytes += buffer.remaining(); + } else { + submittedEncodedFrames += framesPerEncodedSample; } inputBuffer = buffer; } - if (passthrough) { - // Passthrough buffers are not processed. - writeBuffer(inputBuffer, presentationTimeUs); - } else { + if (processingEnabled) { processBuffers(presentationTimeUs); + } else { + writeBuffer(inputBuffer, presentationTimeUs); } if (!inputBuffer.hasRemaining()) { @@ -679,10 +676,9 @@ public final class DefaultAudioSink implements AudioSink { } @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) - throws WriteException { + private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException { if (!buffer.hasRemaining()) { - return true; + return; } if (outputBuffer != null) { Assertions.checkArgument(outputBuffer == buffer); @@ -701,7 +697,7 @@ public final class DefaultAudioSink implements AudioSink { } int bytesRemaining = buffer.remaining(); int bytesWritten = 0; - if (Util.SDK_INT < 21) { // passthrough == false + if (Util.SDK_INT < 21) { // isInputPcm == true // Work out how many bytes we can write without the risk of blocking. int bytesPending = (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); @@ -728,17 +724,15 @@ public final class DefaultAudioSink implements AudioSink { throw new WriteException(bytesWritten); } - if (!passthrough) { + if (isInputPcm) { writtenPcmBytes += bytesWritten; } if (bytesWritten == bytesRemaining) { - if (passthrough) { + if (!isInputPcm) { writtenEncodedFrames += framesPerEncodedSample; } outputBuffer = null; - return true; } - return false; } @Override @@ -758,7 +752,7 @@ public final class DefaultAudioSink implements AudioSink { private boolean drainAudioProcessorsToEndOfStream() throws WriteException { boolean audioProcessorNeedsEndOfStream = false; if (drainingAudioProcessorIndex == C.INDEX_UNSET) { - drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; + drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length; audioProcessorNeedsEndOfStream = true; } while (drainingAudioProcessorIndex < audioProcessors.length) { @@ -799,8 +793,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (passthrough) { - // The playback parameters are always the default in passthrough mode. + if (isInitialized() && !processingEnabled) { + // The playback parameters are always the default if processing is disabled. this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; } @@ -1076,7 +1070,7 @@ public final class DefaultAudioSink implements AudioSink { audioTimestampSet = false; } } - if (getLatencyMethod != null && !passthrough) { + if (getLatencyMethod != null && isInputPcm) { try { // Compute the audio track latency, excluding the latency due to the buffer (leaving // latency due to the mixer and audio hardware driver). @@ -1115,11 +1109,11 @@ public final class DefaultAudioSink implements AudioSink { } private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); + return isInputPcm ? (submittedPcmBytes / pcmFrameSize) : submittedEncodedFrames; } private long getWrittenFrames() { - return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); + return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames; } private void resetSyncParams() { @@ -1212,20 +1206,10 @@ public final class DefaultAudioSink implements AudioSink { MODE_STATIC, audioSessionId); } - @C.Encoding - private static int getEncodingForMimeType(String mimeType) { - switch (mimeType) { - case MimeTypes.AUDIO_AC3: - return C.ENCODING_AC3; - case MimeTypes.AUDIO_E_AC3: - return C.ENCODING_E_AC3; - case MimeTypes.AUDIO_DTS: - return C.ENCODING_DTS; - case MimeTypes.AUDIO_DTS_HD: - return C.ENCODING_DTS_HD; - default: - return C.ENCODING_INVALID; - } + private static boolean isEncodingPcm(@C.Encoding int encoding) { + return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; } private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8206e94cf..18cbcea115 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -51,6 +51,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; + @C.Encoding private int pcmEncoding; private int channelCount; private int encoderDelay; @@ -226,7 +227,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioSink.isPassthroughSupported(mimeType); + return audioSink.isPassthroughSupported(MimeTypes.getEncoding(mimeType)); } @Override @@ -272,10 +273,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) throws ExoPlaybackException { - boolean passthrough = passthroughMediaFormat != null; - String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) - : MimeTypes.AUDIO_RAW; - MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; + @C.Encoding int encoding; + MediaFormat format; + if (passthroughMediaFormat != null) { + encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME)); + format = passthroughMediaFormat; + } else { + encoding = pcmEncoding; + format = outputFormat; + } int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; @@ -289,8 +295,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, - encoderDelay, encoderPadding); + audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, + encoderPadding); } catch (AudioSink.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 98a84fdff8..6be4b1d35d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -329,8 +329,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); - audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount, - outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding); + audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount, + outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding); audioTrackNeedsConfigure = false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 2daf16d3d2..d48d28caa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -208,12 +208,12 @@ public final class MimeTypes { } /** - * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. - * {@link C#TRACK_TYPE_UNKNOWN} if the mime type is not known or the mapping cannot be + * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. + * {@link C#TRACK_TYPE_UNKNOWN} if the MIME type is not known or the mapping cannot be * established. * - * @param mimeType The mimeType. - * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. + * @param mimeType The MIME type. + * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. */ public static int getTrackType(String mimeType) { if (TextUtils.isEmpty(mimeType)) { @@ -239,6 +239,28 @@ public final class MimeTypes { } } + /** + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID} if the mapping cannot be established. + * + * @param mimeType The MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type. + */ + public static @C.Encoding int getEncoding(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_AC3: + return C.ENCODING_AC3; + case MimeTypes.AUDIO_E_AC3: + return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_DTS: + return C.ENCODING_DTS; + case MimeTypes.AUDIO_DTS_HD: + return C.ENCODING_DTS_HD; + default: + return C.ENCODING_INVALID; + } + } + /** * Equivalent to {@code getTrackType(getMediaMimeType(codec))}. * 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 6302563e74..579a70c221 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 @@ -826,6 +826,7 @@ public final class Util { case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: return channelCount * 4; default: throw new IllegalArgumentException(); From b8aedfbf4fa24d9989c0ebcb993ff6fb7e757788 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 02:03:02 -0800 Subject: [PATCH 054/417] Add support for float output for FfmpegAudioRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175940553 --- RELEASENOTES.md | 3 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 52 +++++++++++++++++-- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 29 ++++++++--- extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc | 27 ++++++---- .../ext/flac/LibflacAudioRenderer.java | 3 ++ .../ext/opus/LibopusAudioRenderer.java | 2 + .../android/exoplayer2/audio/AudioSink.java | 8 +-- .../exoplayer2/audio/DefaultAudioSink.java | 11 +++- .../audio/MediaCodecAudioRenderer.java | 12 +++-- .../audio/SimpleDecoderAudioRenderer.java | 10 ++++ .../android/exoplayer2/util/MimeTypes.java | 7 +-- 11 files changed, 130 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f5a694e194..2b3e3ff966 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,7 +6,8 @@ position, for fast backward seeking. The back-buffer can be configured by custom `LoadControl` implementations. * New Cast extension: Simplifies toggling between local and Cast playbacks. -* Support 32-bit PCM float output from `DefaultAudioSink`. +* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to + use this with `FfmpegAudioRenderer`. ### 2.6.0 ### diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index ed8a5b0eac..3e23659bf8 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -41,6 +43,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + private final boolean enableFloatOutput; + private FfmpegDecoder decoder; public FfmpegAudioRenderer() { @@ -55,7 +59,23 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { - super(eventHandler, eventListener, audioProcessors); + this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the + * device/build and if the input format may have bit depth higher than 16-bit. When using + * 32-bit float output, any audio processing will be disabled, including playback speed/pitch + * adjustment. + */ + public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioSink audioSink, boolean enableFloatOutput) { + super(eventHandler, eventListener, null, false, audioSink); + this.enableFloatOutput = enableFloatOutput; } @Override @@ -64,7 +84,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { String sampleMimeType = format.sampleMimeType; if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) { + } else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -82,7 +102,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - format.sampleMimeType, format.initializationData); + format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format)); return decoder; } @@ -90,8 +110,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { public Format getOutputFormat() { int channelCount = decoder.getChannelCount(); int sampleRate = decoder.getSampleRate(); + @C.PcmEncoding int encoding = decoder.getEncoding(); return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null); + Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null); + } + + private boolean isOutputSupported(Format inputFormat) { + return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT); + } + + private boolean shouldUseFloatOutput(Format inputFormat) { + if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) { + return false; + } + switch (inputFormat.sampleMimeType) { + case MimeTypes.AUDIO_RAW: + // For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit. + return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT + || inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT + || inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT; + case MimeTypes.AUDIO_AC3: + // AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding. + return false; + default: + // For all other formats, assume that it's worth using 32-bit float encoding. + return true; + } } } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 2af2101ee7..8807738cfa 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; @@ -29,11 +30,15 @@ import java.util.List; /* package */ final class FfmpegDecoder extends SimpleDecoder { - // Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio. - private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2; + // Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2; + // Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; private final String codecName; private final byte[] extraData; + private final @C.Encoding int encoding; + private final int outputBufferSize; private long nativeContext; // May be reassigned on resetting the codec. private boolean hasOutputFormat; @@ -41,14 +46,17 @@ import java.util.List; private volatile int sampleRate; public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - String mimeType, List initializationData) throws FfmpegDecoderException { + String mimeType, List initializationData, boolean outputFloat) + throws FfmpegDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (!FfmpegLibrary.isAvailable()) { throw new FfmpegDecoderException("Failed to load decoder native libraries."); } codecName = FfmpegLibrary.getCodecName(mimeType); extraData = getExtraData(mimeType, initializationData); - nativeContext = ffmpegInitialize(codecName, extraData); + encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; + outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; + nativeContext = ffmpegInitialize(codecName, extraData, outputFloat); if (nativeContext == 0) { throw new FfmpegDecoderException("Initialization failed."); } @@ -81,8 +89,8 @@ import java.util.List; } ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); - ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE); - int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE); + ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); + int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); if (result < 0) { return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); } @@ -124,6 +132,13 @@ import java.util.List; return sampleRate; } + /** + * Returns the encoding of output audio. + */ + public @C.Encoding int getEncoding() { + return encoding; + } + /** * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if * not required. @@ -153,7 +168,7 @@ import java.util.List; } } - private native long ffmpegInitialize(String codecName, byte[] extraData); + private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat); private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize); private native int ffmpegGetChannelCount(long context); diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index fa615f2ec1..d077c819ab 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -57,8 +57,10 @@ extern "C" { #define ERROR_STRING_BUFFER_LENGTH 256 -// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT. -static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16; +// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT. +static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16; +// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT. +static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT; /** * Returns the AVCodec with the specified name, or NULL if it is not available. @@ -71,7 +73,7 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName); * Returns the created context. */ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData); + jbyteArray extraData, jboolean outputFloat); /** * Decodes the packet into the output buffer, returning the number of bytes @@ -107,13 +109,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { return getCodecByName(env, codecName) != NULL; } -DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData) { +DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, + jboolean outputFloat) { AVCodec *codec = getCodecByName(env, codecName); if (!codec) { LOGE("Codec not found."); return 0L; } - return (jlong) createContext(env, codec, extraData); + return (jlong) createContext(env, codec, extraData, outputFloat); } DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, @@ -177,7 +180,8 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { LOGE("Unexpected error finding codec %d.", codecId); return 0L; } - return (jlong) createContext(env, codec, extraData); + return (jlong) createContext(env, codec, extraData, + context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT); } avcodec_flush_buffers(context); @@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) { } AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData) { + jbyteArray extraData, jboolean outputFloat) { AVCodecContext *context = avcodec_alloc_context3(codec); if (!context) { LOGE("Failed to allocate context."); return NULL; } - context->request_sample_fmt = OUTPUT_FORMAT; + context->request_sample_fmt = + outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT; if (extraData) { jsize size = env->GetArrayLength(extraData); context->extradata_size = size; @@ -275,7 +280,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0); - av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0); + // The output format is always the requested format. + av_opt_set_int(resampleContext, "out_sample_fmt", + context->request_sample_fmt, 0); result = avresample_open(resampleContext); if (result < 0) { logError("avresample_open", result); @@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, context->opaque = resampleContext; } int inSampleSize = av_get_bytes_per_sample(sampleFormat); - int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT); + int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt); int outSamples = avresample_get_out_samples(resampleContext, sampleCount); int bufferOutSize = outSampleSize * channelCount * outSamples; if (outSize + bufferOutSize > outputSize) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index dc376d2ea4..a72b03cd44 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -52,6 +53,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { if (!FlacLibrary.isAvailable() || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { + return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } else { diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index e4745d0c29..b94f3e9332 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { if (!OpusLibrary.isAvailable() || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { + return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index faf3160018..6bb5bf7d8e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -75,7 +75,7 @@ public interface AudioSink { * * @param bufferSize The size of the sink's buffer, in bytes. * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for - * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the * buffered media can have a variable bitrate so the duration may be unknown. * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds. */ @@ -165,12 +165,12 @@ public interface AudioSink { void setListener(Listener listener); /** - * Returns whether it's possible to play audio in the specified encoding using passthrough. + * Returns whether it's possible to play audio in the specified encoding. * * @param encoding The audio encoding. - * @return Whether it's possible to play audio in the specified encoding using passthrough. + * @return Whether it's possible to play audio in the specified encoding. */ - boolean isPassthroughSupported(@C.Encoding int encoding); + boolean isEncodingSupported(@C.Encoding int encoding); /** * Returns the playback position in the stream starting at zero, in microseconds, or 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 0d3365b5d8..ba62ac126e 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 @@ -285,8 +285,15 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public boolean isPassthroughSupported(@C.Encoding int encoding) { - return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); + public boolean isEncodingSupported(@C.Encoding int encoding) { + if (isEncodingPcm(encoding)) { + // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float + // output from platform API version 21 only. Other integer PCM encodings are resampled by this + // sink to 16-bit PCM. + return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; + } else { + return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 18cbcea115..25ad847f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -178,6 +178,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media && mediaCodecSelector.getPassthroughDecoderInfo() != null) { return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; } + if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding)) + || !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) { + // Assume the decoder outputs 16-bit PCM, unless the input is raw. + return FORMAT_UNSUPPORTED_SUBTYPE; + } boolean requiresSecureDecryption = false; DrmInitData drmInitData = format.drmInitData; if (drmInitData != null) { @@ -220,14 +225,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /** * Returns whether encoded audio passthrough should be used for playing back the input format. - * This implementation returns true if the {@link AudioSink} indicates that passthrough is - * supported. + * This implementation returns true if the {@link AudioSink} indicates that encoded audio output + * is supported. * * @param mimeType The type of input media. * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioSink.isPassthroughSupported(MimeTypes.getEncoding(mimeType)); + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 6be4b1d35d..d9ad549104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -200,6 +200,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager, Format format); + /** + * Returns whether the audio sink can accept audio in the specified encoding. + * + * @param encoding The audio encoding. + * @return Whether the audio sink can accept audio in the specified encoding. + */ + protected final boolean supportsOutputEncoding(@C.Encoding int encoding) { + return audioSink.isEncodingSupported(encoding); + } + @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index d48d28caa5..c29a4c3717 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -240,11 +240,12 @@ public final class MimeTypes { } /** - * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or - * {@link C#ENCODING_INVALID} if the mapping cannot be established. + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to specified MIME type, if + * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise. * * @param mimeType The MIME type. - * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID}. */ public static @C.Encoding int getEncoding(String mimeType) { switch (mimeType) { From 74e74402dac18d1687b1f8f549d269dc661b52d2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Nov 2017 05:08:44 -0800 Subject: [PATCH 055/417] Deduplicate ExtractorMediaPeriod discontinuity reporting This change makes sure progress is being made before reporting a discontinuity. Else in cases like having no network and playing a live stream, we allow the discontinuity to be read each time an internal retry occurs, meaning it gets read repeatedly. This does no harm, but is noisy and unnecessary. We should also not allow skipping whilst there is a pending reset or discontinuity notification, just like we don't allow reads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175953064 --- .../exoplayer2/source/ExtractorMediaPeriod.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 086d62e41d..900ba5bd37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -303,7 +303,8 @@ import java.util.Arrays; @Override public long readDiscontinuity() { - if (notifyDiscontinuity) { + if (notifyDiscontinuity + && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) { notifyDiscontinuity = false; return lastSeekPositionUs; } @@ -361,7 +362,7 @@ import java.util.Arrays; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample()); + return !suppressRead() && (loadingFinished || sampleQueues[track].hasNextSample()); } /* package */ void maybeThrowError() throws IOException { @@ -370,7 +371,7 @@ import java.util.Arrays; /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { - if (notifyDiscontinuity || isPendingReset()) { + if (suppressRead()) { return C.RESULT_NOTHING_READ; } return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, @@ -378,6 +379,9 @@ import java.util.Arrays; } /* package */ int skipData(int track, long positionUs) { + if (suppressRead()) { + return 0; + } SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); @@ -387,6 +391,10 @@ import java.util.Arrays; } } + private boolean suppressRead() { + return notifyDiscontinuity || isPendingReset(); + } + // Loader.Callback implementation. @Override From e349905039646aeeb8e5c5470ce28c4399d7c166 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 17 Nov 2017 03:08:55 -0800 Subject: [PATCH 056/417] Add Builder to ExtractorMediaSource. Add Builder pattern to ExtractorMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176088810 --- RELEASENOTES.md | 4 + .../exoplayer2/castdemo/PlayerManager.java | 4 +- .../exoplayer2/imademo/PlayerManager.java | 9 +- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 123 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 8 +- 9 files changed, 149 insertions(+), 35 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2b3e3ff966..93e0573e5e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### dev-v2 (not yet released) ### +* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, + DashMediaSource. +* DASH: + * Support in-MPD EventStream. * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by custom `LoadControl` implementations. diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 1dfe153c97..f00d27a067 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -171,8 +170,7 @@ import com.google.android.gms.cast.framework.CastContext; .forDataSource(uri, DATA_SOURCE_FACTORY) .build(); case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), - null, null); + return new ExtractorMediaSource.Builder(uri, DATA_SOURCE_FACTORY).build(); default: { throw new IllegalStateException("Unsupported type: " + sample.mimeType); } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index e11c840d12..6b840830c5 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -21,8 +21,6 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -69,13 +67,10 @@ import com.google.android.exoplayer2.util.Util; DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getString(R.string.application_name))); - // Produces Extractor instances for parsing the content media (i.e. not the ad). - ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = new ExtractorMediaSource( - Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); + MediaSource contentMediaSource = + new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, 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 3d669c9477..ca253db809 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 @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.source.BehindLiveWindowException; @@ -379,8 +378,9 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_OTHER: - return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), - mainHandler, eventLogger); + return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) + .setEventListener(mainHandler, eventLogger) + .build(); default: { throw new IllegalStateException("Unsupported type: " + type); } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 65fb4c8195..bd6e698dc6 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -76,12 +76,10 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 591f43f38a..aa61df74d9 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -76,12 +76,10 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index c2c1867a90..746f3d273f 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -105,12 +105,10 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 1b3f6cb95c..066953b998 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -98,6 +98,123 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; + /** + * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + + private ExtractorsFactory extractorsFactory; + private int minLoadableRetryCount; + private Handler eventHandler; + private EventListener eventListener; + private String customCacheKey; + private int continueLoadingCheckIntervalBytes; + private boolean isBuildCalled; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + */ + public Builder(Uri uri, DataSource.Factory dataSourceFactory) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + + minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; + continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the factory for {@link Extractor}s to process the media stream. Default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This builder. + */ + public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * Default value is null. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This builder. + */ + public Builder setCustomCacheKey(String customCacheKey) { + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets the number of bytes that should be loaded between each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value + * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This builder. + */ + public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Builds a new {@link ExtractorMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link ExtractorMediaSource}. + */ + public ExtractorMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + if (extractorsFactory == null) { + extractorsFactory = new DefaultExtractorsFactory(); + } + return new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, + minLoadableRetryCount, eventHandler, eventListener, customCacheKey, + continueLoadingCheckIntervalBytes); + } + + } + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. @@ -106,7 +223,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); @@ -122,7 +241,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, String customCacheKey) { @@ -143,7 +264,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { 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 18aa8a63e7..397b8effd3 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 @@ -23,7 +23,6 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -173,9 +172,10 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, - new DefaultExtractorsFactory(), mainHandler, componentListener); + MediaSource adMediaSource = new ExtractorMediaSource.Builder( + adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) + .setEventListener(mainHandler, componentListener) + .build(); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; From 525ae7fed7118edf99cdff5c4f5ca5fb6b23b8e7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 17 Nov 2017 05:26:58 -0800 Subject: [PATCH 057/417] Add simplified FakeTimeline constructor. This is helpful for tests which don't care about detailled timeline set-ups besides the number of windows. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176097369 --- .../android/exoplayer2/ExoPlayerTest.java | 42 +++------- .../testutil/ExoPlayerTestRunner.java | 8 +- .../exoplayer2/testutil/FakeTimeline.java | 76 ++++++++++++++++++- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 0edd19bc09..95d5d96163 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; @@ -68,7 +67,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Object manifest = new Object(); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -87,10 +86,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 0), - new TimelineWindowDefinition(false, false, 0), - new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) @@ -107,10 +103,7 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 10), - new TimelineWindowDefinition(false, false, 10), - new TimelineWindowDefinition(false, false, 10)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { @@ -151,7 +144,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepreparationGivesFreshSourceInfo() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest, @@ -218,10 +211,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepeatModeChanges() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1 .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1 @@ -241,7 +231,7 @@ public final class ExoPlayerTest extends TestCase { } public void testShuffleModeEnabledChanges() throws Exception { - Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), @@ -264,7 +254,6 @@ public final class ExoPlayerTest extends TestCase { } public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { - Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased") .setRepeatMode(Player.REPEAT_MODE_ALL) @@ -274,15 +263,13 @@ public final class ExoPlayerTest extends TestCase { .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish. .build(); new ExoPlayerTestRunner.Builder() - .setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule) + .setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); assertTrue(renderer.isEnded); } public void testSeekProcessedCallback() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") // Initial seek before timeline preparation finished. .pause().seek(10).waitForPlaybackState(Player.STATE_READY) @@ -314,8 +301,7 @@ public final class ExoPlayerTest extends TestCase { } public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -343,9 +329,7 @@ public final class ExoPlayerTest extends TestCase { } public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), - new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -374,8 +358,7 @@ public final class ExoPlayerTest extends TestCase { public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -414,8 +397,7 @@ public final class ExoPlayerTest extends TestCase { public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index a1f8fc7861..591e63dc5b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; @@ -103,8 +102,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The - * default value is a non-seekable, non-dynamic {@link FakeTimeline} with zero duration. Setting - * the timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of + * {@link FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the + * timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. @@ -294,7 +294,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } if (mediaSource == null) { if (timeline == null) { - timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + timeline = new FakeTimeline(1); } mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 2937ee2770..4a9d79f906 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -30,7 +30,10 @@ public final class FakeTimeline extends Timeline { */ public static final class TimelineWindowDefinition { - private static final int WINDOW_DURATION_US = 100000; + /** + * Default test window duration in microseconds. + */ + public static final int DEFAULT_WINDOW_DURATION_US = 100_000; public final int periodCount; public final Object id; @@ -40,19 +43,65 @@ public final class FakeTimeline extends Timeline { public final int adGroupsPerPeriodCount; public final int adsPerAdGroupCount; - public TimelineWindowDefinition(int periodCount, Object id) { - this(periodCount, id, true, false, WINDOW_DURATION_US); + /** + * Creates a seekable, non-dynamic window definition with one period with a duration of + * {@link #DEFAULT_WINDOW_DURATION_US}. + */ + public TimelineWindowDefinition() { + this(1, 0, true, false, DEFAULT_WINDOW_DURATION_US); } + /** + * Creates a seekable, non-dynamic window definition with a duration of + * {@link #DEFAULT_WINDOW_DURATION_US}. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + */ + public TimelineWindowDefinition(int periodCount, Object id) { + this(periodCount, id, true, false, DEFAULT_WINDOW_DURATION_US); + } + + /** + * Creates a window definition with one period. + * + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { this(1, 0, isSeekable, isDynamic, durationUs); } + /** + * Creates a window definition. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) { this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); } + /** + * Creates a window definition with ad groups. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + * @param adGroupsCountPerPeriod The number of ad groups in each period. The position of the ad + * groups is equally distributed in each period starting. + * @param adsPerAdGroupCount The number of ads in each ad group. + */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { this.periodCount = periodCount; @@ -71,6 +120,21 @@ public final class FakeTimeline extends Timeline { private final TimelineWindowDefinition[] windowDefinitions; private final int[] periodOffsets; + /** + * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period + * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. + * + * @param windowCount The number of windows. + */ + public FakeTimeline(int windowCount) { + this(createDefaultWindowDefinitions(windowCount)); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { this.windowDefinitions = windowDefinitions; periodOffsets = new int[windowDefinitions.length + 1]; @@ -141,4 +205,10 @@ public final class FakeTimeline extends Timeline { return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; } + private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { + TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; + Arrays.fill(windowDefinitions, new TimelineWindowDefinition()); + return windowDefinitions; + } + } From 00f68b26c965dea7afc3736982398791b13fc99b Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 17 Nov 2017 06:33:32 -0800 Subject: [PATCH 058/417] Replace hard coded UUID in OfflineLicenseHelper with a parameter ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176102179 --- .../exoplayer2/drm/OfflineLicenseHelperTest.java | 5 ++--- .../exoplayer2/drm/OfflineLicenseHelper.java | 15 +++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 35bfbe613a..22ae57932b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -23,7 +23,6 @@ import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.upstream.HttpDataSource; import java.util.HashMap; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -34,7 +33,6 @@ import org.mockito.MockitoAnnotations; public class OfflineLicenseHelperTest extends InstrumentationTestCase { private OfflineLicenseHelper offlineLicenseHelper; - @Mock private HttpDataSource httpDataSource; @Mock private MediaDrmCallback mediaDrmCallback; @Mock private ExoMediaDrm mediaDrm; @@ -42,7 +40,8 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { protected void setUp() throws Exception { setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); - offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null); + offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, + null); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index cafe41ed09..481bea66c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -27,8 +27,8 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; import java.util.HashMap; +import java.util.UUID; /** * Helper class to download, renew and release offline licenses. @@ -96,7 +96,8 @@ public final class OfflineLicenseHelper { String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + return new OfflineLicenseHelper<>(C.WIDEVINE_UUID, + FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } @@ -104,6 +105,7 @@ public final class OfflineLicenseHelper { /** * Constructs an instance. Call {@link #release()} when the instance is no longer required. * + * @param uuid The UUID of the drm scheme. * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument @@ -111,7 +113,7 @@ public final class OfflineLicenseHelper { * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, * MediaDrmCallback, HashMap, Handler, EventListener) */ - public OfflineLicenseHelper(ExoMediaDrm mediaDrm, MediaDrmCallback callback, + public OfflineLicenseHelper(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); @@ -137,7 +139,7 @@ public final class OfflineLicenseHelper { conditionVariable.open(); } }; - drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback, + drmSessionManager = new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); } @@ -174,12 +176,9 @@ public final class OfflineLicenseHelper { * * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded. * @return The key set id for the downloaded license. - * @throws IOException If an error occurs reading data from the stream. - * @throws InterruptedException If the thread has been interrupted. * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws IOException, - InterruptedException, DrmSessionException { + public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws DrmSessionException { Assertions.checkArgument(drmInitData != null); return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); } From 8455d1036641f59e7ab180f3c954e3e31b175f2c Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:18:06 -0800 Subject: [PATCH 059/417] Simplify LoopingMediaSourceTest setup This test seems to obtain a timeline from a prepared FakeMediaSource, but that's identical to the timeline passed into that source to start with :). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176117133 --- .../exoplayer2/source/LoopingMediaSourceTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 2c8deb74b4..79f646b5c4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -30,12 +30,13 @@ import junit.framework.TestCase; */ public class LoopingMediaSourceTest extends TestCase { - private final Timeline multiWindowTimeline; + private FakeTimeline multiWindowTimeline; - public LoopingMediaSourceTest() { - multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource( - new FakeTimeline(new TimelineWindowDefinition(1, 111), - new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null)); + @Override + public void setUp() throws Exception { + super.setUp(); + multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111), + new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)); } public void testSingleLoop() { From 676f3bfc490ad16e4b1dcf75b187c4f1f83f78a7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:22:17 -0800 Subject: [PATCH 060/417] Add MediaSourceTestRunner for MediaSource tests. - MediaSourceTestRunner aims to encapsulate some of the logic currently used in DynamicConcatenatingMediaSourceTest, so it can be re-used for testing other MediaSource implementations. - The change also fixes DynamicConcatenatingMediaSourceTest to execute calls on the correct threads, and to release handler threads at the end of each test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176117535 --- .../source/ConcatenatingMediaSourceTest.java | 29 +- .../DynamicConcatenatingMediaSourceTest.java | 561 ++++++++---------- .../testutil/MediaSourceTestRunner.java | 290 +++++++++ .../android/exoplayer2/testutil/TestUtil.java | 1 + .../exoplayer2/testutil/TimelineAsserts.java | 50 -- 5 files changed, 572 insertions(+), 359 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 6f6556225e..429325defc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -32,6 +33,8 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { + private static final int TIMEOUT_MS = 10000; + public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -208,18 +211,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - // Prepare and assert timeline contains ad groups. - Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource); - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + try { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); - // Create all periods and assert period creation of child media sources has been called. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + // Create all periods and assert period creation of child media sources has been called. + testRunner.assertPrepareAndReleaseAllPeriods(); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } finally { + testRunner.release(); + } } /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 96d11678c9..536180fafc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -20,19 +20,15 @@ import static org.mockito.Mockito.verify; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import android.os.Message; 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.MediaPeriod.Callback; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.StubExoPlayer; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.util.Arrays; import junit.framework.TestCase; @@ -45,78 +41,84 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private static final int TIMEOUT_MS = 10000; - private Timeline timeline; - private boolean timelineUpdated; - private boolean customRunnableCalled; + private DynamicConcatenatingMediaSource mediaSource; + private MediaSourceTestRunner testRunner; - public void testPlaylistChangesAfterPreparation() throws InterruptedException { - timeline = null; - FakeMediaSource[] childSources = createMediaSources(7); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); - prepareAndListenToTimelineUpdates(mediaSource); - assertNotNull(timeline); - waitForTimelineUpdate(); + @Override + public void setUp() { + mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + } + + @Override + public void tearDown() { + testRunner.release(); + } + + public void testPlaylistChangesAfterPreparation() { + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); + FakeMediaSource[] childSources = createMediaSources(7); + // Add first source. mediaSource.addMediaSource(childSources[0]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); // Add at front of queue. mediaSource.addMediaSource(0, childSources[1]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1); TimelineAsserts.assertWindowIds(timeline, 222, 111); // Add at back of queue. mediaSource.addMediaSource(childSources[2]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); // Add in the middle. mediaSource.addMediaSource(1, childSources[3]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); // Add bulk. - mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4], - (MediaSource) childSources[5], (MediaSource) childSources[6])); - waitForTimelineUpdate(); + mediaSource.addMediaSources(3, Arrays.asList(childSources[4], childSources[5], + childSources[6])); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Move sources. mediaSource.moveMediaSource(2, 3); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); mediaSource.moveMediaSource(3, 2); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); mediaSource.moveMediaSource(0, 6); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); mediaSource.moveMediaSource(6, 0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Remove in the middle. mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(1); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); for (int i = 3; i <= 6; i++) { @@ -146,35 +148,31 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(0, timeline.getLastWindowIndex(true)); // Assert all periods can be prepared. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); // Remove at front of queue. mediaSource.removeMediaSource(0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 3); TimelineAsserts.assertWindowIds(timeline, 111, 333); childSources[1].assertReleased(); // Remove at back of queue. mediaSource.removeMediaSource(1); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); childSources[2].assertReleased(); // Remove last source. mediaSource.removeMediaSource(0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); childSources[3].assertReleased(); } - public void testPlaylistChangesBeforePreparation() throws InterruptedException { - timeline = null; + public void testPlaylistChangesBeforePreparation() { FakeMediaSource[] childSources = createMediaSources(4); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); mediaSource.addMediaSource(0, childSources[2]); @@ -182,11 +180,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSource.removeMediaSource(0); mediaSource.moveMediaSource(1, 0); mediaSource.addMediaSource(1, childSources[3]); - assertNull(timeline); + testRunner.assertNoTimelineChange(); - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - assertNotNull(timeline); + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, @@ -198,98 +194,94 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); } } - public void testPlaylistWithLazyMediaSource() throws InterruptedException { - timeline = null; - + public void testPlaylistWithLazyMediaSource() { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); - FakeMediaSource[] lazySources = new FakeMediaSource[4]; + final FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { lazySources[i] = new FakeMediaSource(null, null); } // Add lazy sources and normal sources before preparation. Also remove one lazy source again // before preparation to check it doesn't throw or change the result. - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(lazySources[0]); mediaSource.addMediaSource(0, fastSources[0]); mediaSource.removeMediaSource(1); mediaSource.addMediaSource(1, lazySources[1]); - assertNull(timeline); + testRunner.assertNoTimelineChange(); // Prepare and assert that the timeline contains all information for normal sources while having // placeholder information for lazy sources. - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - assertNotNull(timeline); + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1); TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); // Trigger source info refresh for lazy source and check that the timeline now contains all // information for all windows. - lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); - waitForTimelineUpdate(); + testRunner.runOnPlaybackThread(new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); + timeline = testRunner.assertTimelineChange(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); // Add further lazy and normal sources after preparation. Also remove one lazy source again to // check it doesn't throw or change the result. mediaSource.addMediaSource(1, lazySources[2]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(2, fastSources[1]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(0, lazySources[3]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(2); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not // called yet. - MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); - assertNotNull(lazyPeriod); - final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); - lazyPeriod.prepare(new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - lazyPeriodPrepared.open(); - } - @Override - public void onContinueLoadingRequested(MediaPeriod source) {} - }, 0); - assertFalse(lazyPeriodPrepared.block(1)); + MediaPeriod lazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + ConditionVariable preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); + assertFalse(preparedCondition.block(1)); + // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); - assertNotNull(secondLazyPeriod); - mediaSource.releasePeriod(secondLazyPeriod); + MediaPeriod secondLazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + testRunner.releasePeriod(secondLazyPeriod); // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. - lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); - waitForTimelineUpdate(); + testRunner.runOnPlaybackThread(new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); + timeline = testRunner.assertTimelineChange(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); - assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); - mediaSource.releasePeriod(lazyPeriod); + assertTrue(preparedCondition.block(1)); - // Release media source and assert all normal and lazy media sources are fully released as well. - mediaSource.releaseSource(); + // Release the period and source. + testRunner.releasePeriod(lazyPeriod); + testRunner.releaseSource(); + + // Assert all sources were fully released. for (FakeMediaSource fastSource : fastSources) { fastSource.assertReleased(); } @@ -298,17 +290,12 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineMediaSource() throws InterruptedException { - timeline = null; - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); - prepareAndListenToTimelineUpdates(mediaSource); - assertNotNull(timeline); - waitForTimelineUpdate(); + public void testEmptyTimelineMediaSource() { + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSources(Arrays.asList(new MediaSource[] { @@ -316,18 +303,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) })); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); // Insert non-empty media source to leave empty sources at the start, the end, and the middle // (with single and multiple empty sources in a row). MediaSource[] mediaSources = createMediaSources(3); mediaSource.addMediaSource(1, mediaSources[0]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(4, mediaSources[1]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(6, mediaSources[2]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, @@ -350,12 +337,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(2, timeline.getLastWindowIndex(false)); assertEquals(2, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); } public void testIllegalArguments() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); // Null sources. @@ -394,7 +379,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddSingle() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -402,7 +386,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddMultiple() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSources(Arrays.asList( @@ -411,7 +394,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); @@ -419,134 +401,159 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(/* index */ 0, Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + mediaSource.addMediaSources(/* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); verify(runnable).run(); } - public void testCustomCallbackBeforePreparationRemove() throws InterruptedException { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + public void testCustomCallbackBeforePreparationRemove() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSource(createFakeMediaSource()); + mediaSource.addMediaSource(createFakeMediaSource()); mediaSource.removeMediaSource(/* index */ 0, runnable); verify(runnable).run(); } - public void testCustomCallbackBeforePreparationMove() throws InterruptedException { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + public void testCustomCallbackBeforePreparationMove() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); verify(runnable).run(); } - public void testCustomCallbackAfterPreparationAddSingle() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddSingle() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(1, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddMultiple() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), - runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddSingleWithIndex() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(1, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources(/* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationRemove() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); - } - }); - waitForTimelineUpdate(); + public void testCustomCallbackAfterPreparationRemove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); - } - }); - waitForCustomRunnable(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(0, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationMove() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); - } - }); - waitForTimelineUpdate(); + public void testCustomCallbackAfterPreparationMove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + } + }); + testRunner.assertTimelineChangeBlocking(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, - runnable); - } - }); - waitForCustomRunnable(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } public void testPeriodCreationWithAds() throws InterruptedException { @@ -557,19 +564,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(mediaSourceContentOnly); mediaSource.addMediaSource(mediaSourceWithAds); - assertNull(timeline); - // Prepare and assert timeline contains ad groups. - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); + Timeline timeline = testRunner.prepareSource(); + + // Assert the timeline contains ad groups. TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); // Create all periods and assert period creation of child media sources has been called. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); @@ -578,66 +582,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } - private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() - throws InterruptedException { - final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); - } - - private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { - mediaSource.prepareSource(new MessageHandlingExoPlayer(), true, new Listener() { - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) { - timeline = newTimeline; - synchronized (DynamicConcatenatingMediaSourceTest.this) { - timelineUpdated = true; - DynamicConcatenatingMediaSourceTest.this.notify(); - } - } - }); - } - - private synchronized void waitForTimelineUpdate() throws InterruptedException { - long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; - while (!timelineUpdated) { - wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= deadlineMs) { - fail("No timeline update occurred within timeout."); - } - } - timelineUpdated = false; - } - - private Runnable createCustomRunnable() { - return new Runnable() { - @Override - public void run() { - synchronized (DynamicConcatenatingMediaSourceTest.this) { - assertTrue(timelineUpdated); - timelineUpdated = false; - customRunnableCalled = true; - DynamicConcatenatingMediaSourceTest.this.notify(); - } - } - }; - } - - private synchronized void waitForCustomRunnable() throws InterruptedException { - long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; - while (!customRunnableCalled) { - wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= deadlineMs) { - fail("No custom runnable call occurred within timeout."); - } - } - customRunnableCalled = false; - } - private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { @@ -654,48 +598,69 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } - private static class DynamicConcatenatingMediaSourceAndHandler { + private static final class DummyMainThread { - public final DynamicConcatenatingMediaSource mediaSource; - public final Handler mainHandler; + private final HandlerThread thread; + private final Handler handler; - public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler mainHandler) { - this.mediaSource = mediaSource; - this.mainHandler = mainHandler; + private DummyMainThread() { + thread = new HandlerThread("DummyMainThread"); + thread.start(); + handler = new Handler(thread.getLooper()); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + handler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + } + + public void release() { + thread.quit(); } } - /** - * ExoPlayer that only accepts custom messages and runs them on a separate handler thread. - */ - private static class MessageHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static final class TimelineGrabber implements Runnable { - private final Handler handler; + private final MediaSourceTestRunner testRunner; + private final ConditionVariable finishedCondition; - public MessageHandlingExoPlayer() { - HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper(), this); + private Timeline timeline; + private AssertionError error; + + public TimelineGrabber(MediaSourceTestRunner testRunner) { + this.testRunner = testRunner; + finishedCondition = new ConditionVariable(); } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); - } - - @Override - public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + public void run() { + try { + timeline = testRunner.assertTimelineChange(); + } catch (AssertionError e) { + error = e; } - return true; + finishedCondition.open(); + } + + public Timeline assertTimelineChangeBlocking() { + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (error != null) { + throw error; + } + return timeline; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java new file mode 100644 index 0000000000..df1282c7e1 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2017 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 junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +/** + * A runner for {@link MediaSource} tests. + */ +public class MediaSourceTestRunner { + + private final long timeoutMs; + private final StubExoPlayer player; + private final MediaSource mediaSource; + private final MediaSourceListener mediaSourceListener; + private final HandlerThread playbackThread; + private final Handler playbackHandler; + private final Allocator allocator; + + private final LinkedBlockingDeque timelines; + private Timeline timeline; + + /** + * @param mediaSource The source under test. + * @param allocator The allocator to use during the test run. + * @param timeoutMs The timeout for operations in milliseconds. + */ + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + this.mediaSource = mediaSource; + this.allocator = allocator; + this.timeoutMs = timeoutMs; + playbackThread = new HandlerThread("PlaybackThread"); + playbackThread.start(); + Looper playbackLooper = playbackThread.getLooper(); + playbackHandler = new Handler(playbackLooper); + player = new EventHandlingExoPlayer(playbackLooper); + mediaSourceListener = new MediaSourceListener(); + timelines = new LinkedBlockingDeque<>(); + } + + /** + * Runs the provided {@link Runnable} on the playback thread, blocking until execution completes. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnPlaybackThread(final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + playbackHandler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertTrue(finishedCondition.block(timeoutMs)); + } + + /** + * Prepares the source on the playback thread, asserting that it provides an initial timeline. + * + * @return The initial {@link Timeline}. + */ + public Timeline prepareSource() { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(player, true, mediaSourceListener); + } + }); + return assertTimelineChangeBlocking(); + } + + /** + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback + * thread, asserting that a non-null {@link MediaPeriod} is returned. + * + * @param periodId The id of the period to create. + * @return The created {@link MediaPeriod}. + */ + public MediaPeriod createPeriod(final MediaPeriodId periodId) { + final MediaPeriod[] holder = new MediaPeriod[1]; + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + holder[0] = mediaSource.createPeriod(periodId, allocator); + } + }); + assertNotNull(holder[0]); + return holder[0]; + } + + /** + * Calls {@link MediaPeriod#prepare(MediaPeriod.Callback, long)} on the playback thread. + * + * @param mediaPeriod The {@link MediaPeriod} to prepare. + * @param positionUs The position at which to prepare. + * @return A {@link ConditionVariable} that will be opened when preparation completes. + */ + public ConditionVariable preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) { + final ConditionVariable preparedCondition = new ConditionVariable(); + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaPeriod.prepare(new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + preparedCondition.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Do nothing. + } + }, positionUs); + } + }); + return preparedCondition; + } + + /** + * Calls {@link MediaSource#releasePeriod(MediaPeriod)} on the playback thread. + * + * @param mediaPeriod The {@link MediaPeriod} to release. + */ + public void releasePeriod(final MediaPeriod mediaPeriod) { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.releasePeriod(mediaPeriod); + } + }); + } + + /** + * Calls {@link MediaSource#releaseSource()} on the playback thread. + */ + public void releaseSource() { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.releaseSource(); + } + }); + } + + /** + * Asserts that the source has not notified its listener of a timeline change since the last call + * to {@link #assertTimelineChangeBlocking()} or {@link #assertTimelineChange()} (or since the + * runner was created if neither method has been called). + */ + public void assertNoTimelineChange() { + assertTrue(timelines.isEmpty()); + } + + /** + * Asserts that the source has notified its listener of a single timeline change. + * + * @return The new {@link Timeline}. + */ + public Timeline assertTimelineChange() { + timeline = timelines.removeFirst(); + assertNoTimelineChange(); + return timeline; + } + + /** + * Asserts that the source notifies its listener of a single timeline change. If the source has + * not yet notified its listener, it has up to the timeout passed to the constructor to do so. + * + * @return The new {@link Timeline}. + */ + public Timeline assertTimelineChangeBlocking() { + try { + timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + assertNotNull(timeline); // Null indicates the poll timed out. + assertNoTimelineChange(); + return timeline; + } catch (InterruptedException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + /** + * Creates and releases all periods (including ad periods) defined in the last timeline to be + * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or + * {@link #assertTimelineChangeBlocking()}. + */ + public void assertPrepareAndReleaseAllPeriods() { + Timeline.Period period = new Timeline.Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + assertPrepareAndReleasePeriod(new MediaPeriodId(i)); + timeline.getPeriod(i, period); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + assertPrepareAndReleasePeriod(new MediaPeriodId(i, adGroupIndex, adIndex)); + } + } + } + } + + private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { + MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); + ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); + assertTrue(preparedCondition.block(timeoutMs)); + // MediaSource is supposed to support multiple calls to createPeriod with the same id without an + // intervening call to releasePeriod. + MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); + ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); + assertTrue(secondPreparedCondition.block(timeoutMs)); + // Release the periods. + releasePeriod(mediaPeriod); + releasePeriod(secondMediaPeriod); + } + + /** + * Releases the runner. Should be called when the runner is no longer required. + */ + public void release() { + playbackThread.quit(); + } + + private class MediaSourceListener implements MediaSource.Listener { + + @Override + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + timelines.addLast(timeline); + } + + } + + private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + + private final Handler handler; + + public EventHandlingExoPlayer(Looper looper) { + this.handler = new Handler(looper, this); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + handler.obtainMessage(0, messages).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; + for (ExoPlayerMessage message : messages) { + try { + message.target.handleMessage(message.messageType, message.message); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); + } + } + return true; + } + + } + +} 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 61d1ecaeea..9ee181024c 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 @@ -146,6 +146,7 @@ public class TestUtil { /** * Extracts the timeline from a media source. */ + // TODO: Remove this method and transition callers over to MediaSourceTestRunner. public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { class TimelineListener implements Listener { private Timeline timeline; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index b1df8f62e1..62af44f32f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -16,19 +16,11 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertTrue; - -import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaPeriod.Callback; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; /** * Unit test for {@link Timeline}. @@ -157,46 +149,4 @@ public final class TimelineAsserts { } } - /** - * Asserts that all period (including ad periods) can be created from the source, prepared, and - * released without exception and within timeout. - */ - public static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - Timeline timeline, long timeoutMs) { - Period period = new Period(); - for (int i = 0; i < timeline.getPeriodCount(); i++) { - assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, new MediaPeriodId(i), timeoutMs); - timeline.getPeriod(i, period); - for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { - for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { - assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, - new MediaPeriodId(i, adGroupIndex, adIndex), timeoutMs); - } - } - } - } - - private static void assertPeriodCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - MediaPeriodId mediaPeriodId, long timeoutMs) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); - assertNotNull(mediaPeriod); - final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); - mediaPeriod.prepare(new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - mediaPeriodPrepared.open(); - } - @Override - public void onContinueLoadingRequested(MediaPeriod source) {} - }, /* positionUs= */ 0); - assertTrue(mediaPeriodPrepared.block(timeoutMs)); - // MediaSource is supposed to support multiple calls to createPeriod with the same id without an - // intervening call to releasePeriod. - MediaPeriod secondMediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); - assertNotNull(secondMediaPeriod); - mediaSource.releasePeriod(secondMediaPeriod); - mediaSource.releasePeriod(mediaPeriod); - } - } - From 24672a9c5d26d75980ca60e66baaccc84f5f41cb Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 13:44:26 -0800 Subject: [PATCH 061/417] Don't push anything from TV demo app yet. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176154160 --- demos/tv/README.md | 4 ---- demos/tv/build.gradle | 49 ------------------------------------------- 2 files changed, 53 deletions(-) delete mode 100644 demos/tv/README.md delete mode 100644 demos/tv/build.gradle diff --git a/demos/tv/README.md b/demos/tv/README.md deleted file mode 100644 index 8a1ab807a0..0000000000 --- a/demos/tv/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# TV tuner demo application # - -This folder contains a demo application that uses ExoPlayer to play broadcast -TV from USB tuners. diff --git a/demos/tv/build.gradle b/demos/tv/build.gradle deleted file mode 100644 index 9e87d5e06b..0000000000 --- a/demos/tv/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2017 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. -apply from: '../../constants.gradle' -apply plugin: 'com.android.application' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion 21 - targetSdkVersion project.ext.targetSdkVersion - } - - buildTypes { - release { - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt') - } - debug { - jniDebuggable = true - } - } - - lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' - } - -} - -dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'internal-extension-tv') - compile project(modulePrefix + 'extension-ffmpeg') -} From 2a02171c42ab262502ef0b8a7c4b642680978909 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 20 Nov 2017 02:41:38 -0800 Subject: [PATCH 062/417] Add Builder to SingleSampleMediaSource. Add Builder pattern to SingleSampleMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176332964 --- RELEASENOTES.md | 2 +- .../source/SingleSampleMediaSource.java | 107 ++++++++++++++++++ .../exoplayer2/upstream/DummyDataSource.java | 2 +- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 93e0573e5e..a43a8eb0bc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,7 +3,7 @@ ### dev-v2 (not yet released) ### * Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource. + DashMediaSource, SingleSampleMediaSource. * DASH: * Support in-MPD EventStream. * Allow a back-buffer of media to be retained behind the current playback diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index dd901958fd..2aa8ccc712 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -45,6 +45,107 @@ public final class SingleSampleMediaSource implements MediaSource { } + /** + * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final Format format; + private final long durationUs; + + private int minLoadableRetryCount; + private Handler eventHandler; + private EventListener eventListener; + private int eventSourceId; + private boolean treatLoadErrorsAsEndOfStream; + private boolean isBuildCalled; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + */ + public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + this.format = format; + this.durationUs = durationUs; + this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the listener to respond to events and the handler to deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. + * + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @return This builder. + */ + public Builder setEventSourceId(int eventSourceId) { + this.eventSourceId = eventSourceId; + return this; + } + + /** + * Sets whether load errors will be treated as end-of-stream signal (load errors will not be + * propagated). The default value is false. + * + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated + * normally by {@link SampleStream#maybeThrowError()}. + * @return This builder. + */ + public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + return this; + } + + /** + * Builds a new {@link SingleSampleMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link SingleSampleMediaSource}. + */ + public SingleSampleMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + + return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, + minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + treatLoadErrorsAsEndOfStream); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -66,7 +167,9 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); @@ -79,7 +182,9 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount) { this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); @@ -98,7 +203,9 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java index c20868ef00..fa3e14f1c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -25,7 +25,7 @@ public final class DummyDataSource implements DataSource { public static final DummyDataSource INSTANCE = new DummyDataSource(); - /** A factory that that produces {@link DummyDataSource}. */ + /** A factory that produces {@link DummyDataSource}. */ public static final Factory FACTORY = new Factory() { @Override public DataSource createDataSource() { From d856d0f0562a169b5e77576a8c498fe96a0ef44f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 02:49:39 -0800 Subject: [PATCH 063/417] Use consistent case for sideloaded ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176333544 --- .../google/android/exoplayer2/source/dash/DashMediaSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 54a5086d3b..02f928544b 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 @@ -83,7 +83,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @return A new builder. */ - public static Builder forSideLoadedManifest(DashManifest manifest, + public static Builder forSideloadedManifest(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory) { Assertions.checkArgument(!manifest.dynamic); return new Builder(manifest, null, null, chunkSourceFactory); From a7c424a15c5139bf8ef9202f8cb92df97b1f99bb Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 03:17:29 -0800 Subject: [PATCH 064/417] Add time unit and javadocs to fields in DashManifest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176335667 --- .../dash/manifest/DashManifestTest.java | 12 +-- .../source/dash/DashMediaSource.java | 28 +++---- .../source/dash/DefaultDashChunkSource.java | 6 +- .../source/dash/manifest/DashManifest.java | 75 +++++++++++++------ 4 files changed, 77 insertions(+), 44 deletions(-) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index 7d77ae82d9..dfcb9e72a5 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -129,13 +129,13 @@ public class DashManifestTest extends TestCase { } private static void assertManifestEquals(DashManifest expected, DashManifest actual) { - assertEquals(expected.availabilityStartTime, actual.availabilityStartTime); - assertEquals(expected.duration, actual.duration); - assertEquals(expected.minBufferTime, actual.minBufferTime); + assertEquals(expected.availabilityStartTimeMs, actual.availabilityStartTimeMs); + assertEquals(expected.durationMs, actual.durationMs); + assertEquals(expected.minBufferTimeMs, actual.minBufferTimeMs); assertEquals(expected.dynamic, actual.dynamic); - assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod); - assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth); - assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay); + assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); + assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); + assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); 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 02f928544b..a82b5af583 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 @@ -199,7 +199,7 @@ public final class DashMediaSource implements MediaSource { public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; /** * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or + * {@link DashManifest#suggestedPresentationDelayMs} if specified by the manifest, or * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the * duration by which the default start position precedes the end of the live window. */ @@ -626,12 +626,12 @@ public final class DashMediaSource implements MediaSource { if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { // The manifest describes an incomplete live stream. Update the start/end times to reflect the // live stream duration and the manifest's time shift buffer depth. - long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); - if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); + if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs; int periodIndex = lastPeriodIndex; while (offsetInPeriodUs < 0 && periodIndex > 0) { @@ -655,8 +655,8 @@ public final class DashMediaSource implements MediaSource { if (manifest.dynamic) { long presentationDelayForManifestMs = livePresentationDelayMs; if (presentationDelayForManifestMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) { - presentationDelayForManifestMs = manifest.suggestedPresentationDelay != C.TIME_UNSET - ? manifest.suggestedPresentationDelay : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; + presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs != C.TIME_UNSET + ? manifest.suggestedPresentationDelayMs : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; } // Snap the default position to the start of the segment containing it. windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); @@ -668,9 +668,9 @@ public final class DashMediaSource implements MediaSource { windowDurationUs / 2); } } - long windowStartTimeMs = manifest.availabilityStartTime + long windowStartTimeMs = manifest.availabilityStartTimeMs + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs); - DashTimeline timeline = new DashTimeline(manifest.availabilityStartTime, windowStartTimeMs, + DashTimeline timeline = new DashTimeline(manifest.availabilityStartTimeMs, windowStartTimeMs, firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs, manifest); sourceListener.onSourceInfoRefreshed(this, timeline, manifest); @@ -693,15 +693,15 @@ public final class DashMediaSource implements MediaSource { if (!manifest.dynamic) { return; } - long minUpdatePeriod = manifest.minUpdatePeriod; - if (minUpdatePeriod == 0) { + long minUpdatePeriodMs = manifest.minUpdatePeriodMs; + if (minUpdatePeriodMs == 0) { // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where - // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit - // signaling in the stream, according to: + // minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is + // explicit signaling in the stream, according to: // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ - minUpdatePeriod = 5000; + minUpdatePeriodMs = 5000; } - long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod; + long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriodMs; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 66455b2f04..b254c4f09a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -220,11 +220,11 @@ public class DefaultDashChunkSource implements DashChunkSource { if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; - if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); + if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); } 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 1ab94ccd30..cd24526d7c 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 @@ -23,41 +23,74 @@ import java.util.LinkedList; import java.util.List; /** - * Represents a DASH media presentation description (mpd). + * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014 + * Section 5.3.1.2. */ public class DashManifest { - public final long availabilityStartTime; + /** + * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long availabilityStartTimeMs; - public final long duration; + /** + * The duration of the presentation in milliseconds, or {@link C#TIME_UNSET} if not applicable. + */ + public final long durationMs; - public final long minBufferTime; + /** + * The {@code minBufferTime} value in milliseconds, or {@link C#TIME_UNSET} if not present. + */ + public final long minBufferTimeMs; + /** + * Whether the manifest has value "dynamic" for the {@code type} attribute. + */ public final boolean dynamic; - public final long minUpdatePeriod; + /** + * The {@code minimumUpdatePeriod} value in milliseconds, or {@link C#TIME_UNSET} if not + * applicable. + */ + public final long minUpdatePeriodMs; - public final long timeShiftBufferDepth; + /** + * The {@code timeShiftBufferDepth} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long timeShiftBufferDepthMs; - public final long suggestedPresentationDelay; + /** + * The {@code suggestedPresentationDelay} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long suggestedPresentationDelayMs; + /** + * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section + * 4.7.2. + */ public final UtcTimingElement utcTiming; + /** + * The location of this manifest. + */ public final Uri location; private final List periods; - public DashManifest(long availabilityStartTime, long duration, long minBufferTime, - boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, - long suggestedPresentationDelay, UtcTimingElement utcTiming, Uri location, + public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, + boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, List periods) { - this.availabilityStartTime = availabilityStartTime; - this.duration = duration; - this.minBufferTime = minBufferTime; + this.availabilityStartTimeMs = availabilityStartTimeMs; + this.durationMs = durationMs; + this.minBufferTimeMs = minBufferTimeMs; this.dynamic = dynamic; - this.minUpdatePeriod = minUpdatePeriod; - this.timeShiftBufferDepth = timeShiftBufferDepth; - this.suggestedPresentationDelay = suggestedPresentationDelay; + this.minUpdatePeriodMs = minUpdatePeriodMs; + this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; + this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -73,7 +106,7 @@ public class DashManifest { public final long getPeriodDurationMs(int index) { return index == periods.size() - 1 - ? (duration == C.TIME_UNSET ? C.TIME_UNSET : (duration - periods.get(index).startMs)) + ? (durationMs == C.TIME_UNSET ? C.TIME_UNSET : (durationMs - periods.get(index).startMs)) : (periods.get(index + 1).startMs - periods.get(index).startMs); } @@ -112,10 +145,10 @@ public class DashManifest { copyPeriods.add(copiedPeriod); } } - long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; - return new DashManifest(availabilityStartTime, newDuration, minBufferTime, dynamic, - minUpdatePeriod, timeShiftBufferDepth, suggestedPresentationDelay, utcTiming, location, - copyPeriods); + long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; + return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, + location, copyPeriods); } private static ArrayList copyAdaptationSets( From 82d0a27fd9f3c375c58ef482268408fc1be20bf1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 03:36:50 -0800 Subject: [PATCH 065/417] Fix some lint issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176337058 --- demos/cast/src/main/res/values/strings.xml | 2 -- extensions/cast/build.gradle | 7 ++++++ extensions/ima/build.gradle | 9 +++---- .../exoplayer2/offline/SegmentDownloader.java | 2 +- .../google/android/exoplayer2/util/Util.java | 3 +++ .../dash/manifest/DashManifestParser.java | 2 +- .../source/dash/offline/DashDownloader.java | 4 ++-- .../smoothstreaming/offline/SsDownloader.java | 4 ++-- library/ui/src/main/res/values-v11/styles.xml | 24 ------------------- library/ui/src/main/res/values/styles.xml | 2 +- .../testutil/FakeSimpleExoPlayer.java | 8 ++++++- 11 files changed, 29 insertions(+), 38 deletions(-) delete mode 100644 library/ui/src/main/res/values-v11/styles.xml diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 766e8972d9..d277bb3cdf 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,6 +20,4 @@ Cast - DRM scheme not supported by this device. - diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 7d252332c9..7becb44d1c 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -26,6 +26,13 @@ android { } dependencies { + // This dependency is necessary to force the supportLibraryVersion of + // com.android.support:support-v4 to be used. Else an older version (25.2.0) + // is included via: + // com.google.android.gms:play-services-cast-framework:11.4.2 + // |-- com.google.android.gms:play-services-basement:11.4.2 + // |-- com.android.support:support-v4:25.2.0 + compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.android.support:appcompat-v7:' + supportLibraryVersion compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 90c0a911d9..5038aaf5b9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -28,10 +28,11 @@ android { dependencies { compile project(modulePrefix + 'library-core') // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: - // com.google.android.gms:play-services-ads:11.2.0 - // |-- com.google.android.gms:play-services-ads-lite:11.2.0 - // |-- com.google.android.gms:play-services-basement:11.2.0 + // com.android.support:support-v4 to be used. Else an older version (25.2.0) + // is included via: + // com.google.android.gms:play-services-ads:11.4.2 + // |-- com.google.android.gms:play-services-ads-lite:11.4.2 + // |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index d81df90b81..3cb5db30ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -265,7 +265,7 @@ public abstract class SegmentDownloader implements Downloader { /** * Returns a list of all segments. * - * @see #getSegments(DataSource, M, Object[], boolean)}. + * @see #getSegments(DataSource, M, Object[], boolean) */ protected abstract List getAllSegments(DataSource dataSource, M manifest, boolean allowPartialIndex) throws InterruptedException, IOException; 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 579a70c221..3b402ec59d 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 @@ -32,6 +32,7 @@ import android.view.Display; import android.view.WindowManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; import java.io.ByteArrayOutputStream; @@ -828,6 +829,8 @@ public final class Util { case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_FLOAT: return channelCount * 4; + case C.ENCODING_INVALID: + case Format.NO_VALUE: default: throw new IllegalArgumentException(); } 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 eb9a849563..1868a54d17 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 @@ -360,7 +360,7 @@ public class DashManifestParser extends DefaultHandler String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); if (schemeIdUri != null) { - switch (schemeIdUri.toLowerCase()) { + switch (Util.toLowerInvariant(schemeIdUri)) { case "urn:mpeg:dash:mp4protection:2011": schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 558adca7bd..4c07e4874e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -39,8 +39,8 @@ import java.util.List; /** * Helper class to download DASH streams. * - *

      Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. + *

      Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and + * {@link #getDownloadedBytes()}, this class isn't thread safe. * *

      Example usage: * diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 21cacdc6f3..5e9ae9a164 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -33,8 +33,8 @@ import java.util.List; /** * Helper class to download SmoothStreaming streams. * - *

      Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. + *

      Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and + * {@link #getDownloadedBytes()}, this class isn't thread safe. * *

      Example usage: * diff --git a/library/ui/src/main/res/values-v11/styles.xml b/library/ui/src/main/res/values-v11/styles.xml deleted file mode 100644 index 6f77440287..0000000000 --- a/library/ui/src/main/res/values-v11/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 4ef8971ccd..b57cbeaddf 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -16,7 +16,7 @@ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index f0e69dfc7e..4a5beb0501 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -180,7 +180,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @SuppressWarnings("ThreadJoinLoop") public void release() { stop(); - playbackThread.quitSafely(); + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + playbackThread.quit(); + } + }); while (playbackThread.isAlive()) { try { playbackThread.join(); From 3f6b4d18a992e3ef912b45fbb44f6a72ce5a09ca Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:20:52 -0800 Subject: [PATCH 066/417] Move MockitoUtils to testutils and use it for all Mockito set-ups. In particular this allows to have the workaround for https://code.google.com/p/dexmaker/issues/detail?id=2 in one place only. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176340526 --- extensions/cronet/build.gradle | 1 + .../ByteArrayUploadDataProviderTest.java | 6 ++---- .../ext/cronet/CronetDataSourceTest.java | 6 ++---- .../drm/OfflineLicenseHelperTest.java | 14 ++------------ .../cache/CachedRegionTrackerTest.java | 14 ++------------ .../dash/offline/DashDownloaderTest.java | 2 +- .../exoplayer2/testutil}/MockitoUtil.java | 18 +++++++++++++++++- 7 files changed, 27 insertions(+), 34 deletions(-) rename {library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash => testutils/src/main/java/com/google/android/exoplayer2/testutil}/MockitoUtil.java (65%) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 197dec80a5..0b6f9a587c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -40,6 +40,7 @@ dependencies { compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') androidTestCompile project(modulePrefix + 'library') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index a65bb0951b..bd81750fcb 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest { @Before public void setUp() { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); } diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 4c6a42849f..f92574b7ab 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import android.net.Uri; import android.os.ConditionVariable; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; @@ -107,9 +107,7 @@ public final class CronetDataSourceTest { @Before public void setUp() throws Exception { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); dataSourceUnderTest = spy( new CronetDataSource( mockCronetEngine, diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 22ae57932b..02b29a31b5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -23,9 +23,9 @@ import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.util.HashMap; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests {@link OfflineLicenseHelper}. @@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); @@ -156,14 +156,4 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { new byte[] {1, 4, 7, 0, 3, 6})); } - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 472b5c724b..f40ae0bc7e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests for {@link CachedRegionTracker}. @@ -46,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); @@ -123,14 +123,4 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); } - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 8532e65a68..ec0292514a 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -27,12 +27,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.MockitoUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java similarity index 65% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java index e7cd9baf59..6bd1048bc0 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source.dash; +package com.google.android.exoplayer2.testutil; +import android.content.Context; import android.test.InstrumentationTestCase; import org.mockito.MockitoAnnotations; @@ -25,6 +26,8 @@ public final class MockitoUtil { /** * Sets up Mockito for an instrumentation test. + * + * @param instrumentationTestCase The instrumentation test case class. */ public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. @@ -33,6 +36,19 @@ public final class MockitoUtil { MockitoAnnotations.initMocks(instrumentationTestCase); } + /** + * Sets up Mockito for a JUnit4 test. + * + * @param targetContext The target context. Usually obtained from + * {@code InstrumentationRegistry.getTargetContext()} + * @param testClass The JUnit4 test class. + */ + public static void setUpMockito(Context targetContext, Object testClass) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", targetContext.getCacheDir().getPath()); + MockitoAnnotations.initMocks(testClass); + } + private MockitoUtil() {} } From 75acfc7957fd9ce2c9a7774266688c83de5c3a10 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:22:12 -0800 Subject: [PATCH 067/417] Move media clock handling to its own class. This class implements MediaClock itself and handles the switching between renderer and standalone media clock. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176340615 --- .../android/exoplayer2/DefaultMediaClock.java | 188 ++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 81 +--- .../exoplayer2/util/StandaloneMediaClock.java | 29 +- .../exoplayer2/DefaultMediaClockTest.java | 441 ++++++++++++++++++ 4 files changed, 672 insertions(+), 67 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java new file mode 100644 index 0000000000..5f342bc722 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017 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; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.util.StandaloneMediaClock; + +/** + * Default {@link MediaClock} which uses a renderer media clock and falls back to a + * {@link StandaloneMediaClock} if necessary. + */ +/* package */ final class DefaultMediaClock implements MediaClock { + + /** + * Listener interface to be notified of changes to the active playback parameters. + */ + public interface PlaybackParameterListener { + + /** + * Called when the active playback parameters changed. + * + * @param newPlaybackParameters The newly active {@link PlaybackParameters}. + */ + void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); + + } + + private final StandaloneMediaClock standaloneMediaClock; + private final PlaybackParameterListener listener; + + private @Nullable Renderer rendererClockSource; + private @Nullable MediaClock rendererClock; + + /** + * Creates a new instance with listener for playback parameter changes. + * + * @param listener A {@link PlaybackParameterListener} to listen for playback parameter + * changes. + */ + public DefaultMediaClock(PlaybackParameterListener listener) { + this(listener, Clock.DEFAULT); + } + + /** + * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use + * for the standalone clock implementation. + * + * @param listener A {@link PlaybackParameterListener} to listen for playback parameter + * changes. + * @param clock A {@link Clock}. + */ + public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) { + this.listener = listener; + this.standaloneMediaClock = new StandaloneMediaClock(clock); + } + + /** + * Starts the standalone fallback clock. + */ + public void start() { + standaloneMediaClock.start(); + } + + /** + * Stops the standalone fallback clock. + */ + public void stop() { + standaloneMediaClock.stop(); + } + + /** + * Resets the position of the standalone fallback clock. + * + * @param positionUs The position to set in microseconds. + */ + public void resetPosition(long positionUs) { + standaloneMediaClock.resetPosition(positionUs); + } + + /** + * Notifies the media clock that a renderer has been enabled. Starts using the media clock of the + * provided renderer if available. + * + * @param renderer The renderer which has been enabled. + * @throws ExoPlaybackException If the renderer provides a media clock and another renderer media + * clock is already provided. + */ + public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException { + MediaClock rendererMediaClock = renderer.getMediaClock(); + if (rendererMediaClock != null && rendererMediaClock != rendererClock) { + if (rendererClock != null) { + throw ExoPlaybackException.createForUnexpected( + new IllegalStateException("Multiple renderer media clocks enabled.")); + } + this.rendererClock = rendererMediaClock; + this.rendererClockSource = renderer; + rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters()); + ensureSynced(); + } + } + + /** + * Notifies the media clock that a renderer has been disabled. Stops using the media clock of this + * renderer if used. + * + * @param renderer The renderer which has been disabled. + */ + public void onRendererDisabled(Renderer renderer) { + if (renderer == rendererClockSource) { + this.rendererClock = null; + this.rendererClockSource = null; + } + } + + /** + * Syncs internal clock if needed and returns current clock position in microseconds. + */ + public long syncAndGetPositionUs() { + if (isUsingRendererClock()) { + ensureSynced(); + return rendererClock.getPositionUs(); + } else { + return standaloneMediaClock.getPositionUs(); + } + } + + // MediaClock implementation. + + @Override + public long getPositionUs() { + if (isUsingRendererClock()) { + return rendererClock.getPositionUs(); + } else { + return standaloneMediaClock.getPositionUs(); + } + } + + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + if (rendererClock != null) { + playbackParameters = rendererClock.setPlaybackParameters(playbackParameters); + } + standaloneMediaClock.setPlaybackParameters(playbackParameters); + listener.onPlaybackParametersChanged(playbackParameters); + return playbackParameters; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return rendererClock != null ? rendererClock.getPlaybackParameters() + : standaloneMediaClock.getPlaybackParameters(); + } + + private void ensureSynced() { + long rendererClockPositionUs = rendererClock.getPositionUs(); + standaloneMediaClock.resetPosition(rendererClockPositionUs); + PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); + if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) { + standaloneMediaClock.setPlaybackParameters(playbackParameters); + listener.onPlaybackParametersChanged(playbackParameters); + } + } + + private boolean isUsingRendererClock() { + // Use the renderer clock if the providing renderer has not ended or needs the next sample + // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting + // stuck if tracks in the current period have uneven durations. + // See: https://github.com/google/ExoPlayer/issues/1874. + return rendererClockSource != null && !rendererClockSource.isEnded() + && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd()); + } + +} 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 6acab54ba0..e4bb11c51f 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 @@ -24,6 +24,7 @@ import android.os.SystemClock; import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -37,8 +38,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MediaClock; -import com.google.android.exoplayer2.util.StandaloneMediaClock; import com.google.android.exoplayer2.util.TraceUtil; import java.io.IOException; @@ -46,7 +45,8 @@ import java.io.IOException; * Implements the internal behavior of {@link ExoPlayerImpl}. */ /* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener { + MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, + PlaybackParameterListener { private static final String TAG = "ExoPlayerImplInternal"; @@ -99,7 +99,6 @@ import java.io.IOException; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; private final LoadControl loadControl; - private final StandaloneMediaClock standaloneMediaClock; private final Handler handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; @@ -109,11 +108,9 @@ import java.io.IOException; private final MediaPeriodInfoSequence mediaPeriodInfoSequence; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; + private final DefaultMediaClock mediaClock; private PlaybackInfo playbackInfo; - private PlaybackParameters playbackParameters; - private Renderer rendererMediaClockSource; - private MediaClock rendererMediaClock; private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; @@ -158,13 +155,12 @@ import java.io.IOException; renderers[i].setIndex(i); rendererCapabilities[i] = renderers[i].getCapabilities(); } - standaloneMediaClock = new StandaloneMediaClock(); + mediaClock = new DefaultMediaClock(this); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); mediaPeriodInfoSequence = new MediaPeriodInfoSequence(); trackSelector.init(this); - playbackParameters = PlaybackParameters.DEFAULT; // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. @@ -284,6 +280,16 @@ import java.io.IOException; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } + // DefaultMediaClock.PlaybackParameterListener implementation. + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // TODO(b/37237846): Make LoadControl, period transition position projection, adaptive track + // selection and potentially any time-related code in renderers take into account the playback + // speed. + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + } + // Handler.Callback implementation. @SuppressWarnings("unchecked") @@ -486,14 +492,14 @@ import java.io.IOException; private void startRenderers() throws ExoPlaybackException { rebuffering = false; - standaloneMediaClock.start(); + mediaClock.start(); for (Renderer renderer : enabledRenderers) { renderer.start(); } } private void stopRenderers() throws ExoPlaybackException { - standaloneMediaClock.stop(); + mediaClock.stop(); for (Renderer renderer : enabledRenderers) { ensureStopped(renderer); } @@ -509,18 +515,7 @@ import java.io.IOException; if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); } else { - // Use the standalone clock if there's no renderer clock, or if the providing renderer has - // ended or needs the next sample stream to reenter the ready state. The latter case uses the - // standalone clock to avoid getting stuck if tracks in the current period have uneven - // durations. See: https://github.com/google/ExoPlayer/issues/1874. - if (rendererMediaClockSource == null || rendererMediaClockSource.isEnded() - || (!rendererMediaClockSource.isReady() - && rendererMediaClockSource.hasReadStreamToEnd())) { - rendererPositionUs = standaloneMediaClock.getPositionUs(); - } else { - rendererPositionUs = rendererMediaClock.getPositionUs(); - standaloneMediaClock.setPositionUs(rendererPositionUs); - } + rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); } playbackInfo.positionUs = periodPositionUs; @@ -573,19 +568,6 @@ import java.io.IOException; maybeThrowPeriodPrepareError(); } - // The standalone media clock never changes playback parameters, so just check the renderer. - if (rendererMediaClock != null) { - PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters(); - if (!playbackParameters.equals(this.playbackParameters)) { - // TODO: Make LoadControl, period transition position projection, adaptive track selection - // and potentially any time-related code in renderers take into account the playback speed. - this.playbackParameters = playbackParameters; - standaloneMediaClock.setPlaybackParameters(playbackParameters); - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters) - .sendToTarget(); - } - } - long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET @@ -771,19 +753,14 @@ import java.io.IOException; rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US : playingPeriodHolder.toRendererTime(periodPositionUs); - standaloneMediaClock.setPositionUs(rendererPositionUs); + mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); } } private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - if (rendererMediaClock != null) { - playbackParameters = rendererMediaClock.setPlaybackParameters(playbackParameters); - } - standaloneMediaClock.setPlaybackParameters(playbackParameters); - this.playbackParameters = playbackParameters; - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + mediaClock.setPlaybackParameters(playbackParameters); } private void stopInternal() { @@ -806,7 +783,7 @@ import java.io.IOException; private void resetInternal(boolean releaseMediaSource) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; - standaloneMediaClock.stop(); + mediaClock.stop(); rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { @@ -857,10 +834,7 @@ import java.io.IOException; } private void disableRenderer(Renderer renderer) throws ExoPlaybackException { - if (renderer == rendererMediaClockSource) { - rendererMediaClock = null; - rendererMediaClockSource = null; - } + mediaClock.onRendererDisabled(renderer); ensureStopped(renderer); renderer.disable(); } @@ -1498,16 +1472,7 @@ import java.io.IOException; renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); - MediaClock mediaClock = renderer.getMediaClock(); - if (mediaClock != null) { - if (rendererMediaClock != null) { - throw ExoPlaybackException.createForUnexpected( - new IllegalStateException("Multiple renderer media clocks enabled.")); - } - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderer; - rendererMediaClock.setPlaybackParameters(playbackParameters); - } + mediaClock.onRendererEnabled(renderer); // Start the renderer if playing. if (playing) { renderer.start(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index 96203bb99a..fad3a00f10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; @@ -25,6 +24,8 @@ import com.google.android.exoplayer2.PlaybackParameters; */ public final class StandaloneMediaClock implements MediaClock { + private final Clock clock; + private boolean started; private long baseUs; private long baseElapsedMs; @@ -34,7 +35,17 @@ public final class StandaloneMediaClock implements MediaClock { * Creates a new standalone media clock. */ public StandaloneMediaClock() { - playbackParameters = PlaybackParameters.DEFAULT; + this(Clock.DEFAULT); + } + + /** + * Creates a new standalone media clock using the given {@link Clock} implementation. + * + * @param clock A {@link Clock}. + */ + public StandaloneMediaClock(Clock clock) { + this.clock = clock; + this.playbackParameters = PlaybackParameters.DEFAULT; } /** @@ -42,7 +53,7 @@ public final class StandaloneMediaClock implements MediaClock { */ public void start() { if (!started) { - baseElapsedMs = SystemClock.elapsedRealtime(); + baseElapsedMs = clock.elapsedRealtime(); started = true; } } @@ -52,20 +63,20 @@ public final class StandaloneMediaClock implements MediaClock { */ public void stop() { if (started) { - setPositionUs(getPositionUs()); + resetPosition(getPositionUs()); started = false; } } /** - * Sets the clock's position. + * Resets the clock's position. * * @param positionUs The position to set in microseconds. */ - public void setPositionUs(long positionUs) { + public void resetPosition(long positionUs) { baseUs = positionUs; if (started) { - baseElapsedMs = SystemClock.elapsedRealtime(); + baseElapsedMs = clock.elapsedRealtime(); } } @@ -73,7 +84,7 @@ public final class StandaloneMediaClock implements MediaClock { public long getPositionUs() { long positionUs = baseUs; if (started) { - long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs; + long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs; if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { @@ -87,7 +98,7 @@ public final class StandaloneMediaClock implements MediaClock { public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { - setPositionUs(getPositionUs()); + resetPosition(getPositionUs()); } this.playbackParameters = playbackParameters; return playbackParameters; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java new file mode 100644 index 0000000000..9db4d57a65 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2017 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link DefaultMediaClock}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DefaultMediaClockTest { + + private static final long TEST_POSITION_US = 123456789012345678L; + private static final long SLEEP_TIME_MS = 1_000; + private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS = + new PlaybackParameters(2.0f, 1.0f); + + @Mock private PlaybackParameterListener listener; + private FakeClock fakeClock; + private DefaultMediaClock mediaClock; + + @Before + public void initMediaClockWithFakeClock() { + initMocks(this); + fakeClock = new FakeClock(0); + mediaClock = new DefaultMediaClock(listener, fakeClock); + } + + @Test + public void standaloneResetPosition_getPositionShouldReturnSameValue() throws Exception { + mediaClock.resetPosition(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + @Test + public void standaloneGetAndResetPosition_shouldNotTriggerCallback() throws Exception { + mediaClock.resetPosition(TEST_POSITION_US); + mediaClock.syncAndGetPositionUs(); + verifyNoMoreInteractions(listener); + } + + @Test + public void standaloneClock_shouldNotAutoStart() throws Exception { + assertClockIsStopped(); + } + + @Test + public void standaloneResetPosition_shouldNotStartClock() throws Exception { + mediaClock.resetPosition(TEST_POSITION_US); + assertClockIsStopped(); + } + + @Test + public void standaloneStart_shouldStartClock() throws Exception { + mediaClock.start(); + assertClockIsRunning(); + } + + @Test + public void standaloneStop_shouldKeepClockStopped() throws Exception { + mediaClock.stop(); + assertClockIsStopped(); + } + + @Test + public void standaloneStartAndStop_shouldStopClock() throws Exception { + mediaClock.start(); + mediaClock.stop(); + assertClockIsStopped(); + } + + @Test + public void standaloneStartStopStart_shouldRestartClock() throws Exception { + mediaClock.start(); + mediaClock.stop(); + mediaClock.start(); + assertClockIsRunning(); + } + + @Test + public void standaloneStartAndStop_shouldNotTriggerCallback() throws Exception { + mediaClock.start(); + mediaClock.stop(); + verifyNoMoreInteractions(listener); + } + + @Test + public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() { + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { + PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void standaloneSetPlaybackParameters_shouldTriggerCallback() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackSpeed() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.start(); + // Asserts that clock is running with speed declared in getPlaybackParameters(). + assertClockIsRunning(); + } + + @Test + public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + PlaybackParameters parameters = mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); + assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void standaloneSetOtherPlaybackParameters_shouldTriggerCallbackAgain() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); + verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + } + + @Test + public void standaloneSetSamePlaybackParametersAgain_shouldTriggerCallbackAgain() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener, times(2)).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + verifyNoMoreInteractions(listener); + } + + @Test + public void enableRendererMediaClockWithFixedParameters_usesRendererPlaybackParameters() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void enableRendererMediaClockWithFixedParameters_shouldTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void enableRendererMediaClockWithFixedButSamePlaybackParameters_shouldNotTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + verifyNoMoreInteractions(listener); + } + + @Test + public void disableRendererMediaClock_shouldKeepPlaybackParameters() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.onRendererDisabled(mediaClockRenderer); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererClockSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererClockSetPlaybackParameters_shouldTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererClockSetPlaybackParametersOverwrite_getParametersShouldReturnSameValue() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void rendererClockSetPlaybackParametersOverwrite_shouldTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + } + + @Test + public void enableRendererMediaClock_usesRendererClockPosition() throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped. + assertClockIsStopped(); + } + + @Test + public void resetPositionWhileUsingRendererMediaClock_shouldHaveNoEffect() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + mediaClock.resetPosition(0); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + @Test + public void disableRendererMediaClock_standaloneShouldBeSynced() throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + mediaClock.syncAndGetPositionUs(); + mediaClock.onRendererDisabled(mediaClockRenderer); + fakeClock.advanceTime(SLEEP_TIME_MS); + assertThat(mediaClock.syncAndGetPositionUs()) + .isEqualTo(TEST_POSITION_US + C.msToUs(SLEEP_TIME_MS)); + assertClockIsRunning(); + } + + @Test + public void getPositionWithPlaybackParameterChange_shouldTriggerCallback() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + // Silently change playback parameters of renderer clock. + mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS; + mediaClock.syncAndGetPositionUs(); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererNotReady_shouldStillUseRendererClock() throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ false, + /* isEnded= */ false, /* hasReadStreamToEnd= */ false); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped. + assertClockIsStopped(); + } + + @Test + public void rendererNotReadyAndReadStreamToEnd_shouldFallbackToStandaloneClock() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ false, + /* isEnded= */ false, /* hasReadStreamToEnd= */ true); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertClockIsRunning(); + } + + @Test + public void rendererEnded_shouldFallbackToStandaloneClock() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ true, + /* isEnded= */ true, /* hasReadStreamToEnd= */ true); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertClockIsRunning(); + } + + @Test + public void staleDisableRendererClock_shouldNotThrow() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClockRenderer.positionUs = TEST_POSITION_US; + mediaClock.onRendererDisabled(mediaClockRenderer); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(C.msToUs(fakeClock.elapsedRealtime())); + } + + @Test + public void enableSameRendererClockTwice_shouldNotThrow() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + @Test + public void enableOtherRendererClock_shouldThrow() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer1 = new MediaClockRenderer(); + MediaClockRenderer mediaClockRenderer2 = new MediaClockRenderer(); + mediaClockRenderer1.positionUs = TEST_POSITION_US; + mediaClock.onRendererEnabled(mediaClockRenderer1); + try { + mediaClock.onRendererEnabled(mediaClockRenderer2); + fail(); + } catch (ExoPlaybackException e) { + // Expected. + } + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + private void assertClockIsRunning() { + long clockStartUs = mediaClock.syncAndGetPositionUs(); + fakeClock.advanceTime(SLEEP_TIME_MS); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs + + mediaClock.getPlaybackParameters().getSpeedAdjustedDurationUs(SLEEP_TIME_MS)); + } + + private void assertClockIsStopped() { + long positionAtStartUs = mediaClock.syncAndGetPositionUs(); + fakeClock.advanceTime(SLEEP_TIME_MS); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(positionAtStartUs); + } + + private static class MediaClockRenderer extends FakeMediaClockRenderer { + + public long positionUs; + public PlaybackParameters playbackParameters; + + private final boolean playbackParametersAreMutable; + + public MediaClockRenderer() throws ExoPlaybackException { + this(PlaybackParameters.DEFAULT, false, true, false, false); + } + + public MediaClockRenderer(PlaybackParameters playbackParameters, + boolean playbackParametersAreMutable) + throws ExoPlaybackException { + this(playbackParameters, playbackParametersAreMutable, true, false, false); + } + + public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd) + throws ExoPlaybackException { + this(PlaybackParameters.DEFAULT, false, isReady, isEnded, hasReadStreamToEnd); + } + + private MediaClockRenderer(PlaybackParameters playbackParameters, + boolean playbackParametersAreMutable, boolean isReady, boolean isEnded, + boolean hasReadStreamToEnd) + throws ExoPlaybackException { + this.positionUs = TEST_POSITION_US; + this.playbackParameters = playbackParameters; + this.playbackParametersAreMutable = playbackParametersAreMutable; + this.isReady = isReady; + this.isEnded = isEnded; + if (!hasReadStreamToEnd) { + resetPosition(0); + } + } + + @Override + public long getPositionUs() { + return positionUs; + } + + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + if (playbackParametersAreMutable) { + this.playbackParameters = playbackParameters; + } + return this.playbackParameters; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; + } + + @Override + public boolean isReady() { + return isReady; + } + + } + +} From c4fe0e648242bf2d2613f217605c83bf7cc91aed Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 04:32:56 -0800 Subject: [PATCH 068/417] Add support for Dolby Atmos Issue: #2465 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176341309 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/Ac3Util.java | 170 +++++++++++++++++- .../exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 60 +++++-- .../android/exoplayer2/util/MimeTypes.java | 4 + .../dash/manifest/DashManifestParser.java | 22 ++- 6 files changed, 237 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a43a8eb0bc..d9ed3e5d2a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,8 @@ * New Cast extension: Simplifies toggling between local and Cast playbacks. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. +* Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e1a70e2579..e9ffab7ace 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -181,7 +185,14 @@ public final class Ac3Util { channelCount += 2; } } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, + String mimeType = MimeTypes.AUDIO_E_AC3; + if (data.bytesLeft() > 0) { + nextByte = data.readUnsignedByte(); + if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a + mimeType = MimeTypes.AUDIO_ATMOS; + } + } + return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); } @@ -198,29 +209,176 @@ public final class Ac3Util { boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; - int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + int streamType = STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; int sampleCount; + boolean lfeon; + int channelCount; if (isEac3) { - mimeType = MimeTypes.AUDIO_E_AC3; + // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. data.skipBits(16); // syncword streamType = data.readBits(2); data.skipBits(3); // substreamid frameSize = (data.readBits(11) + 1) * 2; int fscod = data.readBits(2); int audioBlocks; + int numblkscod; if (fscod == 3) { + numblkscod = 3; sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; audioBlocks = 6; } else { - int numblkscod = data.readBits(2); + numblkscod = data.readBits(2); audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod]; sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; } sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; acmod = data.readBits(3); + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + data.skipBits(5 + 5); // bsid, dialnorm + if (data.readBit()) { // compre + data.skipBits(8); // compr + } + if (acmod == 0) { + data.skipBits(5); // dialnorm2 + if (data.readBit()) { // compr2e + data.skipBits(8); // compr2 + } + } + if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape + data.skipBits(16); // chanmap + } + if (data.readBit()) { // mixmdate + if (acmod > 2) { + data.skipBits(2); // dmixmod + } + if ((acmod & 0x01) != 0 && acmod > 2) { + data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(6); // ltrtsurmixlev, lorosurmixlev + } + if (lfeon && data.readBit()) { // lfemixlevcode + data.skipBits(5); // lfemixlevcod + } + if (streamType == STREAM_TYPE_TYPE0) { + if (data.readBit()) { // pgmscle + data.skipBits(6); //pgmscl + } + if (acmod == 0 && data.readBit()) { // pgmscl2e + data.skipBits(6); // pgmscl2 + } + if (data.readBit()) { // extpgmscle + data.skipBits(6); // extpgmscl + } + int mixdef = data.readBits(2); + if (mixdef == 1) { + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + } else if (mixdef == 2) { + data.skipBits(12); // mixdata + } else if (mixdef == 3) { + int mixdeflen = data.readBits(5); + if (data.readBit()) { // mixdata2e + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + if (data.readBit()) { // extpgmlscle + data.skipBits(4); // extpgmlscl + } + if (data.readBit()) { // extpgmcscle + data.skipBits(4); // extpgmcscl + } + if (data.readBit()) { // extpgmrscle + data.skipBits(4); // extpgmrscl + } + if (data.readBit()) { // extpgmlsscle + data.skipBits(4); // extpgmlsscl + } + if (data.readBit()) { // extpgmrsscle + data.skipBits(4); // extpgmrsscl + } + if (data.readBit()) { // extpgmlfescle + data.skipBits(4); // extpgmlfescl + } + if (data.readBit()) { // dmixscle + data.skipBits(4); // dmixscl + } + if (data.readBit()) { // addche + if (data.readBit()) { // extpgmaux1scle + data.skipBits(4); // extpgmaux1scl + } + if (data.readBit()) { // extpgmaux2scle + data.skipBits(4); // extpgmaux2scl + } + } + } + if (data.readBit()) { // mixdata3e + data.skipBits(5); // spchdat + if (data.readBit()) { // addspchdate + data.skipBits(5 + 2); // spchdat1, spchan1att + if (data.readBit()) { // addspdat1e + data.skipBits(5 + 3); // spchdat2, spchan2att + } + } + } + data.skipBits(8 * (mixdeflen + 2)); // mixdata + data.byteAlign(); // mixdatafill + } + if (acmod < 2) { + if (data.readBit()) { // paninfoe + data.skipBits(8 + 6); // panmean, paninfo + } + if (acmod == 0) { + if (data.readBit()) { // paninfo2e + data.skipBits(8 + 6); // panmean2, paninfo2 + } + } + } + if (data.readBit()) { // frmmixcfginfoe + if (numblkscod == 0) { + data.skipBits(5); // blkmixcfginfo[0] + } else { + for (int blk = 0; blk < audioBlocks; blk++) { + if (data.readBit()) { // blkmixcfginfoe + data.skipBits(5); // blkmixcfginfo[blk] + } + } + } + } + } + } + if (data.readBit()) { // infomdate + data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs + if (acmod == 2) { + data.skipBits(2 + 2); // dsurmod, dheadphonmod + } + if (acmod >= 6) { + data.skipBits(2); // dsurexmod + } + if (data.readBit()) { // audioprodie + data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp + } + if (acmod == 0 && data.readBit()) { // audioprodi2e + data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2 + } + if (fscod < 3) { + data.skipBit(); // sourcefscod + } + } + if (streamType == 0 && numblkscod != 3) { + data.skipBit(); // convsync + } + if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid + data.skipBits(6); // frmsizecod + } + mimeType = MimeTypes.AUDIO_E_AC3; + if (data.readBit()) { // addbsie + int addbsil = data.readBits(6); + if (addbsil == 1 && data.readBits(8) == 1) { // addbsi + mimeType = MimeTypes.AUDIO_ATMOS; + } + } } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 @@ -240,9 +398,9 @@ public final class Ac3Util { } sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } - boolean lfeon = data.readBit(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 6a1c566faf..8383bfb8d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 8; + private static final int HEADER_SIZE = 128; private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index f75ce5a9e5..7ae8eb3cd4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -20,6 +20,7 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -120,7 +121,7 @@ public final class MediaCodecUtil { * exists. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) + public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) throws DecoderQueryException { List decoderInfos = getDecoderInfos(mimeType, secure); return decoderInfos.isEmpty() ? null : decoderInfos.get(0); @@ -140,27 +141,34 @@ public final class MediaCodecUtil { public static synchronized List getDecoderInfos(String mimeType, boolean secure) throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); - List decoderInfos = decoderInfosCache.get(key); - if (decoderInfos != null) { - return decoderInfos; + List cachedDecoderInfos = decoderInfosCache.get(key); + if (cachedDecoderInfos != null) { + return cachedDecoderInfos; } MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); } } + if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) { + // E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D. + CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure); + ArrayList eac3DecoderInfos = + getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); + decoderInfos.addAll(eac3DecoderInfos); + } applyWorkarounds(decoderInfos); - decoderInfos = Collections.unmodifiableList(decoderInfos); - decoderInfosCache.put(key, decoderInfos); - return decoderInfos; + List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, unmodifiableDecoderInfos); + return unmodifiableDecoderInfos; } /** @@ -212,10 +220,21 @@ public final class MediaCodecUtil { // Internal methods. - private static List getDecoderInfosInternal( - CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + /** + * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * {@code mediaCodecList}. + * + * @param key The codec key. + * @param mediaCodecList The codec list. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return The codec information for usable codecs matching the specified key. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + private static ArrayList getDecoderInfosInternal(CodecKey key, + MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { try { - List decoderInfos = new ArrayList<>(); + ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); @@ -223,7 +242,7 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String codecName = codecInfo.getName(); - if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { + if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) { for (String supportedType : codecInfo.getSupportedTypes()) { if (supportedType.equalsIgnoreCase(mimeType)) { try { @@ -265,9 +284,16 @@ public final class MediaCodecUtil { /** * Returns whether the specified codec is usable for decoding on the current device. + * + * @param info The codec information. + * @param name The name of the codec + * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return Whether the specified codec is usable for decoding on the current device. */ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit) { + boolean secureDecodersExplicit, String requestedMimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -356,6 +382,12 @@ public final class MediaCodecUtil { return false; } + // MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041]. + if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType) + && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + return false; + } + return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index c29a4c3717..a68e0142d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -51,6 +51,7 @@ public final class MimeTypes { public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_ATMOS = BASE_TYPE_AUDIO + "/eac3-joc"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -195,6 +196,8 @@ public final class MimeTypes { return MimeTypes.AUDIO_AC3; } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { return MimeTypes.AUDIO_E_AC3; + } else if (codec.startsWith("ec+3")) { + return MimeTypes.AUDIO_ATMOS; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -252,6 +255,7 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_ATMOS: return C.ENCODING_E_AC3; case MimeTypes.AUDIO_DTS: return C.ENCODING_DTS; 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 1868a54d17..d3906acdf6 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 @@ -460,6 +460,7 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -487,12 +488,14 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, - adaptationSetAccessibilityDescriptors, codecs); + adaptationSetAccessibilityDescriptors, codecs, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, @@ -502,9 +505,12 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, - String codecs) { + String codecs, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } if (MimeTypes.isVideo(sampleMimeType)) { return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, width, height, frameRate, null, selectionFlags); @@ -1045,6 +1051,18 @@ public class DashManifestParser extends DefaultHandler return Format.NO_VALUE; } + protected static String parseEac3SupplementalProperties(List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + String schemeIdUri = descriptor.schemeIdUri; + if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value)) { + return MimeTypes.AUDIO_ATMOS; + } + } + return MimeTypes.AUDIO_E_AC3; + } + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { float frameRate = defaultValue; String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); From a9d91b3387262e7fd954db6346b44048616af76c Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Tue, 21 Nov 2017 10:59:04 +0000 Subject: [PATCH 069/417] Fix initializationData check for SSA subtitles --- .../java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index d2f5a67c27..12aa1e97d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -59,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null) { + if (initializationData != null && initializationData.size() > 0) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From e45907193cb1ed7bb9af337aa7e941f8e834ae64 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 06:22:54 -0800 Subject: [PATCH 070/417] Allow human readable strings as DRM intent extras. Issue:#3478 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176351086 --- .../android/exoplayer2/demo/DemoUtil.java | 31 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 12 ++++--- .../demo/SampleChooserActivity.java | 27 +++++----------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index f9e9c34158..5ff7c5cb40 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -16,14 +16,43 @@ package com.google.android.exoplayer2.demo; import android.text.TextUtils; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.util.Locale; +import java.util.UUID; /** * Utility methods for demo application. */ -/*package*/ final class DemoUtil { +/* package */ final class DemoUtil { + + /** + * Derives a DRM {@link UUID} from {@code drmScheme}. + * + * @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or + * {@code "clearkey"}. + * @return The derived {@link UUID}. + * @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}. + */ + public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException { + switch (Util.toLowerInvariant(drmScheme)) { + case "widevine": + return C.WIDEVINE_UUID; + case "playready": + return C.PLAYREADY_UUID; + case "clearkey": + return C.CLEARKEY_UUID; + default: + try { + return UUID.fromString(drmScheme); + } catch (RuntimeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); + } + } + } /** * Builds a track name for display. 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 ca253db809..efde775176 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 @@ -83,7 +83,7 @@ import java.util.UUID; public class PlayerActivity extends Activity implements OnClickListener, PlaybackControlView.VisibilityListener { - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; public static final String DRM_MULTI_SESSION = "drm_multi_session"; @@ -98,6 +98,9 @@ public class PlayerActivity extends Activity implements OnClickListener, public static final String EXTENSION_LIST_EXTRA = "extension_list"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + // For backwards compatibility. + private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { @@ -256,10 +259,8 @@ public class PlayerActivity extends Activity implements OnClickListener, lastSeenTrackGroupArray = null; eventLogger = new EventLogger(trackSelector); - UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) - ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; - if (drmSchemeUuid != null) { + if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); @@ -268,6 +269,9 @@ public class PlayerActivity extends Activity implements OnClickListener, errorStringId = R.string.error_drm_not_supported; } else { try { + String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA + : DRM_SCHEME_UUID_EXTRA; + UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 1f84b1f29c..308bab2a3b 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -32,8 +32,8 @@ import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity { break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmUuid = getDrmUuid(reader.nextString()); + try { + drmUuid = DemoUtil.getDrmUuid(reader.nextString()); + } catch (UnsupportedDrmException e) { + throw new ParserException(e); + } break; case "drm_license_url": Assertions.checkState(!insidePlaylist, @@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity { return group; } - private UUID getDrmUuid(String typeString) throws ParserException { - switch (Util.toLowerInvariant(typeString)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "clearkey": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(typeString); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + typeString); - } - } - } - } private static final class SampleAdapter extends BaseExpandableListAdapter { @@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity { public void updateIntent(Intent intent) { Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); + intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString()); intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); From 0de57cbfae7165dd3bb829e323d089cd312b4b1b Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 20 Nov 2017 08:22:19 -0800 Subject: [PATCH 071/417] Allow more flexible loading strategy when loading multiple sub streams. Currently for a DASH ChunkSource that consists of multiple sub-streams, we always use a CompositeSequenceableLoader, which only allows the furthest behind loader or any loader that are behind current playback position to continue loading. This changes allow clients to have more flexibility when deciding the loading strategy: - They can construct a different kind of composite SequenceableLoader from the sub-loaders, and use it by injecting a different CompositeSequeableLoaderFactory accordingly. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176363870 --- RELEASENOTES.md | 8 +++- .../source/CompositeSequenceableLoader.java | 6 +-- .../CompositeSequenceableLoaderFactory.java | 31 +++++++++++++ ...ultCompositeSequenceableLoaderFactory.java | 29 ++++++++++++ .../exoplayer2/source/MergingMediaPeriod.java | 16 ++++--- .../exoplayer2/source/MergingMediaSource.java | 15 ++++++- .../source/dash/DashMediaPeriod.java | 23 ++++++---- .../source/dash/DashMediaSource.java | 42 ++++++++++++++--- .../exoplayer2/source/hls/HlsMediaPeriod.java | 20 ++++++--- .../exoplayer2/source/hls/HlsMediaSource.java | 42 ++++++++++++++++- .../source/smoothstreaming/SsMediaPeriod.java | 19 +++++--- .../source/smoothstreaming/SsMediaSource.java | 45 +++++++++++++++---- 12 files changed, 244 insertions(+), 52 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d9ed3e5d2a..41748fa10d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,8 +2,12 @@ ### dev-v2 (not yet released) ### -* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource, SingleSampleMediaSource. +* Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, + `SsMediaSource.Builder`, and `MergingMediaSource`. +* Add Builder to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource`, `SingleSampleMediaSource`. * DASH: * Support in-MPD EventStream. * Allow a back-buffer of media to be retained behind the current playback diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index a85d589762..e9a187a747 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -20,9 +20,9 @@ import com.google.android.exoplayer2.C; /** * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s. */ -public final class CompositeSequenceableLoader implements SequenceableLoader { +public class CompositeSequenceableLoader implements SequenceableLoader { - private final SequenceableLoader[] loaders; + protected final SequenceableLoader[] loaders; public CompositeSequenceableLoader(SequenceableLoader[] loaders) { this.loaders = loaders; @@ -53,7 +53,7 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { } @Override - public final boolean continueLoading(long positionUs) { + public boolean continueLoading(long positionUs) { boolean madeProgress = false; boolean madeProgressThisIteration; do { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java new file mode 100644 index 0000000000..b4a266feef --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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; + +/** + * A factory to create composite {@link SequenceableLoader}s. + */ +public interface CompositeSequenceableLoaderFactory { + + /** + * Creates a composite {@link SequenceableLoader}. + * + * @param loaders The sub-loaders that make up the {@link SequenceableLoader} to be built. + * @return A composite {@link SequenceableLoader} that comprises the given loaders. + */ + SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java new file mode 100644 index 0000000000..759b0824af --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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; + +/** + * Default implementation of {@link CompositeSequenceableLoaderFactory}. + */ +public final class DefaultCompositeSequenceableLoaderFactory + implements CompositeSequenceableLoaderFactory { + + @Override + public SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders) { + return new CompositeSequenceableLoader(loaders); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 786a4693d0..bd37b5efec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -30,15 +30,18 @@ import java.util.IdentityHashMap; public final MediaPeriod[] periods; private final IdentityHashMap streamPeriodIndices; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private int pendingChildPrepareCount; private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; - private SequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; - public MergingMediaPeriod(MediaPeriod... periods) { + public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + MediaPeriod... periods) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; streamPeriodIndices = new IdentityHashMap<>(); } @@ -124,7 +127,8 @@ import java.util.IdentityHashMap; // Update the local state. enabledPeriods = new MediaPeriod[enabledPeriodsList.size()]; enabledPeriodsList.toArray(enabledPeriods); - sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods); return positionUs; } @@ -137,12 +141,12 @@ import java.util.IdentityHashMap; @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -168,7 +172,7 @@ import java.util.IdentityHashMap; @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 1550970e47..ea0274796f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -74,6 +74,7 @@ public final class MergingMediaSource implements MediaSource { private final MediaSource[] mediaSources; private final ArrayList pendingTimelineSources; private final Timeline.Window window; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Listener listener; private Timeline primaryTimeline; @@ -85,7 +86,19 @@ public final class MergingMediaSource implements MediaSource { * @param mediaSources The {@link MediaSource}s to merge. */ public MergingMediaSource(MediaSource... mediaSources) { + this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + } + + /** + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @param mediaSources The {@link MediaSource}s to merge. + */ + public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + MediaSource... mediaSources) { this.mediaSources = mediaSources; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); window = new Timeline.Window(); periodCount = PERIOD_COUNT_UNSET; @@ -121,7 +134,7 @@ public final class MergingMediaSource implements MediaSource { for (int i = 0; i < periods.length; i++) { periods[i] = mediaSources[i].createPeriod(id, allocator); } - return new MergingMediaPeriod(periods); + return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); } @Override 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 5a60ee46ae..70fba4dd00 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 @@ -21,7 +21,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; @@ -64,19 +64,21 @@ import java.util.Map; private final Allocator allocator; private final TrackGroupArray trackGroups; private final TrackGroupInfo[] trackGroupInfos; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; - private CompositeSequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; private DashManifest manifest; private int periodIndex; private List eventStreams; public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, - LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { + LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.id = id; this.manifest = manifest; this.periodIndex = periodIndex; @@ -86,9 +88,11 @@ import java.util.Map; this.elapsedRealtimeOffset = elapsedRealtimeOffset; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; sampleStreams = newSampleStreamArray(0); eventSampleStreams = new EventSampleStream[0]; - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); eventStreams = period.eventStreams; Pair result = buildTrackGroups(period.adaptationSets, @@ -163,7 +167,8 @@ import java.util.Map; primarySampleStreams.values().toArray(sampleStreams); eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; eventSampleStreamList.toArray(eventSampleStreams); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } @@ -267,12 +272,12 @@ import java.util.Map; @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -282,7 +287,7 @@ import java.util.Map; @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override 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 a82b5af583..68d39b5a18 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 @@ -28,8 +28,11 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; @@ -71,6 +74,7 @@ public final class DashMediaSource implements MediaSource { private ParsingLoadable.Parser manifestParser; private AdaptiveMediaSourceEventListener eventListener; private Handler eventHandler; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private long livePresentationDelayMs; @@ -171,6 +175,22 @@ public final class DashMediaSource implements MediaSource { return this; } + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of + * {@link DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @return This builder. + */ + public Builder setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + return this; + } + /** * Builds a new {@link DashMediaSource} using the current parameters. *

      @@ -186,9 +206,12 @@ public final class DashMediaSource implements MediaSource { if (loadableManifestUri && manifestParser == null) { manifestParser = new DashManifestParser(); } + if (compositeSequenceableLoaderFactory == null) { + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + } return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, - eventListener); + chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } } @@ -226,6 +249,7 @@ public final class DashMediaSource implements MediaSource { private final boolean sideloadedManifest; private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; @@ -280,7 +304,8 @@ public final class DashMediaSource implements MediaSource { public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, + this(manifest, null, null, null, chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); } @@ -356,14 +381,16 @@ public final class DashMediaSource implements MediaSource { long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, - minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } private DashMediaSource(DashManifest manifest, Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, + DashChunkSource.Factory chunkSourceFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this.manifest = manifest; this.manifestUri = manifestUri; @@ -372,6 +399,7 @@ public final class DashMediaSource implements MediaSource { this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; sideloadedManifest = manifest != null; eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestUriLock = new Object(); @@ -438,7 +466,7 @@ public final class DashMediaSource implements MediaSource { manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher, - elapsedRealtimeOffsetMs, loaderErrorThrower, allocator); + elapsedRealtimeOffsetMs, loaderErrorThrower, allocator, compositeSequenceableLoaderFactory); periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index bc2b92cfe8..b6c74d61bb 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -20,9 +20,10 @@ import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; @@ -53,23 +54,26 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private int pendingPrepareCount; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; - private CompositeSequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, - EventDispatcher eventDispatcher, Allocator allocator) { + EventDispatcher eventDispatcher, Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); @@ -178,7 +182,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Update the local state. enabledSampleStreamWrappers = Arrays.copyOf(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount); - sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader( + enabledSampleStreamWrappers); return positionUs; } @@ -191,12 +197,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -206,7 +212,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override 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 3f28981f0e..a412b8c3e9 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 @@ -23,8 +23,11 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; @@ -59,6 +62,8 @@ public final class HlsMediaSource implements MediaSource, private ParsingLoadable.Parser playlistParser; private AdaptiveMediaSourceEventListener eventListener; private Handler eventHandler; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private int minLoadableRetryCount; private boolean isBuildCalled; @@ -150,6 +155,22 @@ public final class HlsMediaSource implements MediaSource, return this; } + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of + * {@link DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @return This builder. + */ + public Builder setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + return this; + } + /** * Builds a new {@link HlsMediaSource} using the current parameters. *

      @@ -167,8 +188,12 @@ public final class HlsMediaSource implements MediaSource, if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } + if (compositeSequenceableLoaderFactory == null) { + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + } return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - minLoadableRetryCount, eventHandler, eventListener, playlistParser); + compositeSequenceableLoaderFactory, minLoadableRetryCount, eventHandler, eventListener, + playlistParser); } } @@ -181,6 +206,7 @@ public final class HlsMediaSource implements MediaSource, private final HlsExtractorFactory extractorFactory; private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final ParsingLoadable.Parser playlistParser; @@ -242,11 +268,23 @@ public final class HlsMediaSource implements MediaSource, HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { + this(manifestUri, dataSourceFactory, extractorFactory, + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler, + eventListener, playlistParser); + } + + private HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + HlsExtractorFactory extractorFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.playlistParser = playlistParser; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -268,7 +306,7 @@ public final class HlsMediaSource implements MediaSource, public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory, - minLoadableRetryCount, eventDispatcher, allocator); + minLoadableRetryCount, eventDispatcher, allocator, compositeSequenceableLoaderFactory); } @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 3c51abcd49..c079a36d62 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -19,7 +19,7 @@ import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; @@ -49,13 +49,15 @@ import java.util.ArrayList; private final Allocator allocator; private final TrackGroupArray trackGroups; private final TrackEncryptionBox[] trackEncryptionBoxes; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; - private CompositeSequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { this.chunkSourceFactory = chunkSourceFactory; @@ -63,6 +65,7 @@ import java.util.ArrayList; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; trackGroups = buildTrackGroups(manifest); ProtectionElement protectionElement = manifest.protectionElement; @@ -76,7 +79,8 @@ import java.util.ArrayList; } this.manifest = manifest; sampleStreams = newSampleStreamArray(0); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); } public void updateManifest(SsManifest manifest) { @@ -133,7 +137,8 @@ import java.util.ArrayList; } sampleStreams = newSampleStreamArray(sampleStreamsList.size()); sampleStreamsList.toArray(sampleStreams); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } @@ -146,12 +151,12 @@ import java.util.ArrayList; @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -161,7 +166,7 @@ import java.util.ArrayList; @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override 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 5a93847428..a4b601aafe 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 @@ -26,8 +26,11 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; @@ -65,6 +68,7 @@ public final class SsMediaSource implements MediaSource, private ParsingLoadable.Parser manifestParser; private AdaptiveMediaSourceEventListener eventListener; private Handler eventHandler; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private long livePresentationDelayMs; @@ -162,6 +166,22 @@ public final class SsMediaSource implements MediaSource, return this; } + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of + * {@link DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @return This builder. + */ + public Builder setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + return this; + } + /** * Builds a new {@link SsMediaSource} using the current parameters. *

      @@ -177,9 +197,12 @@ public final class SsMediaSource implements MediaSource, if (loadableManifestUri && manifestParser == null) { manifestParser = new SsManifestParser(); } + if (compositeSequenceableLoaderFactory == null) { + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + } return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, - eventListener); + chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } } @@ -206,6 +229,7 @@ public final class SsMediaSource implements MediaSource, private final Uri manifestUri; private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; @@ -252,7 +276,8 @@ public final class SsMediaSource implements MediaSource, public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, + this(manifest, null, null, null, chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); } @@ -324,14 +349,16 @@ public final class SsMediaSource implements MediaSource, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, - minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } private SsMediaSource(SsManifest manifest, Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, + SsChunkSource.Factory chunkSourceFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; @@ -341,6 +368,7 @@ public final class SsMediaSource implements MediaSource, this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); @@ -372,8 +400,9 @@ public final class SsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, - eventDispatcher, manifestLoaderErrorThrower, allocator); + SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, + compositeSequenceableLoaderFactory, minLoadableRetryCount, eventDispatcher, + manifestLoaderErrorThrower, allocator); mediaPeriods.add(period); return period; } From 2a685da4eb6dec725f737803d223ad9e5d27fa75 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 08:42:34 -0800 Subject: [PATCH 072/417] Improve robustness of ImaAdsLoader Remove an assertion that there was a call to pause content between two content -> ad transitions. Also, only use the player position for resuming an ad on reattaching if the player is currently playing an ad, in case IMA pauses content before the player actually transitions to an ad. Issue: #3430 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176365842 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 41748fa10d..81a45c9c24 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -72,6 +72,9 @@ ([#3303](https://github.com/google/ExoPlayer/issues/3303)). * Ignore seeks if an ad is playing ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused between + content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). * UI: * Allow specifying a `Drawable` for the `TimeBar` scrubber ([#3337](https://github.com/google/ExoPlayer/issues/3337)). 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 0b11a97f84..5b61db0264 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 @@ -260,7 +260,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void detachPlayer() { if (adsManager != null && imaPausedContent) { - adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); + adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adsManager.pause(); } lastAdProgress = getAdProgress(); @@ -628,7 +628,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { From 3656230cb1b93092fb5ee698fe260ff4dfeb976f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:48:10 -0800 Subject: [PATCH 073/417] Use MediaSourceTestRunner in additional source tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366471 --- .../source/ClippingMediaSourceTest.java | 13 +++++--- .../source/ConcatenatingMediaSourceTest.java | 12 ++++--- .../DynamicConcatenatingMediaSourceTest.java | 14 ++++----- .../source/LoopingMediaSourceTest.java | 14 ++++++--- .../testutil/MediaSourceTestRunner.java | 30 ++++++++++++------ .../android/exoplayer2/testutil/TestUtil.java | 31 ------------------- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 5e615dbc7f..3c870f06f4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; /** @@ -123,9 +123,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new ClippingMediaSource(mediaSource, startMs, endMs)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 429325defc..1ca32be46d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -33,8 +32,6 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -211,7 +208,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); @@ -241,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - return TestUtil.extractTimelineFromMediaSource(mediaSource); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 536180fafc..16c9e1a17c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -39,19 +39,19 @@ import org.mockito.Mockito; */ public final class DynamicConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - private DynamicConcatenatingMediaSource mediaSource; private MediaSourceTestRunner testRunner; @Override - public void setUp() { + public void setUp() throws Exception { + super.setUp(); mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + testRunner = new MediaSourceTestRunner(mediaSource, null); } @Override - public void tearDown() { + public void tearDown() throws Exception { + super.tearDown(); testRunner.release(); } @@ -623,7 +623,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { finishedCondition.open(); } }); - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); } public void release() { @@ -656,7 +656,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public Timeline assertTimelineChangeBlocking() { - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); if (error != null) { throw error; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 79f646b5c4..6f69923ea2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -110,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase { * the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new LoopingMediaSource(mediaSource, loopCount)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } - diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index df1282c7e1..235c04bef5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -39,7 +41,8 @@ import java.util.concurrent.TimeUnit; */ public class MediaSourceTestRunner { - private final long timeoutMs; + public static final int TIMEOUT_MS = 10000; + private final StubExoPlayer player; private final MediaSource mediaSource; private final MediaSourceListener mediaSourceListener; @@ -53,12 +56,10 @@ public class MediaSourceTestRunner { /** * @param mediaSource The source under test. * @param allocator The allocator to use during the test run. - * @param timeoutMs The timeout for operations in milliseconds. */ - public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator) { this.mediaSource = mediaSource; this.allocator = allocator; - this.timeoutMs = timeoutMs; playbackThread = new HandlerThread("PlaybackThread"); playbackThread.start(); Looper playbackLooper = playbackThread.getLooper(); @@ -74,15 +75,24 @@ public class MediaSourceTestRunner { * @param runnable The {@link Runnable} to run. */ public void runOnPlaybackThread(final Runnable runnable) { + final Throwable[] throwable = new Throwable[1]; final ConditionVariable finishedCondition = new ConditionVariable(); playbackHandler.post(new Runnable() { @Override public void run() { - runnable.run(); - finishedCondition.open(); + try { + runnable.run(); + } catch (Throwable e) { + throwable[0] = e; + } finally { + finishedCondition.open(); + } } }); - assertTrue(finishedCondition.block(timeoutMs)); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (throwable[0] != null) { + Util.sneakyThrow(throwable[0]); + } } /** @@ -200,7 +210,7 @@ public class MediaSourceTestRunner { */ public Timeline assertTimelineChangeBlocking() { try { - timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + timeline = timelines.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(timeline); // Null indicates the poll timed out. assertNoTimelineChange(); return timeline; @@ -231,12 +241,12 @@ public class MediaSourceTestRunner { private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); - assertTrue(preparedCondition.block(timeoutMs)); + assertTrue(preparedCondition.block(TIMEOUT_MS)); // MediaSource is supposed to support multiple calls to createPeriod with the same id without an // intervening call to releasePeriod. MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); - assertTrue(secondPreparedCondition.block(timeoutMs)); + assertTrue(secondPreparedCondition.block(TIMEOUT_MS)); // Release the periods. releasePeriod(mediaPeriod); releasePeriod(secondMediaPeriod); 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 9ee181024c..d10b8a8269 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 @@ -19,10 +19,7 @@ import android.app.Instrumentation; import android.content.Context; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -143,34 +140,6 @@ public class TestUtil { return new String(getByteArray(instrumentation, fileName)); } - /** - * Extracts the timeline from a media source. - */ - // TODO: Remove this method and transition callers over to MediaSourceTestRunner. - public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { - class TimelineListener implements Listener { - private Timeline timeline; - @Override - public synchronized void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - this.timeline = timeline; - this.notify(); - } - } - TimelineListener listener = new TimelineListener(); - mediaSource.prepareSource(null, true, listener); - synchronized (listener) { - while (listener.timeline == null) { - try { - listener.wait(); - } catch (InterruptedException e) { - Assert.fail(e.getMessage()); - } - } - } - return listener.timeline; - } - /** * Asserts that data read from a {@link DataSource} matches {@code expected}. * From c06fe73b66a9664d68adce4a951bfe0e4258fcbd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:50:09 -0800 Subject: [PATCH 074/417] Bump target API level to 27 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366693 --- constants.gradle | 4 ++-- demos/cast/src/main/AndroidManifest.xml | 2 +- demos/ima/src/main/AndroidManifest.xml | 2 +- demos/main/src/main/AndroidManifest.xml | 2 +- extensions/cronet/src/androidTest/AndroidManifest.xml | 2 +- extensions/flac/src/androidTest/AndroidManifest.xml | 2 +- extensions/opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- library/core/src/androidTest/AndroidManifest.xml | 2 +- library/dash/src/androidTest/AndroidManifest.xml | 2 +- library/hls/src/androidTest/AndroidManifest.xml | 2 +- library/smoothstreaming/src/androidTest/AndroidManifest.xml | 2 +- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/constants.gradle b/constants.gradle index 2a7754d65c..bad69389a5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -17,8 +17,8 @@ project.ext { // However, please note that the core media playback functionality provided // by the library requires API level 16 or greater. minSdkVersion = 14 - compileSdkVersion = 26 - targetSdkVersion = 26 + compileSdkVersion = 27 + targetSdkVersion = 27 buildToolsVersion = '26.0.2' testSupportLibraryVersion = '0.5' supportLibraryVersion = '27.0.0' diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 11f8e39b53..8aaef5f8ce 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:versionName="2.6.0"> - + diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5252d2feeb..f14feeda74 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:versionName="2.6.0"> - + diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index d041e24d80..ec8016e8a3 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + - + - + - + - + - + - + - + - + - + Date: Mon, 20 Nov 2017 08:56:23 -0800 Subject: [PATCH 075/417] Report additional position discontinuities - Properly report internal discontinuities - Add DISCONTINUITY_REASON_SEEK_ADJUSTMENT to distinguish seek adjustments from other internal discontinuity events ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176367365 --- RELEASENOTES.md | 4 + .../android/exoplayer2/demo/EventLogger.java | 2 + .../android/exoplayer2/ExoPlayerTest.java | 89 +++++++++++++++++-- .../android/exoplayer2/ExoPlayerImpl.java | 15 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 12 ++- .../com/google/android/exoplayer2/Player.java | 9 +- .../testutil/ExoPlayerTestRunner.java | 34 ++++--- 7 files changed, 139 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 81a45c9c24..6438cbdd68 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### dev-v2 (not yet released) ### +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index d72f747940..9233b016f5 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -499,6 +499,8 @@ import java.util.Locale; return "PERIOD_TRANSITION"; case Player.DISCONTINUITY_REASON_SEEK: return "SEEK"; + case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return "SEEK_ADJUSTMENT"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 95d5d96163..f0f1c23c2b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -23,12 +23,14 @@ import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; +import com.google.android.exoplayer2.upstream.Allocator; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -56,7 +58,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); @@ -73,7 +75,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setManifest(manifest).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); testRunner.assertManifestsEqual(manifest); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); @@ -91,7 +93,9 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -136,7 +140,9 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(videoRenderer, audioRenderer) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); @@ -198,7 +204,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); // The first source's preparation completed with a non-empty timeline. When the player was // re-prepared with the second source, it immediately exposed an empty timeline, but the source // info refresh from the second source was suppressed as we re-prepared with the third source. @@ -226,6 +232,16 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertTrue(renderer.isEnded); } @@ -250,6 +266,12 @@ public final class ExoPlayerTest extends TestCase { .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); assertTrue(renderer.isEnded); } @@ -300,6 +322,63 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testSeekDiscontinuity() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuity") + .seek(10).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setTimeline(timeline) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + } + + public void testSeekDiscontinuityWithAdjustment() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray) { + @Override + public long seekToUs(long positionUs) { + return positionUs + 10; // Adjusts the requested seek position. + } + }; + } + }; + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuityAdjust") + .waitForPlaybackState(Player.STATE_READY).seek(10).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK, + Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } + + public void testInternalDiscontinuity() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray) { + boolean discontinuityRead; + @Override + public long readDiscontinuity() { + if (!discontinuityRead) { + discontinuityRead = true; + return 10; // Return a discontinuity. + } + return C.TIME_UNSET; + } + }; + } + }; + ActionSchedule actionSchedule = new ActionSchedule.Builder("testInternalDiscontinuity") + .waitForPlaybackState(Player.STATE_READY).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); + } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index d28f72e739..ff00f9de91 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -467,7 +467,8 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false); + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { @@ -485,11 +486,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted); + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT); break; } case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true); + @DiscontinuityReason int discontinuityReason = msg.arg1; + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { @@ -515,7 +518,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, - boolean positionDiscontinuity) { + boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareAcks -= prepareAcks; pendingSeekAcks -= seekAcks; @@ -536,9 +539,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } if (positionDiscontinuity) { for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity( - seekAcks > 0 ? DISCONTINUITY_REASON_INTERNAL : DISCONTINUITY_REASON_PERIOD_TRANSITION - ); + listener.onPositionDiscontinuity(positionDiscontinuityReason); } } } 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 e4bb11c51f..d7b2b4cbf4 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 @@ -514,6 +514,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); @@ -875,7 +879,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { - playbackInfo.positionUs = periodPositionUs; + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); resetRendererPosition(periodPositionUs); } @@ -1262,7 +1269,8 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index af653ec2bd..dc703f924a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -243,7 +243,7 @@ public interface Player { */ @Retention(RetentionPolicy.SOURCE) @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_INTERNAL}) + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may @@ -254,10 +254,15 @@ public interface Player { * Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; + /** + * Seek adjustment due to being unable to seek to the requested position or because the seek was + * permitted to be inexact. + */ + int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; /** * Discontinuity introduced internally by the source. */ - int DISCONTINUITY_REASON_INTERNAL = 2; + int DISCONTINUITY_REASON_INTERNAL = 3; /** * Register a listener to receive events from the player. The listener's methods will be called on diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 591e63dc5b..6730bf1c7f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -38,6 +39,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -261,14 +263,14 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public ExoPlayerTestRunner build() { if (supportedFormats == null) { - supportedFormats = new Format[] { VIDEO_FORMAT }; + supportedFormats = new Format[] {VIDEO_FORMAT}; } if (trackSelector == null) { trackSelector = new DefaultTrackSelector(); } if (renderersFactory == null) { if (renderers == null) { - renderers = new Renderer[] { new FakeRenderer(supportedFormats) }; + renderers = new Renderer[] {new FakeRenderer(supportedFormats)}; } renderersFactory = new RenderersFactory() { @Override @@ -317,11 +319,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final LinkedList timelines; private final LinkedList manifests; private final LinkedList periodIndices; + private final ArrayList discontinuityReasons; private SimpleExoPlayer player; private Exception exception; private TrackGroupArray trackGroups; - private int positionDiscontinuityCount; private boolean playerWasPrepared; private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, @@ -337,6 +339,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.timelines = new LinkedList<>(); this.manifests = new LinkedList<>(); this.periodIndices = new LinkedList<>(); + this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); @@ -439,13 +442,24 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } /** - * Asserts that the number of reported discontinuities by - * {@link Player.EventListener#onPositionDiscontinuity(int)} is equal to the provided number. - * - * @param expectedCount The expected number of position discontinuities. + * Asserts that {@link Player.EventListener#onPositionDiscontinuity(int)} was not called. */ - public void assertPositionDiscontinuityCount(int expectedCount) { - Assert.assertEquals(expectedCount, positionDiscontinuityCount); + public void assertNoPositionDiscontinuities() { + Assert.assertTrue(discontinuityReasons.isEmpty()); + } + + /** + * Asserts that the discontinuity reasons reported by + * {@link Player.EventListener#onPositionDiscontinuity(int)} are equal to the provided values. + * + * @param discontinuityReasons The expected discontinuity reasons. + */ + public void assertPositionDiscontinuityReasonsEqual( + @DiscontinuityReason int... discontinuityReasons) { + Assert.assertEquals(discontinuityReasons.length, this.discontinuityReasons.size()); + for (int i = 0; i < discontinuityReasons.length; i++) { + Assert.assertEquals(discontinuityReasons[i], (int) this.discontinuityReasons.get(i)); + } } /** @@ -522,7 +536,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - positionDiscontinuityCount++; + discontinuityReasons.add(reason); periodIndices.add(player.getCurrentPeriodIndex()); } From 13b595ed3908588810dce32811830e601bc2548e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 09:35:01 -0800 Subject: [PATCH 076/417] Don't do work after track selection when in ended state This causes the player to report that it's started loading when in the ended state. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176371892 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 d7b2b4cbf4..1732026540 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 @@ -878,7 +878,7 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (periodPositionUs != playbackInfo.positionUs) { + if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, @@ -923,9 +923,11 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - maybeContinueLoading(); - updatePlaybackPositions(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + if (state != Player.STATE_ENDED) { + maybeContinueLoading(); + updatePlaybackPositions(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } } private boolean isTimelineReady(long playingPeriodDurationUs) { From aac53cac56143e96bebcc0243f74cebe3e52b817 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 01:33:48 -0800 Subject: [PATCH 077/417] Add reason to onTimelineChanged. Currently onTimelineChanged doesn't allow to distinguish easily between the different reasons why it's being called. Especially, finding out whether a new media source has been prepared or the current source refreshed dynamically was impossible without tightly coupling the player operations with the listener. The new reasons provide this disdinction by either indicating a newly initialized media source, a dynamic update to an existing timeline or manifest, or a reset of the player (which usually results in an empty timeline). The original onTimelineChanged method without reason is kept in the DefaultEventListener as deprecated to prevent the need to update all existing listeners in one go. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176478701 --- RELEASENOTES.md | 2 + .../android/exoplayer2/demo/EventLogger.java | 20 ++++++++- .../exoplayer2/ext/cast/CastPlayer.java | 7 +++- .../exoplayer2/ext/ima/ImaAdsLoader.java | 7 ++-- .../ext/leanback/LeanbackPlayerAdapter.java | 4 +- .../mediasession/MediaSessionConnector.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 30 +++++++++++++- .../android/exoplayer2/ExoPlayerImpl.java | 12 ++++-- .../com/google/android/exoplayer2/Player.java | 41 +++++++++++++++++-- .../exoplayer2/ui/PlaybackControlView.java | 5 ++- .../android/exoplayer2/testutil/Action.java | 5 ++- .../testutil/ExoPlayerTestRunner.java | 18 +++++++- .../testutil/FakeSimpleExoPlayer.java | 3 +- 13 files changed, 136 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6438cbdd68..c01f2c29ee 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* Added a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 9233b016f5..473a0d3441 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -116,10 +116,12 @@ import java.util.Locale; } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); - Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); + Log.d(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount + + ", reason=" + getTimelineChangeReasonString(reason)); for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getPeriod(i, period); Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); @@ -507,4 +509,18 @@ import java.util.Locale; return "?"; } } + + private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { + switch (reason) { + case Player.TIMELINE_CHANGE_REASON_PREPARED: + return "PREPARED"; + case Player.TIMELINE_CHANGE_REASON_RESET: + return "RESET"; + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + return "DYNAMIC"; + default: + return "?"; + } + } + } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 9a8986409a..32e064e834 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -116,6 +116,7 @@ public final class CastPlayer implements Player { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; + private boolean waitingForInitialTimeline; /** * @param castContext The context from which the cast session is obtained. @@ -170,6 +171,7 @@ public final class CastPlayer implements Player { public PendingResult loadItems(MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { + waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); } @@ -556,8 +558,11 @@ public final class CastPlayer implements Player { private void maybeUpdateTimelineAndNotify() { if (updateTimeline()) { + @Player.TimelineChangeReason int reason = waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; for (EventListener listener : listeners) { - listener.onTimelineChanged(currentTimeline, null); + listener.onTimelineChanged(currentTimeline, null, reason); } } } 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 5b61db0264..fe6a6d6196 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 @@ -523,9 +523,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Player.EventListener implementation. @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - if (timeline.isEmpty()) { - // The player is being re-prepared and this source will be released. + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { + if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { + // The player is being reset and this source will be released. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 510ed9cf4f..c9ed54398e 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -258,7 +259,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @TimelineChangeReason int reason) { Callback callback = getCallback(); callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); 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 aa007ea1d6..d80487f2bd 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 @@ -628,7 +628,8 @@ public final class MediaSessionConnector { private int currentWindowCount; @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index f0f1c23c2b..59a58a4912 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; @@ -59,7 +61,7 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(); + testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); @@ -78,6 +80,7 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); testRunner.assertManifestsEqual(manifest); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -97,6 +100,7 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); @@ -210,6 +214,8 @@ public final class ExoPlayerTest extends TestCase { // info refresh from the second source was suppressed as we re-prepared with the third source. testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -243,6 +249,7 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertTrue(renderer.isEnded); } @@ -513,4 +520,25 @@ public final class ExoPlayerTest extends TestCase { assertEquals(3, numSelectionsEnabled); } + public void testDynamicTimelineChangeReason() throws Exception { + Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") + .waitForTimelineChanged(timeline1) + .executeRunnable(new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(timeline2, null); + } + }) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline1, timeline2); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_DYNAMIC); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ff00f9de91..77131f5ded 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -56,6 +56,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; + private boolean waitingForInitialTimeline; private boolean isLoading; private TrackGroupArray trackGroups; private TrackSelectionArray trackSelections; @@ -146,7 +147,8 @@ import java.util.concurrent.CopyOnWriteArraySet; if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest); + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, + Player.TIMELINE_CHANGE_REASON_RESET); } } if (tracksSelected) { @@ -159,6 +161,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } } } + waitingForInitialTimeline = true; pendingPrepareAcks++; internalPlayer.prepare(mediaSource, resetPosition); } @@ -532,9 +535,12 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } - if (timelineOrManifestChanged) { + if (timelineOrManifestChanged || waitingForInitialTimeline) { + @Player.TimelineChangeReason int reason = waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest); + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, reason); } } if (positionDiscontinuity) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index dc703f924a..77fced0832 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -59,8 +59,9 @@ public interface Player { * * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. */ - void onTimelineChanged(Timeline timeline, Object manifest); + void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); /** * Called when the available or selected tracks change. @@ -118,7 +119,8 @@ public interface Player { * when the source introduces a discontinuity internally). *

      * When a position discontinuity occurs as a result of a change to the timeline this method is - * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. + * not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this + * case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ @@ -149,8 +151,10 @@ public interface Player { abstract class DefaultEventListener implements EventListener { @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. + public void onTimelineChanged(Timeline timeline, Object manifest, + @TimelineChangeReason int reason) { + // Call deprecated version. Otherwise, do nothing. + onTimelineChanged(timeline, manifest); } @Override @@ -198,6 +202,15 @@ public interface Player { // Do nothing. } + /** + * @deprecated Use {@link DefaultEventListener#onTimelineChanged(Timeline, Object, int)} + * instead. + */ + @Deprecated + public void onTimelineChanged(Timeline timeline, Object manifest) { + // Do nothing. + } + } /** @@ -264,6 +277,26 @@ public interface Player { */ int DISCONTINUITY_REASON_INTERNAL = 3; + /** + * Reasons for timeline and/or manifest changes. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TIMELINE_CHANGE_REASON_PREPARED, TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC}) + public @interface TimelineChangeReason {} + /** + * Timeline and manifest changed as a result of a player initialization with new media. + */ + int TIMELINE_CHANGE_REASON_PREPARED = 0; + /** + * Timeline and manifest changed as a result of a player reset. + */ + int TIMELINE_CHANGE_REASON_RESET = 1; + /** + * Timeline or manifest changed as a result of an dynamic update introduced by the played media. + */ + int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index a96ed3a622..751a6c81a9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -699,6 +699,8 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); break; + default: + // Never happens. } repeatToggleButton.setVisibility(View.VISIBLE); } @@ -1098,7 +1100,8 @@ public class PlaybackControlView extends FrameLayout { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); 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 2abe521883..357d69df38 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 @@ -304,7 +304,7 @@ public abstract class Action { } /** - * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. + * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. */ public static final class WaitForTimelineChanged extends Action { @@ -327,7 +327,8 @@ public abstract class Action { } Player.EventListener listener = new Player.DefaultEventListener() { @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { if (timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 6730bf1c7f..638ad9e12d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -318,6 +318,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final CountDownLatch endedCountDownLatch; private final LinkedList timelines; private final LinkedList manifests; + private final ArrayList timelineChangeReasons; private final LinkedList periodIndices; private final ArrayList discontinuityReasons; @@ -338,6 +339,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.eventListener = eventListener; this.timelines = new LinkedList<>(); this.manifests = new LinkedList<>(); + this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new LinkedList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); @@ -430,6 +432,18 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the timeline change reasons reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * timeline change reasons. + */ + public void assertTimelineChangeReasonsEqual(@Player.TimelineChangeReason int... reasons) { + Assert.assertEquals(reasons.length, timelineChangeReasons.size()); + for (int i = 0; i < reasons.length; i++) { + Assert.assertEquals(reasons[i], (int) timelineChangeReasons.get(i)); + } + } + /** * Asserts that the last track group array reported by * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to @@ -507,9 +521,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Player.EventListener @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { timelines.add(timeline); manifests.add(manifest); + timelineChangeReasons.add(reason); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 4a5beb0501..6dc9cf7fd8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -340,7 +340,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; FakeExoPlayer.this.timeline = timeline; FakeExoPlayer.this.manifest = manifest; - eventListener.onTimelineChanged(timeline, manifest); + eventListener.onTimelineChanged(timeline, manifest, + Player.TIMELINE_CHANGE_REASON_PREPARED); waitForNotification.open(); } } From fdb53ac8d1c9a5f13ce36c0286ef69c070cf3560 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 01:44:18 -0800 Subject: [PATCH 078/417] Correct period index counting in ExoPlayerTestRunner. The initial period index was counted in onPlayerStateChanged. However, we actually want to save the period index in the newly introduced onPositionDiscontinuity after preparation. While being here, also updated deprecated LinkedList to ArrayList. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176479509 --- .../testutil/ExoPlayerTestRunner.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 638ad9e12d..cafc50f0b4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; -import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -316,10 +315,10 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final HandlerThread playerThread; private final Handler handler; private final CountDownLatch endedCountDownLatch; - private final LinkedList timelines; - private final LinkedList manifests; + private final ArrayList timelines; + private final ArrayList manifests; private final ArrayList timelineChangeReasons; - private final LinkedList periodIndices; + private final ArrayList periodIndices; private final ArrayList discontinuityReasons; private SimpleExoPlayer player; @@ -337,10 +336,10 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.loadControl = loadControl; this.actionSchedule = actionSchedule; this.eventListener = eventListener; - this.timelines = new LinkedList<>(); - this.manifests = new LinkedList<>(); + this.timelines = new ArrayList<>(); + this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); - this.periodIndices = new LinkedList<>(); + this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); this.playerThread = new HandlerThread("ExoPlayerTest thread"); @@ -413,8 +412,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public void assertTimelinesEqual(Timeline... timelines) { Assert.assertEquals(timelines.length, this.timelines.size()); - for (Timeline timeline : timelines) { - Assert.assertEquals(timeline, this.timelines.remove()); + for (int i = 0; i < timelines.length; i++) { + Assert.assertEquals(timelines[i], this.timelines.get(i)); } } @@ -427,8 +426,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public void assertManifestsEqual(Object... manifests) { Assert.assertEquals(manifests.length, this.manifests.size()); - for (Object manifest : manifests) { - Assert.assertEquals(manifest, this.manifests.remove()); + for (int i = 0; i < manifests.length; i++) { + Assert.assertEquals(manifests[i], this.manifests.get(i)); } } @@ -486,8 +485,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public void assertPlayedPeriodIndices(int... periodIndices) { Assert.assertEquals(periodIndices.length, this.periodIndices.size()); - for (int periodIndex : periodIndices) { - Assert.assertEquals(periodIndex, (int) this.periodIndices.remove()); + for (int i = 0; i < periodIndices.length; i++) { + Assert.assertEquals(periodIndices[i], (int) this.periodIndices.get(i)); } } @@ -526,6 +525,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { timelines.add(timeline); manifests.add(manifest); timelineChangeReasons.add(reason); + if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { + periodIndices.add(player.getCurrentPeriodIndex()); + } } @Override @@ -535,9 +537,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { - periodIndices.add(player.getCurrentPeriodIndex()); - } playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { @@ -553,7 +552,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { discontinuityReasons.add(reason); - periodIndices.add(player.getCurrentPeriodIndex()); + int currentIndex = player.getCurrentPeriodIndex(); + if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + || periodIndices.isEmpty() + || periodIndices.get(periodIndices.size() - 1) != currentIndex) { + // Ignore seek or internal discontinuities within a period. + periodIndices.add(currentIndex); + } } } From e1d960db68b59590190a0f20ca707cb9a0f745f8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 02:13:04 -0800 Subject: [PATCH 079/417] Unify internal reset method to support state and position resets. The ExoPlayerImplInternal.reset method now takes the same set of options as the ExoPlayer.prepare method. This also allows to - Remove some code duplication within ExoPlayerImplInternal - Fix calls to prepare(sameSource, resetPosition=true, resetState=false) with enabled shuffle mode where the position was not correctly reset to the first period index. - Keep the current timeline when calling stop (in line with ExoPlayerImpl). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176481878 --- .../android/exoplayer2/ExoPlayerTest.java | 27 ++++++++- .../exoplayer2/ExoPlayerImplInternal.java | 59 +++++++++---------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 59a58a4912..2392c32e0a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; @@ -541,4 +540,30 @@ public final class ExoPlayerTest extends TestCase { Player.TIMELINE_CHANGE_REASON_DYNAMIC); } + public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(/* isSeekable= */ true, + /* isDynamic= */ false, /* durationUs= */ 100000)); + ConcatenatingMediaSource firstMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, + new FakeShuffleOrder(/* length= */ 2), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + ); + ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, + new FakeShuffleOrder(/* length= */ 2), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + ); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationWithShuffle") + // Wait for first preparation and enable shuffling. Plays period 0. + .waitForPlaybackState(Player.STATE_READY).setShuffleModeEnabled(true) + // Reprepare with second media source (keeping state, but with position reset). + // Plays period 1 and 0 because of the reversed fake shuffle order. + .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(firstMediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 0); + } + } 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 1732026540..668d52425e 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 @@ -393,17 +393,10 @@ import java.io.IOException; private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { pendingPrepareCount++; - resetInternal(true); + resetInternal(/* releaseMediaSource= */ true, resetPosition); loadControl.onPrepared(); - if (resetPosition) { - playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); - } else { - // The new start position is the current playback position. - playbackInfo = new PlaybackInfo(null, null, playbackInfo.periodId, playbackInfo.positionUs, - playbackInfo.contentPositionUs); - } this.mediaSource = mediaSource; - mediaSource.prepareSource(player, true, this); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); setState(Player.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -638,18 +631,16 @@ import java.io.IOException; Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { - int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( - timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); setState(Player.STATE_ENDED); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, - playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)).sendToTarget(); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); + resetInternal(false, true); + // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 1, 0, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs = */ 0, + /* contentPositionUs= */ C.TIME_UNSET)) + .sendToTarget(); return; } @@ -768,13 +759,13 @@ import java.io.IOException; } private void stopInternal() { - resetInternal(true); + resetInternal(/* releaseMediaSource= */ false, /* resetPosition= */ false); loadControl.onStopped(); setState(Player.STATE_IDLE); } private void releaseInternal() { - resetInternal(true); + resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ true); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -784,7 +775,7 @@ import java.io.IOException; } } - private void resetInternal(boolean releaseMediaSource) { + private void resetInternal(boolean releaseMediaSource, boolean resetPosition) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -804,6 +795,20 @@ import java.io.IOException; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); + if (resetPosition) { + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + Timeline timeline = playbackInfo.timeline; + int firstPeriodIndex = timeline == null || timeline.isEmpty() + ? 0 + : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + .firstPeriodIndex; + playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); + } else { + // The new start position is the current playback position. + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, + playbackInfo.contentPositionUs); + } if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); @@ -1129,18 +1134,12 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback(int prepareAcks, int seekAcks) { - Timeline timeline = playbackInfo.timeline; - int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( - timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); setState(Player.STATE_ENDED); - // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. - notifySourceInfoRefresh(prepareAcks, seekAcks, - playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); + resetInternal(false, true); + // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). + notifySourceInfoRefresh(prepareAcks, seekAcks, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); } private void notifySourceInfoRefresh() { From a8d867be37ef813a60d64779e2cc9693d3364238 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 02:44:09 -0800 Subject: [PATCH 080/417] Support multiple transitions to STATE_ENDED in ExoPlayerTestRunner. Currently testRunner.blockUntilEnded waits for the first transition to STATE_ENDED or _IDLE before returning. In order to support tests with player repreparations after one playback finished, this change adds an option to specifiy the number of expected transitions to ended. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176484047 --- .../testutil/ExoPlayerTestRunner.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index cafc50f0b4..5ada65ef1e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -100,6 +100,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private RenderersFactory renderersFactory; private ActionSchedule actionSchedule; private Player.EventListener eventListener; + private Integer expectedPlayerEndedCount; /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The @@ -255,6 +256,20 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED} + * or {@link Player#STATE_IDLE}. The default is 1. This affects how long + * {@link ExoPlayerTestRunner#blockUntilEnded(long)} waits. + * + * @param expectedPlayerEndedCount The number of times the player is expected to reach the ended + * or idle state. + * @return This builder. + */ + public Builder setExpectedPlayerEndedCount(int expectedPlayerEndedCount) { + this.expectedPlayerEndedCount = expectedPlayerEndedCount; + return this; + } + /** * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults. * @@ -299,8 +314,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); } + if (expectedPlayerEndedCount == null) { + expectedPlayerEndedCount = 1; + } return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector, - loadControl, actionSchedule, eventListener); + loadControl, actionSchedule, eventListener, expectedPlayerEndedCount); } } @@ -328,7 +346,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, - LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener) { + LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener, + int expectedPlayerEndedCount) { this.playerFactory = playerFactory; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -341,7 +360,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); - this.endedCountDownLatch = new CountDownLatch(1); + this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); this.handler = new Handler(playerThread.getLooper()); @@ -514,7 +533,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { if (this.exception == null) { this.exception = exception; } - endedCountDownLatch.countDown(); + while (endedCountDownLatch.getCount() > 0) { + endedCountDownLatch.countDown(); + } } // Player.EventListener From 31e2cfce9e21c0d594d6b10cf726d498b07a7492 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 02:48:44 -0800 Subject: [PATCH 081/417] Pass playback speed to LoadControl and TrackSelection This allows implementations of those classes to take into account the playback speed for adaptive track selection and controlling when to resume the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176484361 --- .../exoplayer2/DefaultLoadControl.java | 5 +- .../exoplayer2/ExoPlayerImplInternal.java | 49 +++++++++++++------ .../android/exoplayer2/LoadControl.java | 6 ++- .../exoplayer2/PlaybackParameters.java | 14 ++++-- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- .../trackselection/BaseTrackSelection.java | 5 ++ .../trackselection/TrackSelection.java | 8 +++ .../exoplayer2/util/StandaloneMediaClock.java | 2 +- .../google/android/exoplayer2/util/Util.java | 14 ++++++ .../exoplayer2/DefaultMediaClockTest.java | 2 +- .../testutil/FakeSimpleExoPlayer.java | 5 +- .../testutil/FakeTrackSelection.java | 5 ++ 12 files changed, 89 insertions(+), 29 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 4cbcc00886..56bc633c9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -174,13 +174,14 @@ public class DefaultLoadControl implements LoadControl { } @Override - public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { + public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, + boolean rebuffering) { long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; } @Override - public boolean shouldContinueLoading(long bufferedDurationUs) { + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; 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 668d52425e..71da7043be 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 @@ -284,10 +284,8 @@ import java.io.IOException; @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // TODO(b/37237846): Make LoadControl, period transition position projection, adaptive track - // selection and potentially any time-related code in renderers take into account the playback - // speed. eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + updateTrackSelectionPlaybackSpeed(playbackParameters.speed); } // Handler.Callback implementation. @@ -573,9 +571,10 @@ import java.io.IOException; setState(Player.STATE_ENDED); stopRenderers(); } else if (state == Player.STATE_BUFFERING) { + float playbackSpeed = mediaClock.getPlaybackParameters().speed; boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded - && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs)) + ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( + rendererPositionUs, playbackSpeed, rebuffering)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(Player.STATE_READY); @@ -853,6 +852,7 @@ import java.io.IOException; // We don't have tracks yet, so we don't care. return; } + float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = playingPeriodHolder; boolean selectionsChangedForReadPeriod = true; @@ -861,7 +861,7 @@ import java.io.IOException; // The reselection did not change any prepared periods. return; } - if (periodHolder.selectTracks()) { + if (periodHolder.selectTracks(playbackSpeed)) { // Selected tracks have changed for this period. break; } @@ -935,6 +935,18 @@ import java.io.IOException; } } + private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { + MediaPeriodHolder periodHolder = + playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; + while (periodHolder != null) { + TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + periodHolder = periodHolder.next; + } + } + private boolean isTimelineReady(long playingPeriodDurationUs) { return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs @@ -1391,7 +1403,7 @@ import java.io.IOException; // Stale event. return; } - loadingPeriodHolder.handlePrepared(); + loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; @@ -1410,7 +1422,8 @@ import java.io.IOException; } private void maybeContinueLoading() { - boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs); + boolean continueLoading = loadingPeriodHolder.shouldContinueLoading( + rendererPositionUs, mediaClock.getPlaybackParameters().speed); setIsLoading(continueLoading); if (continueLoading) { loadingPeriodHolder.continueLoading(rendererPositionUs); @@ -1572,7 +1585,8 @@ import java.io.IOException; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } - public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) { + public boolean haveSufficientBuffer(long rendererPositionUs, float playbackSpeed, + boolean rebuffering) { long bufferedPositionUs = !prepared ? info.startPositionUs : mediaPeriod.getBufferedPositionUs(); if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { @@ -1582,24 +1596,24 @@ import java.io.IOException; bufferedPositionUs = info.durationUs; } return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs), - rebuffering); + playbackSpeed, rebuffering); } - public void handlePrepared() throws ExoPlaybackException { + public void handlePrepared(float playbackSpeed) throws ExoPlaybackException { prepared = true; - selectTracks(); + selectTracks(playbackSpeed); long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); info = info.copyWithStartPositionUs(newStartPositionUs); } - public boolean shouldContinueLoading(long rendererPositionUs) { + public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) { long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { return false; } else { long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; - return loadControl.shouldContinueLoading(bufferedDurationUs); + return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } } @@ -1608,13 +1622,18 @@ import java.io.IOException; mediaPeriod.continueLoading(loadingPeriodPositionUs); } - public boolean selectTracks() throws ExoPlaybackException { + public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups()); if (selectorResult.isEquivalent(periodTrackSelectorResult)) { return false; } trackSelectorResult = selectorResult; + for (TrackSelection trackSelection : trackSelectorResult.selections.getAll()) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + } return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java index 44b16b0cf6..ee4775d048 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -92,19 +92,21 @@ public interface LoadControl { * started or resumed. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by * buffer depletion rather than a user action. Hence this parameter is false during initial * buffering and when buffering as a result of a seek operation. * @return Whether playback should be allowed to start or resume. */ - boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering); + boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); /** * Called by the player to determine whether it should continue to load the source. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @return Whether the loading should continue. */ - boolean shouldContinueLoading(long bufferedDurationUs); + boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index 90aded7660..47d5bc88b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.util.Assertions; + /** * The parameters that apply to playback. */ @@ -40,23 +42,25 @@ public final class PlaybackParameters { /** * Creates new playback parameters. * - * @param speed The factor by which playback will be sped up. - * @param pitch The factor by which the audio pitch will be scaled. + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. */ public PlaybackParameters(float speed, float pitch) { + Assertions.checkArgument(speed > 0); + Assertions.checkArgument(pitch > 0); this.speed = speed; this.pitch = pitch; scaledUsPerMs = Math.round(speed * 1000f); } /** - * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in - * microseconds. + * Returns the media time in microseconds that will elapse in {@code timeMs} milliseconds of + * wallclock time. * * @param timeMs The time to scale, in milliseconds. * @return The scaled time, in microseconds. */ - public long getSpeedAdjustedDurationUs(long timeMs) { + public long getMediaTimeUsForPlayoutTimeMs(long timeMs) { return timeMs * scaledUsPerMs; } 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 ba62ac126e..3b14b69916 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 @@ -1012,7 +1012,8 @@ public final class DefaultAudioSink implements AudioSink { } // We are playing data at a previous playback speed, so fall back to multiplying by the speed. return playbackParametersOffsetUs - + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); + + Util.getMediaDurationForPlayoutDuration( + positionUs - playbackParametersPositionUs, playbackParameters.speed); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 6bc6afb88b..9a58ac07aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -138,6 +138,11 @@ public abstract class BaseTrackSelection implements TrackSelection { return tracks[getSelectedIndex()]; } + @Override + public void onPlaybackSpeed(float playbackSpeed) { + // Do nothing. + } + @Override public int evaluateQueueSize(long playbackPositionUs, List queue) { return queue.size(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 027b2abde9..55e6050622 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -136,6 +136,14 @@ public interface TrackSelection { // Adaptation. + /** + * Called to notify the selection of the current playback speed. The playback speed may affect + * adaptive track selection. + * + * @param speed The playback speed. + */ + void onPlaybackSpeed(float speed); + /** * Updates the selected track. *

      diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index fad3a00f10..3c0ec2a854 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -88,7 +88,7 @@ public final class StandaloneMediaClock implements MediaClock { if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { - positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs); + positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); } } return positionUs; 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 3b402ec59d..4582ab7c86 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 @@ -672,6 +672,20 @@ public final class Util { } } + /** + * Returns the duration of media that will elapse in {@code playoutDuration}. + * + * @param playoutDuration The duration to scale. + * @param speed The playback speed. + * @return The scaled duration, in the same units as {@code playoutDuration}. + */ + public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) { + if (speed == 1f) { + return playoutDuration; + } + return Math.round((double) playoutDuration * speed); + } + /** * Converts a list of integers to a primitive array. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index 9db4d57a65..9edb84eaa9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -368,7 +368,7 @@ public class DefaultMediaClockTest { long clockStartUs = mediaClock.syncAndGetPositionUs(); fakeClock.advanceTime(SLEEP_TIME_MS); assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs - + mediaClock.getPlaybackParameters().getSpeedAdjustedDurationUs(SLEEP_TIME_MS)); + + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); } private void assertClockIsStopped() { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 6dc9cf7fd8..094aaa5273 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -465,7 +465,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; - if (loadControl.shouldContinueLoading(bufferedDurationUs)) { + if (loadControl.shouldContinueLoading(bufferedDurationUs, 1f)) { newIsLoading = true; mediaPeriod.continueLoading(rendererPositionUs); } @@ -488,7 +488,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { return true; } - return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, rebuffering); + return + loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, 1f, rebuffering); } private void handlePlayerError(final ExoPlaybackException e) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java index 20346a0355..717dcda7b1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -111,6 +111,11 @@ public final class FakeTrackSelection implements TrackSelection { return null; } + @Override + public void onPlaybackSpeed(float speed) { + // Do nothing. + } + @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { From c49ae5369993059151813ba58ddfc7009928917d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 03:44:06 -0800 Subject: [PATCH 082/417] Remove unnecessary dependency ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176487991 --- extensions/mediasession/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 85a8ac46e2..651bd952f8 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -27,7 +27,6 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-media-compat:' + supportLibraryVersion - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion } ext { From 856c2f8d3ee59e6b8b0fba23532d8cd98d2616b3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 04:46:52 -0800 Subject: [PATCH 083/417] Make ExtractorMediaSource timeline dynamic until duration is set We (eventually - albeit possibly infinitely far in the future) expect a timeline update with a window of known duration. This also stops live radio stream playbacks transitioning to ended state when their tracks are disabled. As part of this fix, I found an issue where getPeriodPosition could return null even when defaultPositionProjectionUs is 0, which is not as documented. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176492024 --- .../source/ClippingMediaSourceTest.java | 10 +-- .../source/ExtractorMediaSource.java | 7 +- .../source/SinglePeriodTimeline.java | 23 +++--- .../source/SingleSampleMediaSource.java | 2 +- .../source/SinglePeriodTimelineTest.java | 77 +++++++++++++++++++ 5 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 3c870f06f4..c72188ad2c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 066953b998..0839d06fdd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -327,8 +327,11 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - sourceListener.onSourceInfoRefreshed( - this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); + // If the duration is currently unset, we expect to be able to update the window when its + // duration eventually becomes known. + boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + sourceListener.onSourceInfoRefreshed(this, + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 6f35438444..9cce67f68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline of one period of known duration, and a static window starting at zero and - * extending to that duration. + * Creates a timeline containing a single period and a window that spans it. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable) { - this(durationUs, durationUs, 0, 0, isSeekable, false); + public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { + this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); } /** @@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,11 +106,16 @@ public final class SinglePeriodTimeline extends Timeline { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic) { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the live window. + if (isDynamic && defaultPositionProjectionUs != 0) { + if (windowDurationUs == C.TIME_UNSET) { + // Don't allow projection into a window that has an unknown duration. windowDefaultStartPositionUs = C.TIME_UNSET; + } else { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 2aa8ccc712..51afb8eee9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -217,7 +217,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true); + timeline = new SinglePeriodTimeline(durationUs, true, false); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java new file mode 100644 index 0000000000..94ca8b03f0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link SinglePeriodTimeline}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SinglePeriodTimelineTest { + + private Window window; + private Period period; + + @Before + public void setUp() throws Exception { + window = new Window(); + period = new Period(); + } + + @Test + public void testGetPeriodPositionDynamicWindowUnknownDuration() { + SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + // Should return null with any positive position projection. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); + assertThat(position).isNull(); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + + @Test + public void testGetPeriodPositionDynamicWindowKnownDuration() { + long windowDurationUs = 1000; + SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, + 0, false, true); + // Should return null with a positive position projection beyond window duration. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, + windowDurationUs + 1); + assertThat(position).isNull(); + // Should return (0, duration) with a projection equal to window duration. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(windowDurationUs); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + +} From 3998ed49ae0d7dd42fab9e1c1e72e17d27c5b988 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 21 Nov 2017 13:42:12 +0000 Subject: [PATCH 084/417] Mini cleanup --- .../google/android/exoplayer2/text/ssa/SsaDecoder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 12aa1e97d5..eec4a1269c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -53,13 +53,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } /** - * @param initializationData Optional initialization data for the decoder. If not null, the - * initialization data must consist of two byte arrays. The first must contain an SSA format - * line. The second must contain an SSA header that will be assumed common to all samples. + * @param initializationData Optional initialization data for the decoder. If not null or empty, + * the initialization data must consist of two byte arrays. The first must contain an SSA + * format line. The second must contain an SSA header that will be assumed common to all + * samples. */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null && initializationData.size() > 0) { + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From e619079a0d7ef598d3b30c2a34b3dd86f1eac844 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 21 Nov 2017 07:03:55 -0800 Subject: [PATCH 085/417] Let EventMessage encloses its presentationTimeMs. Currently EventMessage's presentationTimeMs is kept separately in EventSampleStream. However, EventMessage's presentationTimeMs maybe used in other places besides EventSampleStream, such as when handling `emsg' messages targeting the player. This CL let EventMessage object to holds its presentationTimeMs for such use cases. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176502938 --- .../metadata/emsg/EventMessage.java | 21 ++++++++++---- .../metadata/emsg/EventMessageDecoder.java | 9 ++++-- .../metadata/emsg/EventMessageEncoder.java | 7 ++--- .../emsg/EventMessageDecoderTest.java | 3 +- .../emsg/EventMessageEncoderTest.java | 22 +++++++------- .../metadata/emsg/EventMessageTest.java | 2 +- .../dash/manifest/DashManifestParserTest.java | 6 ++-- .../source/dash/EventSampleStream.java | 2 +- .../dash/manifest/DashManifestParser.java | 29 +++++++++---------- 9 files changed, 57 insertions(+), 44 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java index fbe3184c0d..57e7f0bfd6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java @@ -41,6 +41,13 @@ public final class EventMessage implements Metadata.Entry { */ public final long durationMs; + /** + * The presentation time value of this event message in microseconds. + *

      + * Except in special cases, application code should not use this field. + */ + public final long presentationTimeUs; + /** * The instance identifier. */ @@ -55,25 +62,27 @@ public final class EventMessage implements Metadata.Entry { private int hashCode; /** - * * @param schemeIdUri The message scheme. * @param value The value for the event. * @param durationMs The duration of the event in milliseconds. * @param id The instance identifier. * @param messageData The body of the message. + * @param presentationTimeUs The presentation time value of this event message in microseconds. */ public EventMessage(String schemeIdUri, String value, long durationMs, long id, - byte[] messageData) { + byte[] messageData, long presentationTimeUs) { this.schemeIdUri = schemeIdUri; this.value = value; this.durationMs = durationMs; this.id = id; this.messageData = messageData; + this.presentationTimeUs = presentationTimeUs; } /* package */ EventMessage(Parcel in) { schemeIdUri = in.readString(); value = in.readString(); + presentationTimeUs = in.readLong(); durationMs = in.readLong(); id = in.readLong(); messageData = in.createByteArray(); @@ -85,6 +94,7 @@ public final class EventMessage implements Metadata.Entry { int result = 17; result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0); result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (int) (presentationTimeUs ^ (presentationTimeUs >>> 32)); result = 31 * result + (int) (durationMs ^ (durationMs >>> 32)); result = 31 * result + (int) (id ^ (id >>> 32)); result = 31 * result + Arrays.hashCode(messageData); @@ -102,9 +112,9 @@ public final class EventMessage implements Metadata.Entry { return false; } EventMessage other = (EventMessage) obj; - return durationMs == other.durationMs && id == other.id - && Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value) - && Arrays.equals(messageData, other.messageData); + return presentationTimeUs == other.presentationTimeUs && durationMs == other.durationMs + && id == other.id && Util.areEqual(schemeIdUri, other.schemeIdUri) + && Util.areEqual(value, other.value) && Arrays.equals(messageData, other.messageData); } // Parcelable implementation. @@ -118,6 +128,7 @@ public final class EventMessage implements Metadata.Entry { public void writeToParcel(Parcel dest, int flags) { dest.writeString(schemeIdUri); dest.writeString(value); + dest.writeLong(presentationTimeUs); dest.writeLong(durationMs); dest.writeLong(id); dest.writeByteArray(messageData); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index 266988246d..7e5125e71c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -24,7 +25,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; /** - * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1. + * Decodes Event Message (emsg) atoms, as defined in ISO/IEC 23009-1:2014, Section 5.10.3.3. *

      * Atom data should be provided to the decoder without the full atom header (i.e. starting from the * first byte of the scheme_id_uri field). @@ -40,11 +41,13 @@ public final class EventMessageDecoder implements MetadataDecoder { String schemeIdUri = emsgData.readNullTerminatedString(); String value = emsgData.readNullTerminatedString(); long timescale = emsgData.readUnsignedInt(); - emsgData.skipBytes(4); // presentation_time_delta + long presentationTimeUs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), + C.MICROS_PER_SECOND, timescale); long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); - return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData, + presentationTimeUs)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java index 2bd54367e1..eca498a6df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -42,11 +42,10 @@ public final class EventMessageEncoder { * * @param eventMessage The event message to be encoded. * @param timescale Timescale of the event message, in units per second. - * @param presentationTimeUs The presentation time of the event message in microseconds. * @return The serialized byte array. */ @Nullable - public byte[] encode(EventMessage eventMessage, long timescale, long presentationTimeUs) { + public byte[] encode(EventMessage eventMessage, long timescale) { Assertions.checkArgument(timescale >= 0); byteArrayOutputStream.reset(); try { @@ -54,8 +53,8 @@ public final class EventMessageEncoder { String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; writeNullTerminatedString(dataOutputStream, nonNullValue); writeUnsignedInt(dataOutputStream, timescale); - long presentationTime = Util.scaleLargeTimestamp(presentationTimeUs, timescale, - C.MICROS_PER_SECOND); + long presentationTime = Util.scaleLargeTimestamp(eventMessage.presentationTimeUs, + timescale, C.MICROS_PER_SECOND); writeUnsignedInt(dataOutputStream, presentationTime); long duration = Util.scaleLargeTimestamp(eventMessage.durationMs, timescale, 1000); writeUnsignedInt(dataOutputStream, duration); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index 1ce0ccb93d..3a6e96b3e8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -38,7 +38,7 @@ public final class EventMessageDecoderTest { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, 0, 0, // presentation_time_delta (ignored) = 0 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -45, // id = 1000403 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} @@ -53,6 +53,7 @@ public final class EventMessageDecoderTest { assertThat(eventMessage.durationMs).isEqualTo(3000); assertThat(eventMessage.id).isEqualTo(1000403); assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); + assertThat(eventMessage.presentationTimeUs).isEqualTo(1000000); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java index f526fc3451..f0a6d3e19b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -36,24 +36,24 @@ public final class EventMessageEncoderTest { @Test public void testEncodeEventStream() throws IOException { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); + new byte[] {0, 1, 2, 3, 4}, 1000000); byte[] expectedEmsgBody = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -45, // id = 1000403 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} - byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000, 1000000); + byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000); assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); } @Test public void testEncodeDecodeEventStream() throws IOException { EventMessage expectedEmsg = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); - byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000, 1); + new byte[] {0, 1, 2, 3, 4}, 1000000); + byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); @@ -66,29 +66,29 @@ public final class EventMessageEncoderTest { @Test public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); + new byte[] {0, 1, 2, 3, 4}, 1000000); byte[] expectedEmsgBody = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -45, // id = 1000403 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, - new byte[] {4, 3, 2, 1, 0}); + new byte[] {4, 3, 2, 1, 0}, 1000000); byte[] expectedEmsgBody1 = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -46, // id = 1000402 4, 3, 2, 1, 0}; // message_data = {4, 3, 2, 1, 0} EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); - byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000, 1000000); + byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000); assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); - byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000, 1000000); + byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000); assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java index b48a071d0d..58f2b9f55d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java @@ -33,7 +33,7 @@ public final class EventMessageTest { @Test public void testEventMessageParcelable() { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); + new byte[] {0, 1, 2, 3, 4}, 1000); // Write to parcel. Parcel parcel = Parcel.obtain(); eventMessage.writeToParcel(parcel, 0); diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 37dc6a748e..5c54a7884b 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -84,7 +84,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { EventStream eventStream1 = period.eventStreams.get(0); assertEquals(1, eventStream1.events.length); EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0, - "+ 1 800 10101010".getBytes()); + "+ 1 800 10101010".getBytes(), 0); assertEquals(expectedEvent1, eventStream1.events[0]); // assert CData-structured event stream @@ -102,7 +102,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { + " GB\n" + " \n" + " \n" - + " ]]>").getBytes()), + + " ]]>").getBytes(), 300000000), eventStream2.events[0]); // assert xml-structured event stream @@ -114,7 +114,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { + " \n" + " /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n" + " \n" - + " ").getBytes()), + + " ").getBytes(), 1000000000), eventStream3.events[0]); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java index 549bfdef7b..694f9f843e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -95,7 +95,7 @@ import java.io.IOException; } int sampleIndex = currentIndex++; byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex], - eventStream.timescale, eventTimesUs[sampleIndex]); + eventStream.timescale); if (serializedEvent != null) { buffer.ensureSpaceForWrite(serializedEvent.length); buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); 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 d3906acdf6..07f9660755 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 @@ -688,23 +688,23 @@ public class DashManifestParser extends DefaultHandler String schemeIdUri = parseString(xpp, "schemeIdUri", ""); String value = parseString(xpp, "value", ""); long timescale = parseLong(xpp, "timescale", 1); - List> timedEvents = new ArrayList<>(); + List eventMessages = new ArrayList<>(); ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512); do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Event")) { - Pair timedEvent = parseEvent(xpp, schemeIdUri, value, timescale, + EventMessage event = parseEvent(xpp, schemeIdUri, value, timescale, scratchOutputStream); - timedEvents.add(timedEvent); + eventMessages.add(event); } } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream")); - long[] presentationTimesUs = new long[timedEvents.size()]; - EventMessage[] events = new EventMessage[timedEvents.size()]; - for (int i = 0; i < timedEvents.size(); i++) { - Pair timedEvent = timedEvents.get(i); - presentationTimesUs[i] = timedEvent.first; - events[i] = timedEvent.second; + long[] presentationTimesUs = new long[eventMessages.size()]; + EventMessage[] events = new EventMessage[eventMessages.size()]; + for (int i = 0; i < eventMessages.size(); i++) { + EventMessage event = eventMessages.get(i); + presentationTimesUs[i] = event.presentationTimeUs; + events[i] = event; } return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events); } @@ -723,11 +723,11 @@ public class DashManifestParser extends DefaultHandler * @param timescale The timescale of the parent EventStream. * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize data * in between and tags into. - * @return The {@link EventStream} parsed from this EventStream node. + * @return The {@link EventMessage} parsed from this EventStream node. * @throws XmlPullParserException If there is any error parsing this node. * @throws IOException If there is any error reading from the underlying input stream. */ - protected Pair parseEvent(XmlPullParser xpp, String schemeIdUri, String value, + protected EventMessage parseEvent(XmlPullParser xpp, String schemeIdUri, String value, long timescale, ByteArrayOutputStream scratchOutputStream) throws IOException, XmlPullParserException { long id = parseLong(xpp, "id", 0); @@ -737,8 +737,7 @@ public class DashManifestParser extends DefaultHandler long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, timescale); byte[] eventObject = parseEventObject(xpp, scratchOutputStream); - return new Pair<>(presentationTimesUs, buildEvent(schemeIdUri, value, id, durationMs, - eventObject)); + return buildEvent(schemeIdUri, value, id, durationMs, eventObject, presentationTimesUs); } /** @@ -807,8 +806,8 @@ public class DashManifestParser extends DefaultHandler } protected EventMessage buildEvent(String schemeIdUri, String value, long id, - long durationMs, byte[] messageData) { - return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + long durationMs, byte[] messageData, long presentationTimeUs) { + return new EventMessage(schemeIdUri, value, durationMs, id, messageData, presentationTimeUs); } protected List parseSegmentTimeline(XmlPullParser xpp) From 15a1f9a55263ac5a571ea89b409125d91676584f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 08:58:02 -0800 Subject: [PATCH 086/417] Remove DefaultLoadControl buffer time state ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176515168 --- .../android/exoplayer2/DefaultLoadControl.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 56bc633c9b..bfafd409f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,10 +51,6 @@ public class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - private static final int ABOVE_HIGH_WATERMARK = 0; - private static final int BETWEEN_WATERMARKS = 1; - private static final int BELOW_LOW_WATERMARK = 2; - private final DefaultAllocator allocator; private final long minBufferUs; @@ -182,11 +178,11 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferTimeState == BELOW_LOW_WATERMARK - || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); + isBuffering = bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering && !targetBufferSizeReached); if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -197,11 +193,6 @@ public class DefaultLoadControl implements LoadControl { return isBuffering; } - private int getBufferTimeState(long bufferedDurationUs) { - return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK - : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); - } - private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 6607f49be609b692efbe210832544d375afc494c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 09:28:10 -0800 Subject: [PATCH 087/417] Fix reporting of format changes in ChunkSampleStream. Until recently, changing primary track formats were reported when the corresponding media chunk was discarded which always happened immediately after the sample has been read. Now, media chunks may be discarded later on or in batches, leaving the current reporting mechanism broken because changes may never be reported. This fix separates the discarding from the reporting such that format changes can be reported when the media chunk is first read from, while the discarding operation only discards without reporting format changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176519071 --- .../source/chunk/ChunkSampleStream.java | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) 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 53742238ef..cfed38dd4a 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 @@ -258,8 +258,12 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, + int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), 1); + } + return result; } @Override @@ -274,6 +278,9 @@ public class ChunkSampleStream implements SampleStream, S skipCount = 0; } } + if (skipCount > 0) { + maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), skipCount); + } return skipCount; } @@ -434,23 +441,48 @@ public class ChunkSampleStream implements SampleStream, S return pendingResetPositionUs != C.TIME_UNSET; } - private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { + private void discardDownstreamMediaChunks(int discardToPrimaryStreamIndex) { if (!mediaChunks.isEmpty()) { while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= discardToPrimaryStreamIndex) { mediaChunks.removeFirst(); } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(primaryDownstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - primaryDownstreamTrackFormat = trackFormat; } } + private void maybeNotifyPrimaryTrackFormatChanged(int toPrimaryStreamReadIndex, int readCount) { + if (!mediaChunks.isEmpty()) { + int fromPrimaryStreamReadIndex = toPrimaryStreamReadIndex - readCount; + int fromChunkIndex = 0; + while (fromChunkIndex < mediaChunks.size() - 1 + && mediaChunks.get(fromChunkIndex + 1).getFirstSampleIndex(0) + <= fromPrimaryStreamReadIndex) { + fromChunkIndex++; + } + int toChunkIndex = fromChunkIndex + 1; + if (readCount > 1) { + while (toChunkIndex < mediaChunks.size() + && mediaChunks.get(toChunkIndex).getFirstSampleIndex(0) < toPrimaryStreamReadIndex) { + toChunkIndex++; + } + } + for (int i = fromChunkIndex; i < toChunkIndex; i++) { + maybeNotifyPrimaryTrackFormatChanged(i); + } + } + } + + private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) { + BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * From e575af3ac38e06e78d92d3b97ddc77d2b5bb9b8f Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 21 Nov 2017 10:16:50 -0800 Subject: [PATCH 088/417] Parse DASH manifest's publish time. Parse DASH manifest's publishTime node as defined by ISO/IEC 23009-1:2014, section 5.3.1.2. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176525922 --- .../source/dash/manifest/DashManifestTest.java | 3 ++- .../source/dash/manifest/DashManifest.java | 15 +++++++++++---- .../source/dash/manifest/DashManifestParser.java | 13 +++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index dfcb9e72a5..882b0eb374 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -136,6 +136,7 @@ public class DashManifestTest extends TestCase { assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); + assertEquals(expected.publishTimeMs, actual.publishTimeMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); @@ -179,7 +180,7 @@ public class DashManifestTest extends TestCase { } private static DashManifest newDashManifest(int duration, Period... periods) { - return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY, + return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, Arrays.asList(periods)); } 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 cd24526d7c..95fe938fa4 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 @@ -67,6 +67,12 @@ public class DashManifest { */ public final long suggestedPresentationDelayMs; + /** + * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long publishTimeMs; + /** * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. @@ -82,8 +88,8 @@ public class DashManifest { public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, - long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, - List periods) { + long suggestedPresentationDelayMs, long publishTimeMs, UtcTimingElement utcTiming, + Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; this.minBufferTimeMs = minBufferTimeMs; @@ -91,6 +97,7 @@ public class DashManifest { this.minUpdatePeriodMs = minUpdatePeriodMs; this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; + this.publishTimeMs = publishTimeMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -147,8 +154,8 @@ public class DashManifest { } long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, - minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, copyPeriods); + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, publishTimeMs, + utcTiming, location, copyPeriods); } private static ArrayList copyAdaptationSets( 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 07f9660755..73d234fa72 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 @@ -119,6 +119,7 @@ public class DashManifestParser extends DefaultHandler ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET; long suggestedPresentationDelayMs = dynamic ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET; + long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET); UtcTimingElement utcTiming = null; Uri location = null; @@ -171,17 +172,17 @@ public class DashManifestParser extends DefaultHandler } return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected DashManifest buildMediaPresentationDescription(long availabilityStartTime, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdateTimeMs, - long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, UtcTimingElement utcTiming, - Uri location, List periods) { + long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, + UtcTimingElement utcTiming, Uri location, List periods) { return new DashManifest(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { From 4193a1f705b68627e0090d4bb66f44bf6302dd5a Mon Sep 17 00:00:00 2001 From: jrochest Date: Wed, 22 Nov 2017 01:49:47 -0800 Subject: [PATCH 089/417] Guard against null TrackSelections in updateTrackSelectionPlaybackSpeed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176629070 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 +++- 1 file changed, 3 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 71da7043be..63ae3c630e 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 @@ -941,7 +941,9 @@ import java.io.IOException; while (periodHolder != null) { TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); for (TrackSelection trackSelection : trackSelections) { - trackSelection.onPlaybackSpeed(playbackSpeed); + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } } periodHolder = periodHolder.next; } From b5480e0e97cce89ce6867a2b2b61620e48f41881 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 05:10:43 -0800 Subject: [PATCH 090/417] Relax requirement that ClippingMediaSource children are not dynamic Tests to follow (want to fix breakages first). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176642610 --- .../android/exoplayer2/source/ClippingMediaSource.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index c6924e844a..0b2ff33f30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -25,8 +25,7 @@ import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source may only have a single period/window and it must not be dynamic - * (live). + * positions. The wrapped source may only have a single period/window. */ public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { @@ -41,7 +40,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste /** * Creates a new clipping source that wraps the specified source. * - * @param mediaSource The single-period, non-dynamic source to wrap. + * @param mediaSource The single-period source to wrap. * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop @@ -61,7 +60,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period * is first read from. * - * @param mediaSource The single-period, non-dynamic source to wrap. + * @param mediaSource The single-period source to wrap. * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop @@ -145,7 +144,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste Assertions.checkArgument(timeline.getWindowCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1); Window window = timeline.getWindow(0, new Window(), false); - Assertions.checkArgument(!window.isDynamic); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { From d909dc1863b5bda2fcfc1edd1e2e00a3a199e3a0 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 05:36:20 -0800 Subject: [PATCH 091/417] Report correct discontinuity from ClippingMediaPeriod It currently always reports 0, but it should report the position passed through selectTracks. Reporting should also be disabled if there's a seekToUs call. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176644228 --- .../source/ClippingMediaPeriod.java | 105 ++++++++---------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 7742444323..36e8e51ffb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -36,10 +36,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb public final MediaPeriod mediaPeriod; private MediaPeriod.Callback callback; - private long startUs; - private long endUs; private ClippingSampleStream[] sampleStreams; - private boolean pendingInitialDiscontinuity; + private long pendingInitialDiscontinuityPositionUs; + /* package */ long startUs; + /* package */ long endUs; /** * Creates a new clipping media period that provides a clipped view of the specified @@ -57,10 +57,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb */ public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) { this.mediaPeriod = mediaPeriod; + sampleStreams = new ClippingSampleStream[0]; + pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? 0 : C.TIME_UNSET; startUs = C.TIME_UNSET; endUs = C.TIME_UNSET; - sampleStreams = new ClippingSampleStream[0]; - pendingInitialDiscontinuity = enableInitialDiscontinuity; } /** @@ -95,29 +95,27 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] internalStreams = new SampleStream[streams.length]; + SampleStream[] childStreams = new SampleStream[streams.length]; for (int i = 0; i < streams.length; i++) { sampleStreams[i] = (ClippingSampleStream) streams[i]; - internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null; + childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; } long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, - internalStreams, streamResetFlags, positionUs + startUs); - if (pendingInitialDiscontinuity) { - pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections); - } - Assertions.checkState(enablePositionUs == positionUs + startUs - || (enablePositionUs >= startUs - && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); + childStreams, streamResetFlags, positionUs + startUs) - startUs; + pendingInitialDiscontinuityPositionUs = isPendingInitialDiscontinuity() && positionUs == 0 + && shouldKeepInitialDiscontinuity(startUs, selections) ? enablePositionUs : C.TIME_UNSET; + Assertions.checkState(enablePositionUs == positionUs + || (enablePositionUs >= 0 + && (endUs == C.TIME_END_OF_SOURCE || startUs + enablePositionUs <= endUs))); for (int i = 0; i < streams.length; i++) { - if (internalStreams[i] == null) { + if (childStreams[i] == null) { sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, - pendingInitialDiscontinuity); + } else if (streams[i] == null || sampleStreams[i].childStream != childStreams[i]) { + sampleStreams[i] = new ClippingSampleStream(childStreams[i]); } streams[i] = sampleStreams[i]; } - return enablePositionUs - startUs; + return enablePositionUs; } @Override @@ -127,16 +125,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long readDiscontinuity() { - if (pendingInitialDiscontinuity) { - for (ClippingSampleStream sampleStream : sampleStreams) { - if (sampleStream != null) { - sampleStream.clearPendingDiscontinuity(); - } - } - pendingInitialDiscontinuity = false; - // Always read an initial discontinuity, using mediaPeriod's discontinuity if set. - long discontinuityUs = readDiscontinuity(); - return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0; + if (isPendingInitialDiscontinuity()) { + long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs; + pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; + // Always read an initial discontinuity from the child, and use it if set. + long childDiscontinuityUs = readDiscontinuity(); + return childDiscontinuityUs != C.TIME_UNSET ? childDiscontinuityUs : initialDiscontinuityUs; } long discontinuityUs = mediaPeriod.readDiscontinuity(); if (discontinuityUs == C.TIME_UNSET) { @@ -159,6 +153,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long seekToUs(long positionUs) { + pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; for (ClippingSampleStream sampleStream : sampleStreams) { if (sampleStream != null) { sampleStream.clearSentEos(); @@ -198,7 +193,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb callback.onContinueLoadingRequested(this); } - private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selections) { + /* package */ boolean isPendingInitialDiscontinuity() { + return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; + } + + private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -208,11 +207,13 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb // discontinuity which resets the renderers before they read the clipping sample stream. // However, for audio-only track selections we assume to have random access seek behaviour and // do not need an initial discontinuity to reset the renderer. - for (TrackSelection trackSelection : selections) { - if (trackSelection != null) { - Format selectedFormat = trackSelection.getSelectedFormat(); - if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { - return true; + if (startUs != 0) { + for (TrackSelection trackSelection : selections) { + if (trackSelection != null) { + Format selectedFormat = trackSelection.getSelectedFormat(); + if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { + return true; + } } } } @@ -222,27 +223,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** * Wraps a {@link SampleStream} and clips its samples. */ - private static final class ClippingSampleStream implements SampleStream { + private final class ClippingSampleStream implements SampleStream { - private final MediaPeriod mediaPeriod; - private final SampleStream stream; - private final long startUs; - private final long endUs; + public final SampleStream childStream; - private boolean pendingDiscontinuity; private boolean sentEos; - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; - this.stream = stream; - this.startUs = startUs; - this.endUs = endUs; - this.pendingDiscontinuity = pendingDiscontinuity; - } - - public void clearPendingDiscontinuity() { - pendingDiscontinuity = false; + public ClippingSampleStream(SampleStream childStream) { + this.childStream = childStream; } public void clearSentEos() { @@ -251,25 +239,25 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public boolean isReady() { - return stream.isReady(); + return !isPendingInitialDiscontinuity() && childStream.isReady(); } @Override public void maybeThrowError() throws IOException { - stream.maybeThrowError(); + childStream.maybeThrowError(); } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - if (pendingDiscontinuity) { + if (isPendingInitialDiscontinuity()) { return C.RESULT_NOTHING_READ; } if (sentEos) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } - int result = stream.readData(formatHolder, buffer, requireFormat); + int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { // Clear gapless playback metadata if the start/end points don't match the media. Format format = formatHolder.format; @@ -294,7 +282,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public int skipData(long positionUs) { - return stream.skipData(startUs + positionUs); + if (isPendingInitialDiscontinuity()) { + return C.RESULT_NOTHING_READ; + } + return childStream.skipData(startUs + positionUs); } } From 494a41c8b2eb84178b07acbe1877d84c32952fb9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 08:30:45 -0800 Subject: [PATCH 092/417] Improve ClippingMediaSource "cannot clip" behavior This brings ClippingMediaSource clip failures in line with what MergingMediaSource does when it cannot merge. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176660123 --- .../source/ClippingMediaSourceTest.java | 22 +++-- .../source/ConcatenatingMediaSourceTest.java | 15 ++-- .../DynamicConcatenatingMediaSourceTest.java | 23 ++--- .../source/LoopingMediaSourceTest.java | 11 +-- .../source/ClippingMediaSource.java | 89 ++++++++++++++++--- .../exoplayer2/source/MergingMediaSource.java | 4 +- .../testutil/MediaSourceTestRunner.java | 16 +++- 7 files changed, 134 insertions(+), 46 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index c72188ad2c..6b17bf1e40 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -21,11 +21,13 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.ClippingMediaSource.IllegalClippingException; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; /** * Unit tests for {@link ClippingMediaSource}. @@ -40,11 +42,12 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); window = new Timeline.Window(); period = new Timeline.Period(); } - public void testNoClipping() { + public void testNoClipping() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -55,7 +58,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testClippingUnseekableWindowThrows() { + public void testClippingUnseekableWindowThrows() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. @@ -64,12 +67,12 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { // If the unseekable window is clipped, clipping fails. getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US); fail("Expected clipping to fail."); - } catch (IllegalArgumentException e) { - // Expected. + } catch (IllegalClippingException e) { + assertEquals(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START, e.reason); } } - public void testClippingStart() { + public void testClippingStart() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, @@ -80,7 +83,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testClippingEnd() { + public void testClippingEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, @@ -91,7 +94,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testClippingStartAndEnd() { + public void testClippingStartAndEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, @@ -102,7 +105,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testWindowAndPeriodIndices() { + public void testWindowAndPeriodIndices() throws IOException { Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, @@ -122,7 +125,8 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { /** * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ - private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { + private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) + throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 1ca32be46d..71c4b71023 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; import junit.framework.TestCase; /** @@ -32,7 +33,7 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { - public void testEmptyConcatenation() { + public void testEmptyConcatenation() throws IOException { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); TimelineAsserts.assertEmpty(timeline); @@ -45,7 +46,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testSingleMediaSource() { + public void testSingleMediaSource() throws IOException { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); @@ -75,7 +76,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testMultipleMediaSources() { + public void testMultipleMediaSources() throws IOException { Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333) }; Timeline timeline = getConcatenatedTimeline(false, timelines); @@ -121,7 +122,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testNestedMediaSources() { + public void testNestedMediaSources() throws IOException { Timeline timeline = getConcatenatedTimeline(false, getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); @@ -149,7 +150,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } - public void testEmptyTimelineMediaSources() { + public void testEmptyTimelineMediaSources() throws IOException { // Empty timelines in the front, back, and the middle (single and multiple in a row). Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY, Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333), @@ -197,7 +198,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testPeriodCreationWithAds() throws InterruptedException { + public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); @@ -231,7 +232,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { * the concatenated timeline. */ private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, - Timeline... timelines) { + Timeline... timelines) throws IOException { MediaSource[] mediaSources = new MediaSource[timelines.length]; for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new FakeMediaSource(timelines[i], null); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 16c9e1a17c..5fa158725d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -55,7 +56,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { testRunner.release(); } - public void testPlaylistChangesAfterPreparation() { + public void testPlaylistChangesAfterPreparation() throws IOException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -171,7 +172,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { childSources[3].assertReleased(); } - public void testPlaylistChangesBeforePreparation() { + public void testPlaylistChangesBeforePreparation() throws IOException { FakeMediaSource[] childSources = createMediaSources(4); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); @@ -201,7 +202,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testPlaylistWithLazyMediaSource() { + public void testPlaylistWithLazyMediaSource() throws IOException { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); @@ -290,7 +291,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineMediaSource() { + public void testEmptyTimelineMediaSource() throws IOException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -426,7 +427,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { verify(runnable).run(); } - public void testCustomCallbackAfterPreparationAddSingle() { + public void testCustomCallbackAfterPreparationAddSingle() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -444,7 +445,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationAddMultiple() { + public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -464,7 +465,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationAddSingleWithIndex() { + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -482,7 +483,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() { + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -502,7 +503,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationRemove() { + public void testCustomCallbackAfterPreparationRemove() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -528,7 +529,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationMove() { + public void testCustomCallbackAfterPreparationMove() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -556,7 +557,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testPeriodCreationWithAds() throws InterruptedException { + public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create dynamic media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 6f69923ea2..7648af195c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; import junit.framework.TestCase; /** @@ -39,7 +40,7 @@ public class LoopingMediaSourceTest extends TestCase { new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)); } - public void testSingleLoop() { + public void testSingleLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -57,7 +58,7 @@ public class LoopingMediaSourceTest extends TestCase { } } - public void testMultiLoop() { + public void testMultiLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); @@ -77,7 +78,7 @@ public class LoopingMediaSourceTest extends TestCase { } } - public void testInfiniteLoop() { + public void testInfiniteLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -94,7 +95,7 @@ public class LoopingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineLoop() { + public void testEmptyTimelineLoop() throws IOException { Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); TimelineAsserts.assertEmpty(timeline); @@ -109,7 +110,7 @@ public class LoopingMediaSourceTest extends TestCase { * Wraps the specified timeline in a {@link LoopingMediaSource} and returns * the looping timeline. */ - private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { + private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 0b2ff33f30..721950f6b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -15,20 +15,68 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source may only have a single period/window. + * positions. The wrapped source must consist of a single period that starts at the beginning of the + * corresponding window. */ public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { + /** + * Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. + */ + public static final class IllegalClippingException extends IOException { + + /** + * The reason the clipping failed. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_PERIOD_OFFSET_IN_WINDOW, + REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) + public @interface Reason {} + /** + * The wrapped source doesn't consist of a single period. + */ + public static final int REASON_INVALID_PERIOD_COUNT = 0; + /** + * The wrapped source period doesn't start at the beginning of the corresponding window. + */ + public static final int REASON_PERIOD_OFFSET_IN_WINDOW = 1; + /** + * The wrapped source is not seekable and a non-zero clipping start position was specified. + */ + public static final int REASON_NOT_SEEKABLE_TO_START = 2; + /** + * The wrapped source ends before the specified clipping start position. + */ + public static final int REASON_START_EXCEEDS_END = 3; + + /** + * The reason clipping failed. + */ + @Reason + public final int reason; + + /** + * @param reason The reason clipping failed. + */ + public IllegalClippingException(@Reason int reason) { + this.reason = reason; + } + + } + private final MediaSource mediaSource; private final long startUs; private final long endUs; @@ -36,6 +84,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; + private IllegalClippingException clippingError; /** * Creates a new clipping source that wraps the specified source. @@ -88,6 +137,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void maybeThrowSourceInfoRefreshError() throws IOException { + if (clippingError != null) { + throw clippingError; + } mediaSource.maybeThrowSourceInfoRefreshError(); } @@ -115,8 +167,17 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { - sourceListener.onSourceInfoRefreshed(this, new ClippingTimeline(timeline, startUs, endUs), - manifest); + if (clippingError != null) { + return; + } + ClippingTimeline clippingTimeline; + try { + clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); + } catch (IllegalClippingException e) { + clippingError = e; + return; + } + sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest); int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).setClipping(startUs, endUs); @@ -138,22 +199,30 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * @param startUs The number of microseconds to clip from the start of {@code timeline}. * @param endUs The end position in microseconds for the clipped timeline relative to the start * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. + * @throws IllegalClippingException If the timeline could not be clipped. */ - public ClippingTimeline(Timeline timeline, long startUs, long endUs) { + public ClippingTimeline(Timeline timeline, long startUs, long endUs) + throws IllegalClippingException { super(timeline); - Assertions.checkArgument(timeline.getWindowCount() == 1); - Assertions.checkArgument(timeline.getPeriodCount() == 1); + if (timeline.getPeriodCount() != 1) { + throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT); + } + if (timeline.getPeriod(0, new Period()).getPositionInWindowUs() != 0) { + throw new IllegalClippingException(IllegalClippingException.REASON_PERIOD_OFFSET_IN_WINDOW); + } Window window = timeline.getWindow(0, new Window(), false); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { resolvedEndUs = window.durationUs; } - Assertions.checkArgument(startUs == 0 || window.isSeekable); - Assertions.checkArgument(startUs <= resolvedEndUs); + if (startUs != 0 && !window.isSeekable) { + throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); + } + if (startUs > resolvedEndUs) { + throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END); + } } - Period period = timeline.getPeriod(0, new Period()); - Assertions.checkArgument(period.getPositionInWindowUs() == 0); this.startUs = startUs; this.endUs = resolvedEndUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index ea0274796f..3b468d8709 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -45,11 +45,11 @@ public final class MergingMediaSource implements MediaSource { @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH}) public @interface Reason {} /** - * The merge failed because one of the sources being merged has a dynamic window. + * One of the sources being merged has a dynamic window. */ public static final int REASON_WINDOWS_ARE_DYNAMIC = 0; /** - * The merge failed because the sources have different period counts. + * The sources have different period counts. */ public static final int REASON_PERIOD_COUNT_MISMATCH = 1; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 235c04bef5..4f31a8b027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -32,7 +32,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; - +import java.io.IOException; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -100,13 +100,25 @@ public class MediaSourceTestRunner { * * @return The initial {@link Timeline}. */ - public Timeline prepareSource() { + public Timeline prepareSource() throws IOException { + final IOException[] prepareError = new IOException[1]; runOnPlaybackThread(new Runnable() { @Override public void run() { mediaSource.prepareSource(player, true, mediaSourceListener); + try { + // TODO: This only catches errors that are set synchronously in prepareSource. To capture + // async errors we'll need to poll maybeThrowSourceInfoRefreshError until the first call + // to onSourceInfoRefreshed. + mediaSource.maybeThrowSourceInfoRefreshError(); + } catch (IOException e) { + prepareError[0] = e; + } } }); + if (prepareError[0] != null) { + throw prepareError[0]; + } return assertTimelineChangeBlocking(); } From e04bdcea50c3242f91537554e01dbada73f1e6a3 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 09:14:00 -0800 Subject: [PATCH 093/417] Relax requirement that MergingMediaSource children are not dynamic ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176664332 --- .../source/MergingMediaSourceTest.java | 80 +++++++++++++++++++ .../exoplayer2/source/MergingMediaSource.java | 25 ++---- 2 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java new file mode 100644 index 0000000000..ba37385c75 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 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; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import java.io.IOException; +import junit.framework.TestCase; + +/** + * Unit tests for {@link MergingMediaSource}. + */ +public class MergingMediaSourceTest extends TestCase { + + public void testMergingDynamicTimelines() throws IOException { + FakeTimeline firstTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, true, C.TIME_UNSET)); + FakeTimeline secondTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, true, C.TIME_UNSET)); + testMergingMediaSourcePrepare(firstTimeline, secondTimeline); + } + + public void testMergingStaticTimelines() throws IOException { + FakeTimeline firstTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 20)); + FakeTimeline secondTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 10)); + testMergingMediaSourcePrepare(firstTimeline, secondTimeline); + } + + public void testMergingTimelinesWithDifferentPeriodCounts() throws IOException { + FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(1, null)); + FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(2, null)); + try { + testMergingMediaSourcePrepare(firstTimeline, secondTimeline); + fail("Expected merging to fail."); + } catch (IllegalMergeException e) { + assertEquals(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH, e.reason); + } + } + + /** + * Wraps the specified timelines in a {@link MergingMediaSource}, prepares it and checks that it + * forwards the first of the wrapped timelines. + */ + private static void testMergingMediaSourcePrepare(Timeline... timelines) throws IOException { + MediaSource[] mediaSources = new MediaSource[timelines.length]; + for (int i = 0; i < timelines.length; i++) { + mediaSources[i] = new FakeMediaSource(timelines[i], null); + } + MergingMediaSource mediaSource = new MergingMediaSource(mediaSources); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + Timeline timeline = testRunner.prepareSource(); + // The merged timeline should always be the one from the first child. + assertEquals(timelines[0], timeline); + } finally { + testRunner.release(); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 3b468d8709..79ed864e25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -28,8 +28,7 @@ import java.util.Arrays; /** * Merges multiple {@link MediaSource}s. *

      - * The {@link Timeline}s of the sources being merged must have the same number of periods, and must - * not have any dynamic windows. + * The {@link Timeline}s of the sources being merged must have the same number of periods. */ public final class MergingMediaSource implements MediaSource { @@ -42,26 +41,20 @@ public final class MergingMediaSource implements MediaSource { * The reason the merge failed. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH}) + @IntDef({REASON_PERIOD_COUNT_MISMATCH}) public @interface Reason {} - /** - * One of the sources being merged has a dynamic window. - */ - public static final int REASON_WINDOWS_ARE_DYNAMIC = 0; /** * The sources have different period counts. */ - public static final int REASON_PERIOD_COUNT_MISMATCH = 1; + public static final int REASON_PERIOD_COUNT_MISMATCH = 0; /** - * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and - * {@link #REASON_PERIOD_COUNT_MISMATCH}. + * The reason the merge failed. */ @Reason public final int reason; /** - * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and - * {@link #REASON_PERIOD_COUNT_MISMATCH}. + * @param reason The reason the merge failed. */ public IllegalMergeException(@Reason int reason) { this.reason = reason; @@ -73,7 +66,6 @@ public final class MergingMediaSource implements MediaSource { private final MediaSource[] mediaSources; private final ArrayList pendingTimelineSources; - private final Timeline.Window window; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Listener listener; @@ -100,7 +92,6 @@ public final class MergingMediaSource implements MediaSource { this.mediaSources = mediaSources; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); - window = new Timeline.Window(); periodCount = PERIOD_COUNT_UNSET; } @@ -170,12 +161,6 @@ public final class MergingMediaSource implements MediaSource { } private IllegalMergeException checkTimelineMerges(Timeline timeline) { - int windowCount = timeline.getWindowCount(); - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window, false).isDynamic) { - return new IllegalMergeException(IllegalMergeException.REASON_WINDOWS_ARE_DYNAMIC); - } - } if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); } else if (timeline.getPeriodCount() != periodCount) { From d537c21888ef0dcc7f4dae0fbb2fc8030877fab3 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 09:33:10 -0800 Subject: [PATCH 094/417] Test ClippingMediaSource handles initial dynamic timelines ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176666247 --- .../source/ClippingMediaSourceTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 6b17bf1e40..07e807ef9e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -94,6 +94,21 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } + public void testClippingStartAndEndInitial() throws IOException { + // Timeline that's dynamic and not seekable. A child source might report such a timeline prior + // to it having loaded sufficient data to establish its duration and seekability. Such timelines + // should not result in clipping failure. + Timeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, + /* isDynamic= */true); + + Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, + TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, + clippedTimeline.getWindow(0, window).getDurationUs()); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, + clippedTimeline.getPeriod(0, period).getDurationUs()); + } + public void testClippingStartAndEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); From 1442c047cffc17d9307d4579a805c1def64b4f22 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 13:22:41 -0800 Subject: [PATCH 095/417] Update gradle wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176693785 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2623db66fc..9f9081a945 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0' + classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: From ba5f35995f21c9c2a86842ac9dce1d13009a6378 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 Nov 2017 02:00:10 -0800 Subject: [PATCH 096/417] Send discontinuity at adjustments after shuffle/repeat mode changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176749136 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 6 +++++- 1 file changed, 5 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 63ae3c630e..316735da77 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 @@ -476,8 +476,12 @@ import java.io.IOException; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); + } } } From 2086c129fcea32590a3b43e0919137c21d61cf1b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 04:26:05 -0800 Subject: [PATCH 097/417] Suppress discontinuities that don't change the position This is mostly useful for suppressing the initial position discontinuity reported by ClippingMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176758972 --- .../android/exoplayer2/ExoPlayerTest.java | 46 ++++++++++--------- .../exoplayer2/ExoPlayerImplInternal.java | 12 +++-- .../exoplayer2/testutil/FakeMediaPeriod.java | 28 ++++++++++- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2392c32e0a..e911778992 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -343,12 +343,9 @@ public final class ExoPlayerTest extends TestCase { @Override protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - return new FakeMediaPeriod(trackGroupArray) { - @Override - public long seekToUs(long positionUs) { - return positionUs + 10; // Adjusts the requested seek position. - } - }; + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + mediaPeriod.setSeekToUsOffset(10); + return mediaPeriod; } }; ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuityAdjust") @@ -359,32 +356,39 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); } - public void testInternalDiscontinuity() throws Exception { + public void testInternalDiscontinuityAtNewPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - return new FakeMediaPeriod(trackGroupArray) { - boolean discontinuityRead; - @Override - public long readDiscontinuity() { - if (!discontinuityRead) { - discontinuityRead = true; - return 10; // Return a discontinuity. - } - return C.TIME_UNSET; - } - }; + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + mediaPeriod.setDiscontinuityPositionUs(10); + return mediaPeriod; } }; - ActionSchedule actionSchedule = new ActionSchedule.Builder("testInternalDiscontinuity") - .waitForPlaybackState(Player.STATE_READY).build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) - .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); } + public void testInternalDiscontinuityAtInitialPosition() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + mediaPeriod.setDiscontinuityPositionUs(0); + return mediaPeriod; + } + }; + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .build().start().blockUntilEnded(TIMEOUT_MS); + // If the position is unchanged we do not expect the discontinuity to be reported externally. + testRunner.assertNoPositionDiscontinuities(); + } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = 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 316735da77..69da4b1f5b 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 @@ -509,10 +509,14 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); - playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, - playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + // A MediaPeriod may report a discontinuity at the current playback position to ensure the + // renderers are flushed. Only report the discontinuity externally if the position changed. + if (periodPositionUs != playbackInfo.positionUs) { + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); + } } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 153a427bbd..0b409f5348 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -33,9 +33,31 @@ public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; private boolean preparedPeriod; + private long seekOffsetUs; + private long discontinuityPositionUs; public FakeMediaPeriod(TrackGroupArray trackGroupArray) { this.trackGroupArray = trackGroupArray; + discontinuityPositionUs = C.TIME_UNSET; + } + + /** + * Sets a discontinuity position to be returned from the next call to + * {@link #readDiscontinuity()}. + * + * @param discontinuityPositionUs The position to be returned, in microseconds. + */ + public void setDiscontinuityPositionUs(long discontinuityPositionUs) { + this.discontinuityPositionUs = discontinuityPositionUs; + } + + /** + * Sets an offset to be applied to positions returned by {@link #seekToUs(long)}. + * + * @param seekOffsetUs The offset to be applied, in microseconds. + */ + public void setSeekToUsOffset(long seekOffsetUs) { + this.seekOffsetUs = seekOffsetUs; } public void release() { @@ -92,7 +114,9 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long readDiscontinuity() { Assert.assertTrue(preparedPeriod); - return C.TIME_UNSET; + long positionDiscontinuityUs = this.discontinuityPositionUs; + this.discontinuityPositionUs = C.TIME_UNSET; + return positionDiscontinuityUs; } @Override @@ -104,7 +128,7 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long seekToUs(long positionUs) { Assert.assertTrue(preparedPeriod); - return positionUs; + return positionUs + seekOffsetUs; } @Override From 7eb0af7c0ef801c1a5a3497b91d129ab82abf942 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 Nov 2017 04:27:34 -0800 Subject: [PATCH 098/417] Replace LinkedList with ArrayList in ChunkSampleStream. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176759080 --- .../source/chunk/ChunkSampleStream.java | 89 ++++++++++--------- .../google/android/exoplayer2/util/Util.java | 11 +++ 2 files changed, 59 insertions(+), 41 deletions(-) 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 cfed38dd4a..e352ba551e 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 @@ -28,9 +28,10 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -51,7 +52,7 @@ public class ChunkSampleStream implements SampleStream, S private final int minLoadableRetryCount; private final Loader loader; private final ChunkHolder nextChunkHolder; - private final LinkedList mediaChunks; + private final ArrayList mediaChunks; private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; private final SampleQueue[] embeddedSampleQueues; @@ -85,7 +86,7 @@ public class ChunkSampleStream implements SampleStream, S this.minLoadableRetryCount = minLoadableRetryCount; loader = new Loader("Loader:ChunkSampleStream"); nextChunkHolder = new ChunkHolder(); - mediaChunks = new LinkedList<>(); + mediaChunks = new ArrayList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; @@ -170,7 +171,7 @@ public class ChunkSampleStream implements SampleStream, S return pendingResetPositionUs; } else { long bufferedPositionUs = lastSeekPositionUs; - BaseMediaChunk lastMediaChunk = mediaChunks.getLast(); + BaseMediaChunk lastMediaChunk = getLastMediaChunk(); BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; if (lastCompletedMediaChunk != null) { @@ -268,9 +269,11 @@ public class ChunkSampleStream implements SampleStream, S @Override public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; + } int skipCount; if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.advanceToEnd(); skipCount = primarySampleQueue.advanceToEnd(); } else { skipCount = primarySampleQueue.advanceTo(positionUs, true, true); @@ -325,7 +328,7 @@ public class ChunkSampleStream implements SampleStream, S } else { canceled = true; if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.removeLast(); + BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); Assertions.checkState(removed == loadable); primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { @@ -363,7 +366,7 @@ public class ChunkSampleStream implements SampleStream, S previousChunk = null; loadPositionUs = pendingResetPositionUs; } else { - previousChunk = mediaChunks.getLast(); + previousChunk = getLastMediaChunk(); loadPositionUs = previousChunk.endTimeUs; } chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); @@ -399,7 +402,7 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs; + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; } } @@ -412,6 +415,7 @@ public class ChunkSampleStream implements SampleStream, S * * @param positionUs The current playback position in microseconds. */ + @SuppressWarnings("unused") private void maybeDiscardUpstream(long positionUs) { int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); discardUpstreamMediaChunks(Math.max(1, queueSize)); @@ -425,7 +429,7 @@ public class ChunkSampleStream implements SampleStream, S * Returns whether samples have been read from {@code mediaChunks.getLast()}. */ private boolean haveReadFromLastMediaChunk() { - BaseMediaChunk lastChunk = mediaChunks.getLast(); + BaseMediaChunk lastChunk = getLastMediaChunk(); if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) { return true; } @@ -442,33 +446,21 @@ public class ChunkSampleStream implements SampleStream, S } private void discardDownstreamMediaChunks(int discardToPrimaryStreamIndex) { - if (!mediaChunks.isEmpty()) { - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= discardToPrimaryStreamIndex) { - mediaChunks.removeFirst(); - } + int discardToMediaChunkIndex = + primaryStreamIndexToMediaChunkIndex(discardToPrimaryStreamIndex, /* minChunkIndex= */ 0); + if (discardToMediaChunkIndex > 0) { + Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex); } } private void maybeNotifyPrimaryTrackFormatChanged(int toPrimaryStreamReadIndex, int readCount) { - if (!mediaChunks.isEmpty()) { - int fromPrimaryStreamReadIndex = toPrimaryStreamReadIndex - readCount; - int fromChunkIndex = 0; - while (fromChunkIndex < mediaChunks.size() - 1 - && mediaChunks.get(fromChunkIndex + 1).getFirstSampleIndex(0) - <= fromPrimaryStreamReadIndex) { - fromChunkIndex++; - } - int toChunkIndex = fromChunkIndex + 1; - if (readCount > 1) { - while (toChunkIndex < mediaChunks.size() - && mediaChunks.get(toChunkIndex).getFirstSampleIndex(0) < toPrimaryStreamReadIndex) { - toChunkIndex++; - } - } - for (int i = fromChunkIndex; i < toChunkIndex; i++) { - maybeNotifyPrimaryTrackFormatChanged(i); - } + int fromMediaChunkIndex = primaryStreamIndexToMediaChunkIndex( + toPrimaryStreamReadIndex - readCount, /* minChunkIndex= */ 0); + int toMediaChunkIndexInclusive = readCount == 1 ? fromMediaChunkIndex + : primaryStreamIndexToMediaChunkIndex(toPrimaryStreamReadIndex - 1, + /* minChunkIndex= */ fromMediaChunkIndex); + for (int i = fromMediaChunkIndex; i <= toMediaChunkIndexInclusive; i++) { + maybeNotifyPrimaryTrackFormatChanged(i); } } @@ -483,6 +475,23 @@ public class ChunkSampleStream implements SampleStream, S primaryDownstreamTrackFormat = trackFormat; } + /** + * Returns media chunk index for primary stream sample index. May be -1 if the list of media + * chunks is empty or the requested index is less than the first index in the first media chunk. + */ + private int primaryStreamIndexToMediaChunkIndex(int primaryStreamIndex, int minChunkIndex) { + for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) { + if (mediaChunks.get(i).getFirstSampleIndex(0) > primaryStreamIndex) { + return i - 1; + } + } + return mediaChunks.size() - 1; + } + + private BaseMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * @@ -493,16 +502,14 @@ public class ChunkSampleStream implements SampleStream, S if (mediaChunks.size() <= queueLength) { return false; } - BaseMediaChunk removed; - long startTimeUs; - long endTimeUs = mediaChunks.getLast().endTimeUs; - do { - removed = mediaChunks.removeLast(); - startTimeUs = removed.startTimeUs; - } while (mediaChunks.size() > queueLength); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + + long endTimeUs = getLastMediaChunk().endTimeUs; + BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength); + long startTimeUs = firstRemovedChunk.startTimeUs; + Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size()); + primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); } loadingFinished = false; eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); 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 4582ab7c86..b5a897dc16 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 @@ -191,6 +191,17 @@ public final class Util { return false; } + /** + * Removes an indexed range from a List. + * + * @param list The List to remove the range from. + * @param fromIndex The first index to be removed (inclusive). + * @param toIndex The last index to be removed (exclusive). + */ + public static void removeRange(List list, int fromIndex, int toIndex) { + list.subList(fromIndex, toIndex).clear(); + } + /** * Instantiates a new single threaded executor whose thread has the specified name. * From 2537e883d6750751bd1149a1d1d1a66c37c002c4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 04:58:03 -0800 Subject: [PATCH 099/417] Move HlsSampleStreamWrapper to use ArrayList Also prevent skip when there's a pending reset, and add a TODO to split/fix chunk discard and downstream format change reporting. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176760955 --- .../source/hls/HlsSampleStreamWrapper.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) 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 adedee7e83..06d48f1b08 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 @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -81,7 +81,7 @@ import java.util.LinkedList; private final Loader loader; private final EventDispatcher eventDispatcher; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; - private final LinkedList mediaChunks; + private final ArrayList mediaChunks; private final Runnable maybeFinishPrepareRunnable; private final Handler handler; @@ -137,7 +137,7 @@ import java.util.LinkedList; sampleQueues = new SampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; - mediaChunks = new LinkedList<>(); + mediaChunks = new ArrayList<>(); maybeFinishPrepareRunnable = new Runnable() { @Override public void run() { @@ -260,7 +260,7 @@ import java.util.LinkedList; if (!seenFirstTrackSelection) { long bufferedDurationUs = positionUs < 0 ? -positionUs : 0; primaryTrackSelection.updateSelectedTrack(positionUs, bufferedDurationUs, C.TIME_UNSET); - int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + int chunkIndex = chunkSource.getTrackGroup().indexOf(getLastMediaChunk().trackFormat); if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { // This is the first selection and the chunk loaded during preparation does not match // the initially selected format. @@ -332,7 +332,7 @@ import java.util.LinkedList; return pendingResetPositionUs; } else { long bufferedPositionUs = lastSeekPositionUs; - HlsMediaChunk lastMediaChunk = mediaChunks.getLast(); + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; if (lastCompletedMediaChunk != null) { @@ -389,11 +389,17 @@ import java.util.LinkedList; return C.RESULT_NOTHING_READ; } + // TODO: Split into discard (in discardBuffer) and format change (here and in skipData) steps. if (!mediaChunks.isEmpty()) { - while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { - mediaChunks.removeFirst(); + int discardToMediaChunkIndex = 0; + while (discardToMediaChunkIndex < mediaChunks.size() - 1 + && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) { + discardToMediaChunkIndex++; } - HlsMediaChunk currentChunk = mediaChunks.getFirst(); + if (discardToMediaChunkIndex > 0) { + Util.removeRange(mediaChunks, 0, discardToMediaChunkIndex); + } + HlsMediaChunk currentChunk = mediaChunks.get(0); Format trackFormat = currentChunk.trackFormat; if (!trackFormat.equals(downstreamTrackFormat)) { eventDispatcher.downstreamFormatChanged(trackType, trackFormat, @@ -408,6 +414,10 @@ import java.util.LinkedList; } public int skipData(int sampleQueueIndex, long positionUs) { + if (isPendingReset()) { + return 0; + } + SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); @@ -449,7 +459,7 @@ import java.util.LinkedList; previousChunk = null; loadPositionUs = pendingResetPositionUs; } else { - previousChunk = mediaChunks.getLast(); + previousChunk = getLastMediaChunk(); loadPositionUs = previousChunk.endTimeUs; } chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); @@ -489,7 +499,7 @@ import java.util.LinkedList; if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs; + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; } } @@ -531,7 +541,7 @@ import java.util.LinkedList; boolean canceled = false; if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { if (isMediaChunk) { - HlsMediaChunk removed = mediaChunks.removeLast(); + HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); Assertions.checkState(removed == loadable); if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; @@ -764,6 +774,10 @@ import java.util.LinkedList; containerFormat.language); } + private HlsMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); + } + private boolean isMediaChunk(Chunk chunk) { return chunk instanceof HlsMediaChunk; } From 874d1be8529771aac6e193f61b20f2cefbc3cde9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 05:43:38 -0800 Subject: [PATCH 100/417] stopInternal should release MediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176763841 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 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 69da4b1f5b..8895b8e03a 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 @@ -766,7 +766,7 @@ import java.io.IOException; } private void stopInternal() { - resetInternal(/* releaseMediaSource= */ false, /* resetPosition= */ false); + resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ false); loadControl.onStopped(); setState(Player.STATE_IDLE); } From 5d70b9e02d452aeba8a9bd92b5493dbffa596545 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 06:00:46 -0800 Subject: [PATCH 101/417] Partialy revert "Make ExtractorMediaSource timeline dynamic until duration is set" This change broke playback through playlists. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176764830 --- .../android/exoplayer2/source/ExtractorMediaSource.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 0839d06fdd..351416df6a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -327,11 +327,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // If the duration is currently unset, we expect to be able to update the window when its - // duration eventually becomes known. - boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. sourceListener.onSourceInfoRefreshed(this, - new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null); } } From 91bcde033caeafdb7747a1a85c031e0022777120 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 06:55:50 -0800 Subject: [PATCH 102/417] Fix release notes (change was cherry-picked) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176768835 --- RELEASENOTES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c01f2c29ee..3972b06d76 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,10 +2,6 @@ ### dev-v2 (not yet released) ### -* Fix reporting of internal position discontinuities via - `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is - added to disambiguate position adjustments during seeks from other types of - internal position discontinuity. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, @@ -36,6 +32,10 @@ * SimpleExoPlayer: Support for multiple video, text and metadata outputs. * Support for `Renderer`s that don't consume any media ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` ([#3362](https://github.com/google/ExoPlayer/issues/3362)). * Fix playbacks involving looping, concatenation and ads getting stuck when From 77d8c13621e1ecb8a8fef8062ac439477c5d3dd1 Mon Sep 17 00:00:00 2001 From: simophin Date: Fri, 24 Nov 2017 17:27:35 +1300 Subject: [PATCH 103/417] Guard against out-of-range timestamp We've found that in our production environment, the AAC stream's timestamp exceeds the 33bit limit from time to time, when it happens, `peekId3PrivTimestamp` returns a value that is greater than `TimestampAdjuster.MAX_PTS_PLUS_ONE`, which causes a overflow in `TimestampAdjuster.adjustTsTimestamp` (overflow inside `ptsToUs`) after playing for a while . When the overflow happens, the start time of the stream becomes negative and the playback simply stucks at buffering forever. I fully understand that the 33bit is a spec requirement, thus I asked our stream provider to correct this mistake. But in the mean time, I'd also like ExoPlayer to handle this situation more error tolerance, as in other platforms (iOS, browsers) we see more tolerance behavior. --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5ca8675dd9..83167c152f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong(); + return id3Data.readLong() & ((1L << 33) - 1L); } } } From 5cd8869646b0391cd26a194564772d0c64587df0 Mon Sep 17 00:00:00 2001 From: baiming Date: Fri, 24 Nov 2017 01:32:44 -0800 Subject: [PATCH 104/417] Really fix the NPE in ExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176821463 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 8895b8e03a..4e37211e80 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 @@ -947,10 +947,12 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; while (periodHolder != null) { - TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); - for (TrackSelection trackSelection : trackSelections) { - if (trackSelection != null) { - trackSelection.onPlaybackSpeed(playbackSpeed); + if (periodHolder.trackSelectorResult != null) { + TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } } } periodHolder = periodHolder.next; From de476ba4e67bb3f1b9ad4f5a97ef8f997ebe0463 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 02:20:35 -0800 Subject: [PATCH 105/417] Propagate the player error to ExoPlayerTestRunner In a test run where no exceptions were thrown on the main thread and the test did not time out, exceptions from onPlayerError were not correctly propagated to the test thread (handleException would be called with null). Fix ExoPlayerTestRunner.onPlayerError to propagate the actual exception from the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176825907 --- .../google/android/exoplayer2/testutil/ExoPlayerTestRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 5ada65ef1e..759af41039 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -567,7 +567,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPlayerError(ExoPlaybackException error) { - handleException(exception); + handleException(error); } @Override From 36255c42cf66c7be10ec2a4336cda22e094c7bf7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 05:37:49 -0800 Subject: [PATCH 106/417] Test setPlaybackParameters before preparation completes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176837939 --- .../android/exoplayer2/ExoPlayerTest.java | 44 ++++++++++++ .../android/exoplayer2/testutil/Action.java | 25 +++++++ .../exoplayer2/testutil/ActionSchedule.java | 13 ++++ .../exoplayer2/testutil/FakeMediaPeriod.java | 68 +++++++++++++++---- 4 files changed, 136 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index e911778992..5f41e57a6a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -570,4 +570,48 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertPlayedPeriodIndices(0, 1, 0); } + public void testSetPlaybackParametersBeforePreparationCompletesSucceeds() throws Exception { + // Test that no exception is thrown when playback parameters are updated between creating a + // period and preparation of the period completing. + final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); + final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1]; + MediaSource mediaSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { + // Defer completing preparation of the period until playback parameters have been set. + fakeMediaPeriodHolder[0] = + new FakeMediaPeriod(trackGroupArray, /* deferOnPrepared= */ true); + createPeriodCalledCountDownLatch.countDown(); + return fakeMediaPeriodHolder[0]; + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetPlaybackParametersBeforePreparationCompletesSucceeds") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Block until createPeriod has been called on the fake media source. + .executeRunnable(new Runnable() { + @Override + public void run() { + try { + createPeriodCalledCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + }) + // Set playback parameters (while the fake media period is not yet prepared). + .setPlaybackParameters(new PlaybackParameters(2f, 2f)) + // Complete preparation of the fake media period. + .executeRunnable(new Runnable() { + @Override + public void run() { + fakeMediaPeriodHolder[0].setPreparationComplete(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + } } 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 357d69df38..003d08cd59 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 @@ -19,6 +19,7 @@ import android.os.Handler; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -303,6 +304,30 @@ public abstract class Action { } + /** + * Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. + */ + public static final class SetPlaybackParameters extends Action { + + private final PlaybackParameters playbackParameters; + + /** + * @param tag A tag to use for logging. + * @param playbackParameters The playback parameters. + */ + public SetPlaybackParameters(String tag, PlaybackParameters playbackParameters) { + super(tag, "SetPlaybackParameters:" + playbackParameters); + this.playbackParameters = playbackParameters; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setPlaybackParameters(playbackParameters); + } + + } + /** * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. */ 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 ddfa2345ee..2dbb4e18d2 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 @@ -19,6 +19,7 @@ import android.os.Handler; import android.os.Looper; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; +import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; @@ -151,6 +153,17 @@ public final class ActionSchedule { .apply(new WaitForPlaybackState(tag, Player.STATE_READY)); } + /** + * Schedules a playback parameters setting action to be executed. + * + * @param playbackParameters The playback parameters to set. + * @return The builder, for convenience. + * @see Player#setPlaybackParameters(PlaybackParameters) + */ + public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { + return apply(new SetPlaybackParameters(tag, playbackParameters)); + } + /** * Schedules a stop action to be executed. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 0b409f5348..c1be199b1e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; @@ -32,12 +34,30 @@ public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; - private boolean preparedPeriod; + @Nullable private Handler playerHandler; + @Nullable private Callback prepareCallback; + + private boolean deferOnPrepared; + private boolean prepared; private long seekOffsetUs; private long discontinuityPositionUs; + /** + * @param trackGroupArray The track group array. + */ public FakeMediaPeriod(TrackGroupArray trackGroupArray) { + this(trackGroupArray, false); + } + + /** + * @param trackGroupArray The track group array. + * @param deferOnPrepared Whether {@link MediaPeriod.Callback#onPrepared(MediaPeriod)} should be + * called only after {@link #setPreparationComplete()} has been called. If {@code false} + * preparation completes immediately. + */ + public FakeMediaPeriod(TrackGroupArray trackGroupArray, boolean deferOnPrepared) { this.trackGroupArray = trackGroupArray; + this.deferOnPrepared = deferOnPrepared; discontinuityPositionUs = C.TIME_UNSET; } @@ -51,6 +71,22 @@ public class FakeMediaPeriod implements MediaPeriod { this.discontinuityPositionUs = discontinuityPositionUs; } + /** + * Allows the fake media period to complete preparation. May be called on any thread. + */ + public synchronized void setPreparationComplete() { + deferOnPrepared = false; + if (playerHandler != null && prepareCallback != null) { + playerHandler.post(new Runnable() { + @Override + public void run() { + prepared = true; + prepareCallback.onPrepared(FakeMediaPeriod.this); + } + }); + } + } + /** * Sets an offset to be applied to positions returned by {@link #seekToUs(long)}. * @@ -61,31 +97,35 @@ public class FakeMediaPeriod implements MediaPeriod { } public void release() { - preparedPeriod = false; + prepared = false; } @Override - public void prepare(Callback callback, long positionUs) { - Assert.assertFalse(preparedPeriod); - preparedPeriod = true; - callback.onPrepared(this); + public synchronized void prepare(Callback callback, long positionUs) { + if (deferOnPrepared) { + playerHandler = new Handler(); + prepareCallback = callback; + } else { + prepared = true; + callback.onPrepared(this); + } } @Override public void maybeThrowPrepareError() throws IOException { - Assert.assertTrue(preparedPeriod); + // Do nothing. } @Override public TrackGroupArray getTrackGroups() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return trackGroupArray; } @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); int rendererCount = selections.length; for (int i = 0; i < rendererCount; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { @@ -113,7 +153,7 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long readDiscontinuity() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); long positionDiscontinuityUs = this.discontinuityPositionUs; this.discontinuityPositionUs = C.TIME_UNSET; return positionDiscontinuityUs; @@ -121,25 +161,25 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long getBufferedPositionUs() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return C.TIME_END_OF_SOURCE; } @Override public long seekToUs(long positionUs) { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return positionUs + seekOffsetUs; } @Override public long getNextLoadPositionUs() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return C.TIME_END_OF_SOURCE; } @Override public boolean continueLoading(long positionUs) { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return false; } From c4385c738f12986702cf94ff6330ae2768bdae7b Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 24 Nov 2017 08:30:53 -0800 Subject: [PATCH 107/417] Temporarily fix flakiness of testPlayEmptyTimeline. Fixed by explicitly waiting for the timeline update. This shouldn't be necessary and will be removed as soon as the correct order of events can be guaranteed (timeline change -> state change -> onSeekProcessed). The waiting for the timeline update is implemented by introducing the feature that the test runner also waits until the action schedule has finished before stopping the test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176848540 --- .../android/exoplayer2/ExoPlayerTest.java | 9 ++- .../exoplayer2/testutil/ActionSchedule.java | 52 +++++++++++++++++- .../exoplayer2/testutil/ExoHostedTest.java | 4 +- .../testutil/ExoPlayerTestRunner.java | 55 ++++++++++++++++--- 4 files changed, 106 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 5f41e57a6a..8213e6133d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -56,8 +56,15 @@ public final class ExoPlayerTest extends TestCase { public void testPlayEmptyTimeline() throws Exception { Timeline timeline = Timeline.EMPTY; FakeRenderer renderer = new FakeRenderer(); + // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline + // update happens after the transition to STATE_ENDED and the test runner may already have been + // stopped. Remove action schedule as soon as state changes are part of the masking and the + // correct order of events is restored. + ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlayEmptyTimeline") + .waitForTimelineChanged(timeline) + .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline).setRenderers(renderer) + .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); 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 2dbb4e18d2..5e3d6bcb9a 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; @@ -47,13 +48,28 @@ import com.google.android.exoplayer2.util.Clock; */ public final class ActionSchedule { + /** + * Callback to notify listener that the action schedule has finished. + */ + public interface Callback { + + /** + * Called when action schedule finished executing all its actions. + */ + void onActionScheduleFinished(); + + } + private final ActionNode rootNode; + private final CallbackAction callbackAction; /** * @param rootNode The first node in the sequence. + * @param callbackAction The final action which can be used to trigger a callback. */ - private ActionSchedule(ActionNode rootNode) { + private ActionSchedule(ActionNode rootNode, CallbackAction callbackAction) { this.rootNode = rootNode; + this.callbackAction = callbackAction; } /** @@ -63,9 +79,12 @@ public final class ActionSchedule { * @param trackSelector The track selector to which actions should be applied. * @param surface The surface to use when applying actions. * @param mainHandler A handler associated with the main thread of the host activity. + * @param callback A {@link Callback} to notify when the action schedule finishes, or null if no + * notification is needed. */ /* package */ void start(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface, Handler mainHandler) { + Surface surface, Handler mainHandler, @Nullable Callback callback) { + callbackAction.setCallback(callback); rootNode.schedule(player, trackSelector, surface, mainHandler); } @@ -304,7 +323,9 @@ public final class ActionSchedule { } public ActionSchedule build() { - return new ActionSchedule(rootNode); + CallbackAction callbackAction = new CallbackAction(tag); + apply(callbackAction); + return new ActionSchedule(rootNode, callbackAction); } private Builder appendActionNode(ActionNode actionNode) { @@ -420,4 +441,29 @@ public final class ActionSchedule { } + /** + * An action calling a specified {@link ActionSchedule.Callback}. + */ + private static final class CallbackAction extends Action { + + private @Nullable Callback callback; + + public CallbackAction(String tag) { + super(tag, "FinishedCallback"); + } + + public void setCallback(@Nullable Callback callback) { + this.callback = callback; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + if (callback != null) { + callback.onActionScheduleFinished(); + } + } + + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ee4018ba0e..ab31238983 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -126,7 +126,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen if (player == null) { pendingSchedule = schedule; } else { - schedule.start(player, trackSelector, surface, actionHandler); + schedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); } } @@ -162,7 +162,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen actionHandler = new Handler(); // Schedule any pending actions. if (pendingSchedule != null) { - pendingSchedule.start(player, trackSelector, surface, actionHandler); + pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); pendingSchedule = null; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 759af41039..7b3292db89 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.HandlerThread; +import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -48,7 +50,8 @@ import junit.framework.Assert; /** * Helper class to run an ExoPlayer test. */ -public final class ExoPlayerTestRunner extends Player.DefaultEventListener { +public final class ExoPlayerTestRunner extends Player.DefaultEventListener + implements ActionSchedule.Callback { /** * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for @@ -327,12 +330,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final RenderersFactory renderersFactory; private final MappingTrackSelector trackSelector; private final LoadControl loadControl; - private final ActionSchedule actionSchedule; - private final Player.EventListener eventListener; + private final @Nullable ActionSchedule actionSchedule; + private final @Nullable Player.EventListener eventListener; private final HandlerThread playerThread; private final Handler handler; private final CountDownLatch endedCountDownLatch; + private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; private final ArrayList manifests; private final ArrayList timelineChangeReasons; @@ -346,8 +350,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, - LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener, - int expectedPlayerEndedCount) { + LoadControl loadControl, @Nullable ActionSchedule actionSchedule, + @Nullable Player.EventListener eventListener, int expectedPlayerEndedCount) { this.playerFactory = playerFactory; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -361,6 +365,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); + this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); this.handler = new Handler(playerThread.getLooper()); @@ -387,7 +392,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } player.setPlayWhenReady(true); if (actionSchedule != null) { - actionSchedule.start(player, trackSelector, null, handler); + actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } player.prepare(mediaSource); } catch (Exception e) { @@ -400,8 +405,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { /** * Blocks the current thread until the test runner finishes. A test is deemed to be finished when - * the playback state transitions to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE}, or - * when am {@link ExoPlaybackException} is thrown. + * the action schedule finished and the playback state transitioned to {@link Player#STATE_ENDED} + * or {@link Player#STATE_IDLE} for the specified number of times. The test also finishes when an + * {@link ExoPlaybackException} is thrown. * * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed * the method will throw a {@link TimeoutException}. @@ -409,6 +415,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { * @throws Exception If any exception occurred during playback, release, or due to a timeout. */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + long deadlineMs = SystemClock.elapsedRealtime() + timeoutMs; + try { + blockUntilActionScheduleFinished(timeoutMs); + } catch (TimeoutException error) { + exception = error; + } + timeoutMs = Math.max(0, deadlineMs - SystemClock.elapsedRealtime()); if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); } @@ -420,6 +433,24 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Blocks the current thread until the action schedule finished. Also returns when an + * {@link ExoPlaybackException} is thrown. This does not release the test runner and the test must + * still call {@link #blockUntilEnded(long)}. + * + * @param timeoutMs The maximum time to wait for the action schedule to finish. + * @return This test runner. + * @throws TimeoutException If the action schedule did not finish within the specified timeout. + * @throws InterruptedException If the test thread gets interrupted while waiting. + */ + public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) + throws TimeoutException, InterruptedException { + if (!actionScheduleFinishedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Test playback timed out waiting for action schedule to finish."); + } + return this; + } + // Assertions called on the test thread after test finished. /** @@ -536,6 +567,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { while (endedCountDownLatch.getCount() > 0) { endedCountDownLatch.countDown(); } + actionScheduleFinishedCountDownLatch.countDown(); } // Player.EventListener @@ -582,4 +614,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + // ActionSchedule.Callback + + @Override + public void onActionScheduleFinished() { + actionScheduleFinishedCountDownLatch.countDown(); + } + } From a9ed6b191dc57c971d70842b07c6a325dffb0574 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 10:00:44 -0800 Subject: [PATCH 108/417] Switch from currentTimeMillis to elapsedRealtime currentTimeMillis is not guaranteed to be monotonic and elapsedRealtime is recommend for interval timing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176853118 --- .../google/android/exoplayer2/util/ConditionVariable.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 262d120af8..058a5d6dd2 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 @@ -60,18 +60,18 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until timeout milliseconds have passed. + * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. * * @param timeout The maximum time to wait in milliseconds. - * @return true If the condition was opened, false if the call returns because of the timeout. + * @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 = System.currentTimeMillis(); + long now = android.os.SystemClock.elapsedRealtime(); long end = now + timeout; while (!isOpen && now < end) { wait(end - now); - now = System.currentTimeMillis(); + now = android.os.SystemClock.elapsedRealtime(); } return isOpen; } From 8833a2930c0f6a7f43dd27787b72fd6d2c866ce5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Nov 2017 01:43:34 -0800 Subject: [PATCH 109/417] Take into account the playback speed for loading Update the default AdaptiveTrackSelection and DefaultLoadControl to use playback speed information. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176989168 --- .../exoplayer2/DefaultLoadControl.java | 5 +++++ .../AdaptiveTrackSelection.java | 22 ++++++++++++++----- .../google/android/exoplayer2/util/Util.java | 13 +++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index bfafd409f3..d329f6584b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -172,6 +172,11 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + if (bufferedDurationUs >= minBufferUs) { + // It's possible that we're not loading, so allow playback to start unconditionally. + return true; + } + bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index f9eddab286..ba45b2b186 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Util; import java.util.List; /** @@ -139,6 +140,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private float playbackSpeed; private int selectedIndex; private int reason; @@ -196,10 +198,16 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + playbackSpeed = 1f; selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; } + @Override + public void onPlaybackSpeed(float playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { @@ -254,8 +262,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return 0; } int queueSize = queue.size(); - long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; - if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) { + long mediaBufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; + long playoutBufferedDurationUs = + Util.getPlayoutDurationForMediaDuration(mediaBufferedDurationUs, playbackSpeed); + if (playoutBufferedDurationUs < minDurationToRetainAfterDiscardUs) { return queueSize; } int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); @@ -266,8 +276,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < queueSize; i++) { MediaChunk chunk = queue.get(i); Format format = chunk.trackFormat; - long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; - if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs + long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; + long playoutDurationBeforeThisChunkUs = + Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed); + if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs && format.bitrate < idealFormat.bitrate && format.height != Format.NO_VALUE && format.height < 720 && format.width != Format.NO_VALUE && format.width < 1280 @@ -292,7 +304,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { Format format = getFormat(i); - if (format.bitrate <= effectiveBitrate) { + if (Math.round(format.bitrate * playbackSpeed) <= effectiveBitrate) { return i; } else { lowestBitrateNonBlacklistedIndex = i; 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 b5a897dc16..881da0868f 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 @@ -697,6 +697,19 @@ public final class Util { return Math.round((double) playoutDuration * speed); } + /** + * Returns the playout duration of {@code mediaDuration} of media. + * + * @param mediaDuration The duration to scale. + * @return The scaled duration, in the same units as {@code mediaDuration}. + */ + public static long getPlayoutDurationForMediaDuration(long mediaDuration, float speed) { + if (speed == 1f) { + return mediaDuration; + } + return Math.round((double) mediaDuration / speed); + } + /** * Converts a list of integers to a primitive array. * From 70169af6a09f25f2465650f3e67931a55e860ce5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Nov 2017 01:50:47 -0800 Subject: [PATCH 110/417] Update version strings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176989632 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3902ec5cbd..ecfe3eb96f 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,18 @@ Next add a gradle compile dependency to the `build.gradle` file of your app module. The following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `r2.X.X` is your preferred version. Alternatively, you can depend on only +where `2.X.X` is your preferred version. Alternatively, you can depend on only the library modules that you actually need. For example the following will add dependencies on the Core, DASH and UI library modules, as might be required for an app that plays DASH content: ```gradle -compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer-core:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` The available library modules are listed below. Adding a dependency to the full From a4fbb453252f1318b3d0df775e47490536398451 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:45:24 -0800 Subject: [PATCH 111/417] Remove race condition when stopping FakeExoPlayer. A message to stop the playback and to quit the playback thread was posted in release(). The stop message removed all other already queued messages which might include the second message to quit the thread. That led to infinite waiting in the release method because the playback thread never got the quit signal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997104 --- .../testutil/FakeSimpleExoPlayer.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 094aaa5273..dc4f191885 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -166,27 +166,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - releaseMedia(); - changePlaybackState(Player.STATE_IDLE); - } - }); + stop(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(); - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - playbackThread.quit(); - } - }); + stop(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -527,6 +513,20 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } + private void stop(boolean quitPlaybackThread) { + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + releaseMedia(); + changePlaybackState(Player.STATE_IDLE); + if (quitPlaybackThread) { + playbackThread.quit(); + } + } + }); + } + } } From 818d5a0b0062e846acdd26cda1ffdde8c178eb63 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:57:45 -0800 Subject: [PATCH 112/417] Add final to boolean used within Runnable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997767 --- .../google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index dc4f191885..58f19ace1e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -513,7 +513,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(boolean quitPlaybackThread) { + private void stop(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { From 16c43c6bb71a6ee486d79355189e2445ef7753b9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 27 Nov 2017 06:29:47 -0800 Subject: [PATCH 113/417] Support undefined text track language when preferred is not available Also slightly improve language normalization/documentation. For this CL, it is assumed that null and "und" languages are different entities. Once we fully tackle language tag normalization, we can decide whether to normalize the "undefined" language. Issue:#2867 Issue:#2980 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177008509 --- RELEASENOTES.md | 5 +- .../java/com/google/android/exoplayer2/C.java | 5 + .../trackselection/DefaultTrackSelector.java | 149 ++++++++++++------ .../google/android/exoplayer2/util/Util.java | 14 +- .../DefaultTrackSelectorTest.java | 64 ++++++++ 5 files changed, 181 insertions(+), 56 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3972b06d76..dd4a6ce655 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,8 +18,11 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Added a reason to `EventListener.onTimelineChanged` to distinguish between +* Added a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). ### 2.6.0 ### 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 592589e221..6a35c0c5e8 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 @@ -424,6 +424,11 @@ public final class C { */ public static final int SELECTION_FLAG_AUTOSELECT = 4; + /** + * Represents an undetermined language as an ISO 639 alpha-3 language code. + */ + public static final String LANGUAGE_UNDETERMINED = "und"; + /** * Represents a streaming or other media type. */ 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 c789caded4..0029cdbd31 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 @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicReference; * Parameters currentParameters = trackSelector.getParameters(); * // Generate new parameters to prefer German audio and impose a maximum video size constraint. * Parameters newParameters = currentParameters - * .withPreferredAudioLanguage("de") + * .withPreferredAudioLanguage("deu") * .withMaxVideoSize(1024, 768); * // Set the new parameters on the selector. * trackSelector.setParameters(newParameters);} @@ -81,17 +81,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * {@code null} selects the default track, or the first track if there's no default. */ public final String preferredAudioLanguage; // Text /** - * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * default track if there is one, or no track otherwise. */ public final String preferredTextLanguage; + /** + * Whether a text track with undetermined language should be selected if no track with + * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + */ + public final boolean selectUndeterminedTextLanguage; // Video /** @@ -150,6 +155,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
        *
      • No preferred audio language is set.
      • *
      • No preferred text language is set.
      • + *
      • Text tracks with undetermined language are not selected if no track with + * {@link #preferredTextLanguage} is available.
      • *
      • Lowest bitrate track selections are not forced.
      • *
      • Adaptation between different mime types is not allowed.
      • *
      • Non seamless adaptation is allowed.
      • @@ -161,13 +168,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
      */ public Parameters() { - this(null, null, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** * @param preferredAudioLanguage See {@link #preferredAudioLanguage} * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. * @param forceLowestBitrate See {@link #forceLowestBitrate}. * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} @@ -181,13 +189,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, - boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, + boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -209,10 +218,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -223,10 +233,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + /** + * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. + */ + public Parameters withSelectUndeterminedTextLanguageAsFallback( + boolean selectUndeterminedTextLanguage) { + if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -236,10 +262,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (forceLowestBitrate == this.forceLowestBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -249,10 +276,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -262,10 +290,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -275,10 +304,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -288,10 +318,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoBitrate == this.maxVideoBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -320,10 +351,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -334,10 +366,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -350,10 +383,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -880,17 +914,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, params.preferredTextLanguage)) { + boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); + if (preferredLanguageFound + || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { if (isDefault) { - trackScore = 6; + trackScore = 8; } else if (!isForced) { // Prefer non-forced to forced if a preferred text language has been specified. Where // both are provided the non-forced track will usually contain the forced subtitles as // a subset. - trackScore = 5; + trackScore = 6; } else { trackScore = 4; } + trackScore += preferredLanguageFound ? 1 : 0; } else if (isDefault) { trackScore = 3; } else if (isForced) { @@ -980,6 +1017,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} does not define a language. + * + * @param format The {@link Format}. + * @return Whether the {@link Format} does not define a language. + */ + protected static boolean formatHasNoLanguage(Format format) { + return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED); + } + /** * Returns whether a {@link Format} specifies a particular language, or {@code false} if * {@code language} is null. 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 881da0868f..0594f52288 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 @@ -50,6 +50,7 @@ import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -249,13 +250,18 @@ public final class Util { } /** - * Returns a normalized RFC 5646 language code. + * Returns a normalized RFC 639-2/T code for {@code language}. * - * @param language A possibly non-normalized RFC 5646 language code. - * @return The normalized code, or null if the input was null. + * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. + * @return The all-lowercase normalized code, or null if the input was null, or + * {@code language.toLowerCase()} if the language could not be normalized. */ public static String normalizeLanguageCode(String language) { - return language == null ? null : new Locale(language).getLanguage(); + try { + return language == null ? null : new Locale(language).getISO3Language(); + } catch (MissingResourceException e) { + return language.toLowerCase(); + } } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index a0e499139c..b2b149b004 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -36,6 +36,8 @@ public final class DefaultTrackSelectorTest { private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_TEXT); private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); @@ -534,6 +536,60 @@ public final class DefaultTrackSelectorTest { .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** + * Tests that the default track selector will select a text track with undetermined language if no + * text track with the preferred language is available but + * {@link Parameters#selectUndeterminedTextLanguage} is true. + */ + @Test + public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ + Format spanish = Format.createTextContainerFormat("spanish", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "spa"); + Format german = Format.createTextContainerFormat("german", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "de"); + Format undeterminedUnd = Format.createTextContainerFormat("undeterminedUnd", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "und"); + Format undeterminedNull = Format.createTextContainerFormat("undeterminedNull", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, null); + + RendererCapabilities[] textRendererCapabilites = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedNull); + + result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german)); + assertThat(result.selections.get(0)).isNull(); + } + /** * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} * indicate lowest bitrate preference, even when tracks are within capabilities. @@ -562,6 +618,14 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(new TrackGroup(formats)); } + private static TrackGroupArray wrapFormats(Format... formats) { + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < trackGroups.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + return new TrackGroupArray(trackGroups); + } + /** * A {@link RendererCapabilities} that advertises support for all formats of a given type using * a provided support value. For any format that does not have the given track type, From ee26da682c495d0a8b6ed4022273bf5d0fa4d196 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 07:02:33 -0800 Subject: [PATCH 114/417] Add throws IllegalSeekPositionException doc to seekTo(windowIndex, positionMs). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177011497 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 77fced0832..a036a2021d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -401,6 +401,8 @@ public interface Player { * @param windowIndex The index of the window. * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to * the window's default position. + * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided + * {@code windowIndex} is not within the bounds of the current timeline. */ void seekTo(int windowIndex, long positionMs); From 95de9c96fe0eedee5b7f34bcc939ed5f3da6ea41 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 08:58:57 -0800 Subject: [PATCH 115/417] Don't always wait for action schedule in ExoPlayerTestRunner. Unconditionally waiting for the action schedule to finish in ExoPlayerTestRunner doesn't work if the action schedule is not intended to be finished. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177024139 --- .../google/android/exoplayer2/ExoPlayerTest.java | 2 +- .../exoplayer2/testutil/ExoPlayerTestRunner.java | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8213e6133d..27e4a97ac5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -65,7 +65,7 @@ public final class ExoPlayerTest extends TestCase { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) - .build().start().blockUntilEnded(TIMEOUT_MS); + .build().start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 7b3292db89..62e950091b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.HandlerThread; -import android.os.SystemClock; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; @@ -405,9 +404,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener /** * Blocks the current thread until the test runner finishes. A test is deemed to be finished when - * the action schedule finished and the playback state transitioned to {@link Player#STATE_ENDED} - * or {@link Player#STATE_IDLE} for the specified number of times. The test also finishes when an - * {@link ExoPlaybackException} is thrown. + * the playback state transitioned to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE} for + * the specified number of times. The test also finishes when an {@link ExoPlaybackException} is + * thrown. * * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed * the method will throw a {@link TimeoutException}. @@ -415,13 +414,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener * @throws Exception If any exception occurred during playback, release, or due to a timeout. */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { - long deadlineMs = SystemClock.elapsedRealtime() + timeoutMs; - try { - blockUntilActionScheduleFinished(timeoutMs); - } catch (TimeoutException error) { - exception = error; - } - timeoutMs = Math.max(0, deadlineMs - SystemClock.elapsedRealtime()); if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); } From a0f6bba842d8aeb8716c31a2ff2a4b207b970861 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 12:42:53 -0800 Subject: [PATCH 116/417] Force wrapping of HLS ID3 timestamp Merge of https://github.com/google/ExoPlayer/pull/3495 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177057183 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 83167c152f..1ad5acc5c5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong() & ((1L << 33) - 1L); + return id3Data.readLong() & 0x1FFFFFFFFL; } } } From 117608edef8c896f9496f0ed3651d0826c4ccf28 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 28 Nov 2017 17:02:04 +0000 Subject: [PATCH 117/417] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..e85c0c28c7 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From ad16efdf5688b1f25fb3d6f694b41d9c928287e3 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:45:54 +0100 Subject: [PATCH 118/417] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) 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 68d39b5a18..fbaf9ea111 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 @@ -962,19 +962,39 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { + private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - // TODO: It may be necessary to handle timestamp offsets from UTC. - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); + + if (firstLine != null) { + //determine format pattern + String formatPattern; + if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_2; + } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_3; + } else { + formatPattern = ISO_8601_FORMAT; + } + //parse + try { + SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.parse(firstLine).getTime(); + } catch (ParseException e) { + throw new ParserException(e); + } + + } else { + throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } } From 28d709aa8f7b5a4c57f0e68515b3a099e3da4a57 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:52:12 +0100 Subject: [PATCH 119/417] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 fbaf9ea111..e2143b4bf5 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 @@ -963,10 +963,9 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { @@ -975,10 +974,10 @@ public final class DashMediaSource implements MediaSource { if (firstLine != null) { //determine format pattern String formatPattern; - if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_2; - } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_3; + if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; } else { formatPattern = ISO_8601_FORMAT; } @@ -995,6 +994,7 @@ public final class DashMediaSource implements MediaSource { throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } + } + } From d84398788ab67871c4a511df06248464c412e5dc Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 13:32:44 -0800 Subject: [PATCH 120/417] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177063576 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 1ad5acc5c5..c4e54d4bd3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -306,6 +306,8 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); + // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the + // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495. return id3Data.readLong() & 0x1FFFFFFFFL; } } From f46cb907b7f88eb753497ff4bb92ceb694025329 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 01:53:15 -0800 Subject: [PATCH 121/417] Add stop with position reset to Player interface. The ExoPlayerImpl implementation forwards the stop request with this optional parameter. To ensure correct masking (e.g. when timeline updates arrive after calling reset in ExoPlayerImpl but before resetInternal in ExoPlayerImplInternal), we use the existing prepareAck counter and extend it also count stop operations. For this to work, we also return the updated empty timeline after finishing the reset. The CastPlayer doesn't support the two reset options so far. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177132107 --- RELEASENOTES.md | 3 +- .../exoplayer2/ext/cast/CastPlayer.java | 6 + .../android/exoplayer2/ExoPlayerTest.java | 154 ++++++++++++++++++ .../android/exoplayer2/ExoPlayerImpl.java | 85 +++++----- .../exoplayer2/ExoPlayerImplInternal.java | 44 +++-- .../com/google/android/exoplayer2/Player.java | 24 ++- .../android/exoplayer2/SimpleExoPlayer.java | 8 + .../android/exoplayer2/testutil/Action.java | 54 +++++- .../exoplayer2/testutil/ActionSchedule.java | 21 +++ .../testutil/FakeSimpleExoPlayer.java | 11 +- .../exoplayer2/testutil/StubExoPlayer.java | 5 + 11 files changed, 349 insertions(+), 66 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dd4a6ce655..2c07ad6118 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,11 +18,12 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Added a reason to `EventListener.onTimelineChanged` to distinguish between +* Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add optional parameter to `Player.stop` to reset the player when stopping. ### 2.6.0 ### diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 32e064e834..92e36c7f2d 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -359,7 +359,13 @@ public final class CastPlayer implements Player { @Override public void stop() { + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { if (remoteMediaClient != null) { + // TODO(b/69792021): Support or emulate stop without position reset. remoteMediaClient.stop(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 27e4a97ac5..4c5ac1ac0f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -621,4 +621,158 @@ public final class ExoPlayerTest extends TestCase { new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); } + + public void testStopDoesNotResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopDoesNotResetPosition") + .waitForPlaybackState(Player.STATE_READY) + .stop() + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithoutResetDoesNotResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopWithoutResetDoesNotReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithResetDoesResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopWithResetDoesReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithoutResetReleasesMediaSource() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + mediaSource.assertReleased(); + testRunner.blockUntilEnded(TIMEOUT_MS); + } + + public void testStopWithResetReleasesMediaSource() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + mediaSource.assertReleased(); + testRunner.blockUntilEnded(TIMEOUT_MS); + } + + public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationAfterStop") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(secondSource) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekAfterStopWithReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + // If we were still using the first timeline, this would throw. + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .prepareSource(secondSource) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + testRunner.assertPlayedPeriodIndices(0, 1); + } + + public void testStopDuringPreparationOverwritesPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopOverwritesPrepare") + .waitForPlaybackState(Player.STATE_BUFFERING) + .stop(true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 77131f5ded..34dffd0e73 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -55,7 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean shuffleModeEnabled; private int playbackState; private int pendingSeekAcks; - private int pendingPrepareAcks; + private int pendingPrepareOrStopAcks; private boolean waitingForInitialTimeline; private boolean isLoading; private TrackGroupArray trackGroups; @@ -134,35 +134,9 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - if (!resetPosition) { - maskingWindowIndex = getCurrentWindowIndex(); - maskingPeriodIndex = getCurrentPeriodIndex(); - maskingWindowPositionMs = getCurrentPosition(); - } else { - maskingWindowIndex = 0; - maskingPeriodIndex = 0; - maskingWindowPositionMs = 0; - } - if (resetState) { - if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { - playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, - Player.TIMELINE_CHANGE_REASON_RESET); - } - } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - } waitingForInitialTimeline = true; - pendingPrepareAcks++; + pendingPrepareOrStopAcks++; + reset(resetPosition, resetState); internalPlayer.prepare(mediaSource, resetPosition); } @@ -286,7 +260,14 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop() { - internalPlayer.stop(); + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { + pendingPrepareOrStopAcks++; + reset(/* resetPosition= */ reset, /* resetState= */ reset); + internalPlayer.stop(reset); } @Override @@ -468,14 +449,14 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - int prepareAcks = msg.arg1; + int prepareOrStopAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, seekAcks, false, /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareAcks == 0) { + if (pendingPrepareOrStopAcks == 0) { TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; tracksSelected = true; trackGroups = trackSelectorResult.groups; @@ -520,12 +501,12 @@ import java.util.concurrent.CopyOnWriteArraySet; } } - private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, + private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); - pendingPrepareAcks -= prepareAcks; + pendingPrepareOrStopAcks -= prepareOrStopAcks; pendingSeekAcks -= seekAcks; - if (pendingPrepareAcks == 0 && pendingSeekAcks == 0) { + if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; @@ -556,6 +537,36 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private void reset(boolean resetPosition, boolean resetState) { + if (resetPosition) { + maskingWindowIndex = 0; + maskingPeriodIndex = 0; + maskingWindowPositionMs = 0; + } else { + maskingWindowIndex = getCurrentWindowIndex(); + maskingPeriodIndex = getCurrentPeriodIndex(); + maskingWindowPositionMs = getCurrentPosition(); + } + if (resetState) { + if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { + playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, + Player.TIMELINE_CHANGE_REASON_RESET); + } + } + if (tracksSelected) { + tracksSelected = false; + trackGroups = TrackGroupArray.EMPTY; + trackSelections = emptyTrackSelections; + trackSelector.onSelectionActivated(null); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } + } + } + } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { long positionMs = C.usToMs(positionUs); if (!playbackInfo.periodId.isAd()) { @@ -566,7 +577,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareAcks > 0; + return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0; } } 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 4e37211e80..f62d36e48b 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 @@ -196,8 +196,8 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } - public void stop() { - handler.sendEmptyMessage(MSG_STOP); + public void stop(boolean reset) { + handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } public void sendMessages(ExoPlayerMessage... messages) { @@ -324,7 +324,7 @@ import java.io.IOException; return true; } case MSG_STOP: { - stopInternal(); + stopInternal(/* reset= */ msg.arg1 != 0); return true; } case MSG_RELEASE: { @@ -357,18 +357,18 @@ import java.io.IOException; } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } catch (IOException e) { Log.e(TAG, "Source error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } } @@ -394,8 +394,8 @@ import java.io.IOException; resetInternal(/* releaseMediaSource= */ true, resetPosition); loadControl.onPrepared(); this.mediaSource = mediaSource; - mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); setState(Player.STATE_BUFFERING); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -765,8 +765,23 @@ import java.io.IOException; mediaClock.setPlaybackParameters(playbackParameters); } - private void stopInternal() { - resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ false); + private void stopInternal(boolean reset) { + // Releasing the internal player sets the timeline to null. Use the current timeline or + // Timeline.EMPTY for notifying the eventHandler. + Timeline publicTimeline = reset || playbackInfo.timeline == null + ? Timeline.EMPTY : playbackInfo.timeline; + Object publicManifest = reset ? null : playbackInfo.manifest; + resetInternal(/* releaseMediaSource= */ true, reset); + PlaybackInfo publicPlaybackInfo = playbackInfo.copyWithTimeline(publicTimeline, publicManifest); + if (reset) { + // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for + // notifying the eventHandler. + publicPlaybackInfo = + publicPlaybackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET); + } + int prepareOrStopAcks = pendingPrepareCount + 1; + pendingPrepareCount = 0; + notifySourceInfoRefresh(prepareOrStopAcks, 0, publicPlaybackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1170,13 +1185,14 @@ import java.io.IOException; notifySourceInfoRefresh(0, 0); } - private void notifySourceInfoRefresh(int prepareAcks, int seekAcks) { - notifySourceInfoRefresh(prepareAcks, seekAcks, playbackInfo); + private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks) { + notifySourceInfoRefresh(prepareOrStopAcks, seekAcks, playbackInfo); } - private void notifySourceInfoRefresh(int prepareAcks, int seekAcks, PlaybackInfo playbackInfo) { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareAcks, seekAcks, playbackInfo) - .sendToTarget(); + private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks, + PlaybackInfo playbackInfo) { + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, seekAcks, + playbackInfo).sendToTarget(); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index a036a2021d..b3ae4c28c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -429,17 +429,29 @@ public interface Player { PlaybackParameters getPlaybackParameters(); /** - * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention - * is to pause playback. - *

      - * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than + * this method if the intention is to pause playback. + * + *

      Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The * player instance can still be used, and {@link #release()} must still be called on the player if * it's no longer required. - *

      - * Calling this method does not reset the playback position. + * + *

      Calling this method does not reset the playback position. */ void stop(); + /** + * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather + * than this method if the intention is to pause playback. + * + *

      Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * player instance can still be used, and {@link #release()} must still be called on the player if + * it's no longer required. + * + * @param reset Whether the player should be reset. + */ + void stop(boolean reset); + /** * Releases the player. This method must be called when the player is no longer required. The * player must not be used after calling this method. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5a5a948d58..a153e4ed43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -133,6 +133,9 @@ public class SimpleExoPlayer implements ExoPlayer { case C.TRACK_TYPE_AUDIO: audioRendererCount++; break; + default: + // Don't count other track types. + break; } } this.videoRendererCount = videoRendererCount; @@ -692,6 +695,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.stop(); } + @Override + public void stop(boolean reset) { + player.stop(reset); + } + @Override public void release() { player.release(); 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 003d08cd59..ff0b8a6bc0 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 @@ -89,45 +89,89 @@ public abstract class Action { Surface surface); /** - * Calls {@link Player#seekTo(long)}. + * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. */ public static final class Seek extends Action { + private final Integer windowIndex; private final long positionMs; /** + * Action calls {@link Player#seekTo(long)}. + * * @param tag A tag to use for logging. * @param positionMs The seek position. */ public Seek(String tag, long positionMs) { super(tag, "Seek:" + positionMs); + this.windowIndex = null; + this.positionMs = positionMs; + } + + /** + * Action calls {@link Player#seekTo(int, long)}. + * + * @param tag A tag to use for logging. + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + */ + public Seek(String tag, int windowIndex, long positionMs) { + super(tag, "Seek:" + positionMs); + this.windowIndex = windowIndex; this.positionMs = positionMs; } @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - player.seekTo(positionMs); + if (windowIndex == null) { + player.seekTo(positionMs); + } else { + player.seekTo(windowIndex, positionMs); + } } } /** - * Calls {@link Player#stop()}. + * Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { + private static final String STOP_ACTION_TAG = "Stop"; + + private final Boolean reset; + /** + * Action will call {@link Player#stop()}. + * * @param tag A tag to use for logging. */ public Stop(String tag) { - super(tag, "Stop"); + super(tag, STOP_ACTION_TAG); + this.reset = null; + } + + /** + * Action will call {@link Player#stop(boolean)}. + * + * @param tag A tag to use for logging. + * @param reset The value to pass to {@link Player#stop(boolean)}. + */ + public Stop(String tag, boolean reset) { + super(tag, STOP_ACTION_TAG); + this.reset = reset; } @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - player.stop(); + if (reset == null) { + player.stop(); + } else { + player.stop(reset); + } + } } 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 5e3d6bcb9a..abca2cafdb 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 @@ -160,6 +160,17 @@ public final class ActionSchedule { return apply(new Seek(tag, positionMs)); } + /** + * Schedules a seek action to be executed. + * + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + * @return The builder, for convenience. + */ + public Builder seek(int windowIndex, long positionMs) { + return apply(new Seek(tag, windowIndex, positionMs)); + } + /** * Schedules a seek action to be executed and waits until playback resumes after the seek. * @@ -192,6 +203,16 @@ public final class ActionSchedule { return apply(new Stop(tag)); } + /** + * Schedules a stop action to be executed. + * + * @param reset Whether the player should be reset. + * @return The builder, for convenience. + */ + public Builder stop(boolean reset) { + return apply(new Stop(tag, reset)); + } + /** * Schedules a play action to be executed. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 58f19ace1e..0358e5d980 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -166,13 +166,18 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - stop(/* quitPlaybackThread= */ false); + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { + stopPlayback(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(/* quitPlaybackThread= */ true); + stopPlayback(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -513,7 +518,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(final boolean quitPlaybackThread) { + private void stopPlayback(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index e03f6fbad9..0d94b8fa03 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -130,6 +130,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void stop(boolean resetStateAndPosition) { + throw new UnsupportedOperationException(); + } + @Override public void release() { throw new UnsupportedOperationException(); From 1ae50cb9e56e6b6a00606f1fac3066d2461db17d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 03:58:30 -0800 Subject: [PATCH 122/417] Add some clarifications to MediaSource documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177141094 --- .../android/exoplayer2/source/MediaSource.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 7288b39897..4a0d8e196d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -35,7 +35,8 @@ import java.io.IOException; * player to load and read the media. * * All methods are called on the player's internal playback thread, as described in the - * {@link ExoPlayer} Javadoc. + * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances + * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once. */ public interface MediaSource { @@ -150,6 +151,8 @@ public interface MediaSource { /** * Starts preparation of the source. + *

      + * Should not be called directly from application code. * * @param player The player for which this source is being prepared. * @param isTopLevelSource Whether this source has been passed directly to @@ -162,6 +165,8 @@ public interface MediaSource { /** * Throws any pending error encountered while loading or refreshing source information. + *

      + * Should not be called directly from application code. */ void maybeThrowSourceInfoRefreshError() throws IOException; @@ -169,6 +174,8 @@ public interface MediaSource { * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called * multiple times with the same period identifier without an intervening call to * {@link #releasePeriod(MediaPeriod)}. + *

      + * Should not be called directly from application code. * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -178,6 +185,8 @@ public interface MediaSource { /** * Releases the period. + *

      + * Should not be called directly from application code. * * @param mediaPeriod The period to release. */ @@ -186,8 +195,7 @@ public interface MediaSource { /** * Releases the source. *

      - * This method should be called when the source is no longer required. It may be called in any - * state. + * Should not be called directly from application code. */ void releaseSource(); From efc709f36616d7bdce397f307d69e9fd09b4192b Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 04:40:06 -0800 Subject: [PATCH 123/417] Remove initial seek counting in ExoPlayerImplInternal. We can acknoledge seeks before preparation finished immediately now, because ExoPlayerImpl won't leave the masking state until the first prepare operation is processed. As a side effect, it also cleans up the responsibility of the callbacks. Prepares are always acknowledged with a SOURCE_INFO_REFRESHED, while seeks are always acknowledged with a SEEK_ACK. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177144089 --- .../android/exoplayer2/ExoPlayerTest.java | 29 +++++++---- .../android/exoplayer2/ExoPlayerImpl.java | 15 +++--- .../exoplayer2/ExoPlayerImplInternal.java | 48 +++++++++---------- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 4c5ac1ac0f..efb7b0e96c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -306,11 +306,19 @@ public final class ExoPlayerTest extends TestCase { public void testSeekProcessedCallback() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") - // Initial seek before timeline preparation finished. - .pause().seek(10).waitForPlaybackState(Player.STATE_READY) - // Re-seek to same position, start playback and wait until playback reaches second window. - .seek(10).play().waitForPositionDiscontinuity() - // Seek twice in concession, expecting the first seek to be replaced. + // Initial seek before timeline preparation started. Expect immediate seek processed while + // the player is still in STATE_IDLE. + .pause().seek(5) + // Wait until the media source starts preparing and issue more initial seeks. Expect only + // one seek processed after the source has been prepared. + .waitForPlaybackState(Player.STATE_BUFFERING).seek(2).seek(10) + // Wait until media source prepared and re-seek to same position. Expect a seek processed + // while still being in STATE_READY. + .waitForPlaybackState(Player.STATE_READY).seek(10) + // Start playback and wait until playback reaches second window. + .play().waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced (and thus except only + // on seek processed callback). .seek(5).seek(60).build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); Player.EventListener eventListener = new Player.DefaultEventListener() { @@ -329,10 +337,11 @@ public final class ExoPlayerTest extends TestCase { new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); - assertEquals(3, playbackStatesWhenSeekProcessed.size()); - assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); - assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(1)); - assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); + assertEquals(4, playbackStatesWhenSeekProcessed.size()); + assertEquals(Player.STATE_IDLE, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(1)); + assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(2)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); } public void testSeekDiscontinuity() throws Exception { @@ -742,7 +751,7 @@ public final class ExoPlayerTest extends TestCase { .waitForPlaybackState(Player.STATE_IDLE) // If we were still using the first timeline, this would throw. .seek(/* windowIndex= */ 1, /* positionMs= */ 0) - .prepareSource(secondSource) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 34dffd0e73..37fccafd08 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -450,8 +450,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareOrStopAcks = msg.arg1; - int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, seekAcks, false, + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, 0, false, /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } @@ -510,13 +509,13 @@ import java.util.concurrent.CopyOnWriteArraySet; boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; - if (playbackInfo.timeline.isEmpty()) { - // Update the masking variables, which are used when the timeline is empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; - } if (timelineOrManifestChanged || waitingForInitialTimeline) { + if (playbackInfo.timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline becomes empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } @Player.TimelineChangeReason int reason = waitingForInitialTimeline ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; waitingForInitialTimeline = false; 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 f62d36e48b..909f52fad8 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 @@ -125,8 +125,7 @@ import java.io.IOException; private long elapsedRealtimeUs; private int pendingPrepareCount; - private int pendingInitialSeekCount; - private SeekPosition pendingSeekPosition; + private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private MediaPeriodHolder loadingPeriodHolder; @@ -631,8 +630,9 @@ import java.io.IOException; private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { Timeline timeline = playbackInfo.timeline; if (timeline == null) { - pendingInitialSeekCount++; - pendingSeekPosition = seekPosition; + pendingInitialSeekPosition = seekPosition; + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 0, 0, + playbackInfo.copyWithTimeline(Timeline.EMPTY, null)).sendToTarget(); return; } @@ -781,7 +781,7 @@ import java.io.IOException; } int prepareOrStopAcks = pendingPrepareCount + 1; pendingPrepareCount = 0; - notifySourceInfoRefresh(prepareOrStopAcks, 0, publicPlaybackInfo); + notifySourceInfoRefresh(prepareOrStopAcks, publicPlaybackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -825,6 +825,7 @@ import java.io.IOException; ? 0 : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) .firstPeriodIndex; + pendingInitialSeekPosition = null; playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); } else { // The new start position is the current playback position. @@ -1009,15 +1010,13 @@ import java.io.IOException; if (oldTimeline == null) { int processedPrepareAcks = pendingPrepareCount; pendingPrepareCount = 0; - if (pendingInitialSeekCount > 0) { - Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - int processedInitialSeekCount = pendingInitialSeekCount; - pendingInitialSeekCount = 0; - pendingSeekPosition = null; + if (pendingInitialSeekPosition != null) { + Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks, processedInitialSeekCount); + handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); } else { int periodIndex = periodPosition.first; long positionUs = periodPosition.second; @@ -1025,11 +1024,11 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(processedPrepareAcks, processedInitialSeekCount); + notifySourceInfoRefresh(processedPrepareAcks); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks, 0); + handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); } else { Pair defaultPosition = getPeriodPosition(timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); @@ -1039,10 +1038,10 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(processedPrepareAcks, 0); + notifySourceInfoRefresh(processedPrepareAcks); } } else { - notifySourceInfoRefresh(processedPrepareAcks, 0); + notifySourceInfoRefresh(processedPrepareAcks); } return; } @@ -1169,30 +1168,29 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback() { - handleSourceInfoRefreshEndedPlayback(0, 0); + handleSourceInfoRefreshEndedPlayback(0); } - private void handleSourceInfoRefreshEndedPlayback(int prepareAcks, int seekAcks) { + private void handleSourceInfoRefreshEndedPlayback(int prepareAcks) { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false, true); // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - notifySourceInfoRefresh(prepareAcks, seekAcks, + notifySourceInfoRefresh(prepareAcks, playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); } private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(0, 0); + notifySourceInfoRefresh(0); } - private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks) { - notifySourceInfoRefresh(prepareOrStopAcks, seekAcks, playbackInfo); + private void notifySourceInfoRefresh(int prepareOrStopAcks) { + notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); } - private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks, - PlaybackInfo playbackInfo) { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, seekAcks, - playbackInfo).sendToTarget(); + private void notifySourceInfoRefresh(int prepareOrStopAcks, PlaybackInfo playbackInfo) { + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) + .sendToTarget(); } /** From 20567633a03fb4c90b36e31b8cfad2e25f81f3fd Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 05:02:15 -0800 Subject: [PATCH 124/417] Add queue manipulation to the Cast demo Against all odds, samples can be reordered by using drag & drop. Issue:#1706 Issue:#2283 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177145553 --- demos/cast/build.gradle | 1 + .../exoplayer2/castdemo/MainActivity.java | 178 +++++++-- .../exoplayer2/castdemo/PlayerManager.java | 347 ++++++++++++++---- .../res/drawable/ic_add_circle_white_24dp.xml | 20 + .../src/main/res/layout/main_activity.xml | 32 +- .../cast/src/main/res/layout/sample_list.xml | 25 ++ demos/cast/src/main/res/values/strings.xml | 2 + .../exoplayer2/ext/cast/CastPlayer.java | 19 +- 8 files changed, 517 insertions(+), 107 deletions(-) create mode 100644 demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml create mode 100644 demos/cast/src/main/res/layout/sample_list.xml diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index a9fa27ad58..8f074c9238 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -48,4 +48,5 @@ dependencies { compile project(modulePrefix + 'library-smoothstreaming') compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'extension-cast') + compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 094e9f9e6e..d34888352f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -15,37 +15,54 @@ */ package com.google.android.exoplayer2.castdemo; -import android.graphics.Color; +import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; +import android.support.v4.graphics.ColorUtils; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; /** * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. */ -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements OnClickListener, + PlayerManager.QueuePositionListener { private SimpleExoPlayerView simpleExoPlayerView; private PlaybackControlView castControlView; private PlayerManager playerManager; + private MediaQueueAdapter listAdapter; + private CastContext castContext; // Activity lifecycle methods. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + castContext = CastContext.getSharedInstance(this); setContentView(R.layout.main_activity); @@ -54,24 +71,30 @@ public class MainActivity extends AppCompatActivity { castControlView = findViewById(R.id.cast_control_view); - ListView sampleList = findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleListAdapter()); - sampleList.setOnItemClickListener(new SampleClickListener()); + RecyclerView sampleList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(sampleList); + sampleList.setLayoutManager(new LinearLayoutManager(this)); + sampleList.setHasFixedSize(true); + listAdapter = new MediaQueueAdapter(); + sampleList.setAdapter(listAdapter); + + findViewById(R.id.add_sample_button).setOnClickListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.menu, menu); - CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, - R.id.media_route_menu_item); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); return true; } @Override public void onResume() { super.onResume(); - playerManager = new PlayerManager(simpleExoPlayerView, castControlView, this); + playerManager = PlayerManager.createPlayerManager(this, simpleExoPlayerView, castControlView, + this, castContext); } @Override @@ -89,32 +112,141 @@ public class MainActivity extends AppCompatActivity { return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); } - // User controls. + @Override + public void onClick(View view) { + new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) + .setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() + .show(); + } - private final class SampleListAdapter extends ArrayAdapter { + // PlayerManager.QueuePositionListener implementation. - public SampleListAdapter() { - super(getApplicationContext(), android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + listAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + listAdapter.notifyItemChanged(newIndex); + } + } + + // Internal methods. + + private View buildSampleListView() { + View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); + ListView sampleList = dialogList.findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter(this)); + sampleList.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + listAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + } + + }); + return dialogList; + } + + // Internal classes. + + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); } @Override - @NonNull - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - view.setBackgroundColor(Color.WHITE); - return view; + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); } } - private class SampleClickListener implements AdapterView.OnItemClickListener { + private class MediaQueueAdapter extends RecyclerView.Adapter { @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (parent.getSelectedItemPosition() != position) { - DemoUtil.Sample currentSample = DemoUtil.SAMPLES.get(position); - playerManager.setCurrentSample(currentSample, 0, true); + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + TextView view = holder.textView; + view.setText(playerManager.getItem(position).name); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; } + draggingToPosition = toPosition; + listAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + if (playerManager.removeItem(position)) { + listAdapter.notifyItemRemoved(position); + } + } + + @Override + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + listAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index f00d27a067..0f4adfae99 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -19,11 +19,19 @@ import android.content.Context; import android.net.Uri; import android.view.KeyEvent; import android.view.View; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -40,14 +48,25 @@ import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; /** - * Manages players for the ExoPlayer/Cast integration app. + * Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener { +/* package */ final class PlayerManager extends DefaultEventListener + implements CastPlayer.SessionAvailabilityListener { - private static final int PLAYBACK_REMOTE = 1; - private static final int PLAYBACK_LOCAL = 2; + /** + * Listener for changes in the media queue playback position. + */ + public interface QueuePositionListener { + + /** + * Called when the currently played item of the media queue changes. + */ + void onQueuePositionChanged(int previousIndex, int newIndex); + + } private static final String USER_AGENT = "ExoCastDemoPlayer"; private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); @@ -58,64 +77,174 @@ import com.google.android.gms.cast.framework.CastContext; private final PlaybackControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final QueuePositionListener listener; - private int playbackLocation; - private DemoUtil.Sample currentSample; + private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; + private boolean castMediaQueueCreationPending; + private int currentItemIndex; + private Player currentPlayer; /** + * @param listener A {@link QueuePositionListener} for queue position changes. * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. * @param castControlView The {@link PlaybackControlView} to control remote playback. * @param context A {@link Context}. + * @param castContext The {@link CastContext}. */ - public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, - Context context) { + public static PlayerManager createPlayerManager(QueuePositionListener listener, + SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, Context context, + CastContext castContext) { + PlayerManager playerManager = new PlayerManager(listener, exoPlayerView, castControlView, + context, castContext); + playerManager.init(); + return playerManager; + } + + private PlayerManager(QueuePositionListener listener, SimpleExoPlayerView exoPlayerView, + PlaybackControlView castControlView, Context context, CastContext castContext) { + this.listener = listener; this.exoPlayerView = exoPlayerView; this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + exoPlayer.addListener(this); exoPlayerView.setPlayer(exoPlayer); - castPlayer = new CastPlayer(CastContext.getSharedInstance(context)); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); + } - setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); } /** - * Starts playback of the given sample at the given position. - * - * @param currentSample The {@link DemoUtil} to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. + * Returns the index of the currently played item. */ - public void setCurrentSample(DemoUtil.Sample currentSample, long positionMs, - boolean playWhenReady) { - this.currentSample = currentSample; - if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); - castPlayer.setPlayWhenReady(playWhenReady); - } else /* playbackLocation == PLAYBACK_LOCAL */ { - exoPlayer.prepare(buildMediaSource(currentSample), true, true); - exoPlayer.setPlayWhenReady(playWhenReady); - exoPlayer.seekTo(positionMs); + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code sample} to the media queue. + * + * @param sample The {@link Sample} to append. + */ + public void addItem(Sample sample) { + mediaQueue.add(sample); + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + } else { + castPlayer.addItems(buildMediaQueueItem(sample)); } } /** - * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current - * playback location. + * Returns the size of the media queue. + */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public Sample getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param itemIndex The index of the item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(int itemIndex) { + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); + } else { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param fromIndex The index of the item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(int fromIndex, int toIndex) { + // Player update. + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + // Miscellaneous methods. + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. * * @param event The {@link KeyEvent}. * @return Whether the event was handled by the target view. */ public boolean dispatchKeyEvent(KeyEvent event) { - if (playbackLocation == PLAYBACK_REMOTE) { - return castControlView.dispatchKeyEvent(event); - } else /* playbackLocation == PLAYBACK_REMOTE */ { + if (currentPlayer == exoPlayer) { return exoPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); } } @@ -123,33 +252,136 @@ import com.google.android.gms.cast.framework.CastContext; * Releases the manager and the players that it holds. */ public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); exoPlayerView.setPlayer(null); exoPlayer.release(); } + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + updateCurrentItemIndex(); + } + // CastPlayer.SessionAvailabilityListener implementation. @Override public void onCastSessionAvailable() { - setPlaybackLocation(PLAYBACK_REMOTE); + setCurrentPlayer(castPlayer); } @Override public void onCastSessionUnavailable() { - setPlaybackLocation(PLAYBACK_LOCAL); + setCurrentPlayer(exoPlayer); } // Internal methods. - private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); - MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) - .setMetadata(movieMetadata).build(); - return new MediaQueueItem.Builder(mediaInfo).build(); + private void init() { + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + exoPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + exoPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + if (this.currentPlayer != null) { + int playbackState = this.currentPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = this.currentPlayer.getCurrentPosition(); + playWhenReady = this.currentPlayer.getPlayWhenReady(); + windowIndex = this.currentPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + this.currentPlayer.stop(true); + } else { + // This is the initial setup. No need to save any state. + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + castMediaQueueCreationPending = currentPlayer == castPlayer; + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); + for (int i = 0; i < mediaQueue.size(); i++) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); + } + exoPlayer.prepare(dynamicConcatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (castMediaQueueCreationPending) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castMediaQueueCreationPending = false; + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } } private static MediaSource buildMediaSource(DemoUtil.Sample sample) { @@ -177,36 +409,13 @@ import com.google.android.gms.cast.framework.CastContext; } } - private void setPlaybackLocation(int playbackLocation) { - if (this.playbackLocation == playbackLocation) { - return; - } - - // View management. - if (playbackLocation == PLAYBACK_LOCAL) { - exoPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else { - exoPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - long playbackPositionMs; - boolean playWhenReady; - if (this.playbackLocation == PLAYBACK_LOCAL) { - playbackPositionMs = exoPlayer.getCurrentPosition(); - playWhenReady = exoPlayer.getPlayWhenReady(); - exoPlayer.stop(); - } else /* this.playbackLocation == PLAYBACK_REMOTE */ { - playbackPositionMs = castPlayer.getCurrentPosition(); - playWhenReady = castPlayer.getPlayWhenReady(); - castPlayer.stop(); - } - - this.playbackLocation = playbackLocation; - if (currentSample != null) { - setCurrentSample(currentSample, playbackPositionMs, playWhenReady); - } + private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); + MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) + .setMetadata(movieMetadata).build(); + return new MediaQueueItem.Builder(mediaInfo).build(); } } diff --git a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml new file mode 100644 index 0000000000..5f3c8961ef --- /dev/null +++ b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 5d94931b64..1cce287b28 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -13,8 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + - + + + + + + + + + + diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index d277bb3cdf..3505c40400 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,4 +20,6 @@ Cast + Add samples + diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 92e36c7f2d..1f39fe0023 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -151,7 +151,8 @@ public final class CastPlayer implements Player { * * @param item The item to load. * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. + * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback + * starts at position 0. * @return The Cast {@code PendingResult}, or null if no session is available. */ public PendingResult loadItem(MediaQueueItem item, long positionMs) { @@ -164,13 +165,15 @@ public final class CastPlayer implements Player { * @param items The items to load. * @param startIndex The index of the item at which playback should start. * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. + * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback + * starts at position 0. * @param repeatMode The repeat mode for the created media queue. * @return The Cast {@code PendingResult}, or null if no session is available. */ public PendingResult loadItems(MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { + positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); @@ -327,6 +330,9 @@ public final class CastPlayer implements Player { @Override public void seekTo(int windowIndex, long positionMs) { MediaStatus mediaStatus = getMediaStatus(); + // We assume the default position is 0. There is no support for seeking to the default position + // in RemoteMediaClient. + positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; if (mediaStatus != null) { if (getCurrentWindowIndex() != windowIndex) { remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid, @@ -364,6 +370,7 @@ public final class CastPlayer implements Player { @Override public void stop(boolean reset) { + playbackState = STATE_IDLE; if (remoteMediaClient != null) { // TODO(b/69792021): Support or emulate stop without position reset. remoteMediaClient.stop(); @@ -450,14 +457,18 @@ public final class CastPlayer implements Player { @Override public int getNextWindowIndex() { - return C.INDEX_UNSET; + return currentTimeline.isEmpty() ? C.INDEX_UNSET + : currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false); } @Override public int getPreviousWindowIndex() { - return C.INDEX_UNSET; + return currentTimeline.isEmpty() ? C.INDEX_UNSET + : currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false); } + // TODO: Fill the cast timeline information with ProgressListener's duration updates. + // See [Internal: b/65152553]. @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET From 69f8b250d5c60470394f7a1e5787345ae411cd6b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 05:18:35 -0800 Subject: [PATCH 125/417] Match SequenceableLoader method order in HlsSampleStreamWrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177146923 --- .../source/hls/HlsSampleStreamWrapper.java | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) 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 06d48f1b08..87585a52da 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 @@ -324,28 +324,6 @@ import java.util.Arrays; return true; } - @Override - public long getBufferedPositionUs() { - if (loadingFinished) { - return C.TIME_END_OF_SOURCE; - } else if (isPendingReset()) { - return pendingResetPositionUs; - } else { - long bufferedPositionUs = lastSeekPositionUs; - HlsMediaChunk lastMediaChunk = getLastMediaChunk(); - HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk - : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; - if (lastCompletedMediaChunk != null) { - bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); - } - for (SampleQueue sampleQueue : sampleQueues) { - bufferedPositionUs = Math.max(bufferedPositionUs, - sampleQueue.getLargestQueuedTimestampUs()); - } - return bufferedPositionUs; - } - } - public void release() { boolean releasedSynchronously = loader.release(this); if (prepared && !releasedSynchronously) { @@ -447,6 +425,37 @@ import java.util.Arrays; // SequenceableLoader implementation + @Override + public long getBufferedPositionUs() { + if (loadingFinished) { + return C.TIME_END_OF_SOURCE; + } else if (isPendingReset()) { + return pendingResetPositionUs; + } else { + long bufferedPositionUs = lastSeekPositionUs; + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); + HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk + : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; + if (lastCompletedMediaChunk != null) { + bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); + } + for (SampleQueue sampleQueue : sampleQueues) { + bufferedPositionUs = Math.max(bufferedPositionUs, + sampleQueue.getLargestQueuedTimestampUs()); + } + return bufferedPositionUs; + } + } + + @Override + public long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; + } + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { @@ -494,15 +503,6 @@ import java.util.Arrays; return true; } - @Override - public long getNextLoadPositionUs() { - if (isPendingReset()) { - return pendingResetPositionUs; - } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; - } - } - // Loader.Callback implementation. @Override @@ -755,33 +755,10 @@ import java.util.Arrays; enabledSampleQueueCount = enabledSampleQueueCount + (enabledState ? 1 : -1); } - /** - * Derives a track format corresponding to a given container format, by combining it with sample - * level information obtained from the samples. - * - * @param containerFormat The container format for which the track format should be derived. - * @param sampleFormat A sample format from which to obtain sample level information. - * @return The derived track format. - */ - private static Format deriveFormat(Format containerFormat, Format sampleFormat) { - if (containerFormat == null) { - return sampleFormat; - } - int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); - return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, - containerFormat.width, containerFormat.height, containerFormat.selectionFlags, - containerFormat.language); - } - private HlsMediaChunk getLastMediaChunk() { return mediaChunks.get(mediaChunks.size() - 1); } - private boolean isMediaChunk(Chunk chunk) { - return chunk instanceof HlsMediaChunk; - } - private boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } @@ -810,4 +787,27 @@ import java.util.Arrays; return true; } + /** + * Derives a track format corresponding to a given container format, by combining it with sample + * level information obtained from the samples. + * + * @param containerFormat The container format for which the track format should be derived. + * @param sampleFormat A sample format from which to obtain sample level information. + * @return The derived track format. + */ + private static Format deriveFormat(Format containerFormat, Format sampleFormat) { + if (containerFormat == null) { + return sampleFormat; + } + int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); + String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); + return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, + containerFormat.width, containerFormat.height, containerFormat.selectionFlags, + containerFormat.language); + } + + private static boolean isMediaChunk(Chunk chunk) { + return chunk instanceof HlsMediaChunk; + } + } From f2d554175297c3f8ae6c42a25d20fad6d2db14cf Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 08:40:21 -0800 Subject: [PATCH 126/417] Extractor cleanup - Align class summary Javadoc - Fix ErrorProne + Style warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177165593 --- .../extractor/flv/FlvExtractor.java | 46 ++++++++++++------- .../extractor/mkv/MatroskaExtractor.java | 2 +- .../extractor/mp3/Mp3Extractor.java | 2 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 3 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../extractor/mp4/Mp4Extractor.java | 2 +- .../extractor/ogg/DefaultOggSeeker.java | 2 +- .../exoplayer2/extractor/ogg/FlacReader.java | 3 +- .../extractor/ogg/OggExtractor.java | 2 +- .../extractor/rawcc/RawCcExtractor.java | 2 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 2 +- .../extractor/ts/AdtsExtractor.java | 2 +- .../exoplayer2/extractor/ts/PsExtractor.java | 2 +- .../exoplayer2/extractor/ts/TsExtractor.java | 2 +- .../extractor/wav/WavExtractor.java | 4 +- 15 files changed, 47 insertions(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 218e6ffd82..30b66d65fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -25,9 +26,11 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** - * Facilitates the extraction of data from the FLV container format. + * Extracts data from the FLV container format. */ public final class FlvExtractor implements Extractor, SeekMap { @@ -43,16 +46,22 @@ public final class FlvExtractor implements Extractor, SeekMap { }; - // Header sizes. - private static final int FLV_HEADER_SIZE = 9; - private static final int FLV_TAG_HEADER_SIZE = 11; - - // Parser states. + /** + * Extractor states. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_FLV_HEADER, STATE_SKIPPING_TO_TAG_HEADER, STATE_READING_TAG_HEADER, + STATE_READING_TAG_DATA}) + private @interface States {} private static final int STATE_READING_FLV_HEADER = 1; private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; private static final int STATE_READING_TAG_HEADER = 3; private static final int STATE_READING_TAG_DATA = 4; + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + // Tag types. private static final int TAG_TYPE_AUDIO = 8; private static final int TAG_TYPE_VIDEO = 9; @@ -71,11 +80,11 @@ public final class FlvExtractor implements Extractor, SeekMap { private ExtractorOutput extractorOutput; // State variables. - private int parserState; + private @States int state; private int bytesToNextTagHeader; - public int tagType; - public int tagDataSize; - public long tagTimestampUs; + private int tagType; + private int tagDataSize; + private long tagTimestampUs; // Tags readers. private AudioTagPayloadReader audioReader; @@ -87,7 +96,7 @@ public final class FlvExtractor implements Extractor, SeekMap { headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; } @Override @@ -128,7 +137,7 @@ public final class FlvExtractor implements Extractor, SeekMap { @Override public void seek(long position, long timeUs) { - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; bytesToNextTagHeader = 0; } @@ -141,7 +150,7 @@ public final class FlvExtractor implements Extractor, SeekMap { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { while (true) { - switch (parserState) { + switch (state) { case STATE_READING_FLV_HEADER: if (!readFlvHeader(input)) { return RESULT_END_OF_INPUT; @@ -160,6 +169,9 @@ public final class FlvExtractor implements Extractor, SeekMap { return RESULT_CONTINUE; } break; + default: + // Never happens. + throw new IllegalStateException(); } } } @@ -199,7 +211,7 @@ public final class FlvExtractor implements Extractor, SeekMap { // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return true; } @@ -213,7 +225,7 @@ public final class FlvExtractor implements Extractor, SeekMap { private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { input.skipFully(bytesToNextTagHeader); bytesToNextTagHeader = 0; - parserState = STATE_READING_TAG_HEADER; + state = STATE_READING_TAG_HEADER; } /** @@ -236,7 +248,7 @@ public final class FlvExtractor implements Extractor, SeekMap { tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; tagHeaderBuffer.skipBytes(3); // streamId - parserState = STATE_READING_TAG_DATA; + state = STATE_READING_TAG_DATA; return true; } @@ -261,7 +273,7 @@ public final class FlvExtractor implements Extractor, SeekMap { wasConsumed = false; } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 5aefd041c4..4b0bbda275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -53,7 +53,7 @@ import java.util.Locale; import java.util.UUID; /** - * Extracts data from a Matroska or WebM file. + * Extracts data from the Matroska and WebM container formats. */ public final class MatroskaExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index a4349ada09..dc7d21851a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -38,7 +38,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Extracts data from an MP3 file. + * Extracts data from the MP3 container format. */ public final class Mp3Extractor implements Extractor { 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 5e8d72f18d..9b1158dfa8 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 @@ -113,12 +113,13 @@ import com.google.android.exoplayer2.util.Util; fx = 256f; } else { int a = (int) percent; - float fa, fb; + float fa; if (a == 0) { fa = 0f; } else { fa = tableOfContents[a - 1]; } + float fb; if (a < 99) { fb = tableOfContents[a]; } else { 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 e86157dd92..4bc1b04418 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 @@ -53,7 +53,7 @@ import java.util.Stack; import java.util.UUID; /** - * Facilitates the extraction of data from the fragmented mp4 container format. + * Extracts data from the FMP4 container format. */ public final class FragmentedMp4Extractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index f23af98e7f..f2412bf4ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -41,7 +41,7 @@ import java.util.List; import java.util.Stack; /** - * Extracts data from an unfragmented MP4 file. + * Extracts data from the MP4 container format. */ public final class Mp4Extractor implements Extractor, SeekMap { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 5470e2badc..77def57275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -186,7 +186,7 @@ import java.io.IOException; return start; } - long offset = pageSize * (granuleDistance <= 0 ? 2 : 1); + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); long nextPosition = input.getPosition() - offset + (granuleDistance * (end - start) / (endGranule - startGranule)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index f4da6e3960..304fb3dd96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -118,8 +118,9 @@ import java.util.List; case 14: case 15: return 256 << (blockSizeCode - 8); + default: + return -1; } - return -1; } private class FlacOggSeeker implements OggSeeker, SeekMap { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 54e168c665..a4d8f97d5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; /** - * Ogg {@link Extractor}. + * Extracts data from the Ogg container format. */ public class OggExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index 7840eafce6..aa77aba30e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts CEA data from a RawCC file. + * Extracts data from the RawCC container format. */ public final class RawCcExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 4d54600c6d..bc37277c57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from (E-)AC-3 bitstreams. + * Extracts data from (E-)AC-3 bitstreams. */ public final class Ac3Extractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 5ce15952a5..a0a748660e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from AAC bit streams with ADTS framing. + * Extracts data from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index 69c5745eaa..f3aad6ba6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 PS container format. + * Extracts data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { 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 213d30d47d..13e669da23 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 @@ -45,7 +45,7 @@ import java.util.Collections; import java.util.List; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Extracts data from the MPEG-2 TS container format. */ public final class TsExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index cb46aa5519..cb9a2653d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -28,7 +28,9 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; -/** {@link Extractor} to extract samples from a WAV byte stream. */ +/** + * Extracts data from WAV byte streams. + */ public final class WavExtractor implements Extractor, SeekMap { /** From 54a1bb186e860c172c473713eb24f1cc5d073d07 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 09:00:43 -0800 Subject: [PATCH 127/417] Allow resetInternal to release MediaSource but keep timeline. This allows to keep the state synced with ExoPlayerImpl after stopping the player, but still releases the media source immediately as it needs to be reprepared. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177167980 --- .../android/exoplayer2/ExoPlayerTest.java | 24 ++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 58 ++++++++++++------- .../exoplayer2/testutil/ActionSchedule.java | 9 +++ 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index efb7b0e96c..2443f8b892 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -784,4 +784,28 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertNoPositionDiscontinuities(); } + public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { + // Combining additional stop and seek after initial stop in one test to get the seek processed + // callback which ensures that all operations have been processed by the player. + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testStopTwice") + .waitForPlaybackState(Player.STATE_READY) + .stop(false) + .stop(false) + .seek(0) + .waitForSeekProcessed() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + } } 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 909f52fad8..3bd1d2b00f 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 @@ -390,11 +390,11 @@ import java.io.IOException; private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { pendingPrepareCount++; - resetInternal(/* releaseMediaSource= */ true, resetPosition); + resetInternal(/* releaseMediaSource= */ true, resetPosition, /* resetState= */ true); loadControl.onPrepared(); this.mediaSource = mediaSource; setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener= */ this); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -629,10 +629,15 @@ import java.io.IOException; private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { Timeline timeline = playbackInfo.timeline; - if (timeline == null) { + if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; - eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 0, 0, - playbackInfo.copyWithTimeline(Timeline.EMPTY, null)).sendToTarget(); + eventHandler + .obtainMessage( + MSG_SEEK_ACK, + /* seekAdjusted */ 0, + 0, + timeline == null ? playbackInfo.copyWithTimeline(Timeline.EMPTY, null) : playbackInfo) + .sendToTarget(); return; } @@ -642,10 +647,11 @@ import java.io.IOException; // timeline has changed and a suitable seek position could not be resolved in the new one. setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false, true); + resetInternal( + /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 1, 0, - playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs = */ 0, + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs= */ 0, /* contentPositionUs= */ C.TIME_UNSET)) .sendToTarget(); return; @@ -766,14 +772,15 @@ import java.io.IOException; } private void stopInternal(boolean reset) { - // Releasing the internal player sets the timeline to null. Use the current timeline or - // Timeline.EMPTY for notifying the eventHandler. - Timeline publicTimeline = reset || playbackInfo.timeline == null - ? Timeline.EMPTY : playbackInfo.timeline; - Object publicManifest = reset ? null : playbackInfo.manifest; - resetInternal(/* releaseMediaSource= */ true, reset); - PlaybackInfo publicPlaybackInfo = playbackInfo.copyWithTimeline(publicTimeline, publicManifest); - if (reset) { + resetInternal( + /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); + PlaybackInfo publicPlaybackInfo = playbackInfo; + if (playbackInfo.timeline == null) { + // Resetting the state sets the timeline to null. Use Timeline.EMPTY for notifying the + // eventHandler. + publicPlaybackInfo = publicPlaybackInfo.copyWithTimeline(Timeline.EMPTY, null); + } + if (playbackInfo.startPositionUs == C.TIME_UNSET) { // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for // notifying the eventHandler. publicPlaybackInfo = @@ -787,7 +794,8 @@ import java.io.IOException; } private void releaseInternal() { - resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ true); + resetInternal( + /* releaseMediaSource= */ true, /* resetPosition= */ true, /* resetState= */ true); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -797,7 +805,8 @@ import java.io.IOException; } } - private void resetInternal(boolean releaseMediaSource, boolean resetPosition) { + private void resetInternal( + boolean releaseMediaSource, boolean resetPosition, boolean resetState) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -832,13 +841,15 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, playbackInfo.contentPositionUs); } + if (resetState) { + mediaPeriodInfoSequence.setTimeline(null); + playbackInfo = playbackInfo.copyWithTimeline(null, null); + } if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); mediaSource = null; } - mediaPeriodInfoSequence.setTimeline(null); - playbackInfo = playbackInfo.copyWithTimeline(null, null); } } @@ -1174,7 +1185,8 @@ import java.io.IOException; private void handleSourceInfoRefreshEndedPlayback(int prepareAcks) { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false, true); + resetInternal( + /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). notifySourceInfoRefresh(prepareAcks, playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); @@ -1279,6 +1291,10 @@ import java.io.IOException; } private void updatePeriods() throws ExoPlaybackException, IOException { + if (mediaSource == null) { + // The player has no media source yet. + return; + } if (playbackInfo.timeline == null) { // We're waiting to get information about periods. mediaSource.maybeThrowSourceInfoRefreshError(); 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 abca2cafdb..7a2ce9270c 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 @@ -183,6 +183,15 @@ public final class ActionSchedule { .apply(new WaitForPlaybackState(tag, Player.STATE_READY)); } + /** + * Schedules a delay until the player indicates that a seek has been processed. + * + * @return The builder, for convenience. + */ + public Builder waitForSeekProcessed() { + return apply(new WaitForSeekProcessed(tag)); + } + /** * Schedules a playback parameters setting action to be executed. * From baa80a1b68329c8c6ea2e746b332dfae1a00a024 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 09:22:32 -0800 Subject: [PATCH 128/417] Fix DefaultTrackSelector#Parameter withSelectUndeterminedTextLanguage ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177170994 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 3 +-- .../exoplayer2/trackselection/DefaultTrackSelectorTest.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) 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 0029cdbd31..49b8e8964b 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 @@ -243,8 +243,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. */ - public Parameters withSelectUndeterminedTextLanguageAsFallback( - boolean selectUndeterminedTextLanguage) { + public Parameters withSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { return this; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index b2b149b004..6b14d139ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -561,8 +561,7 @@ public final class DefaultTrackSelectorTest { wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters( - DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.setParameters(DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); @@ -577,7 +576,7 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0)).isNull(); trackSelector.setParameters( - trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.getParameters().withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); From ff49bc97c459062a87310557197c72162dfa7dcb Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 09:43:08 -0800 Subject: [PATCH 129/417] Clean up some extrator SeekMap implementations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177173618 --- .../extractor/flv/FlvExtractor.java | 45 +++++------ .../extractor/flv/ScriptTagPayloadReader.java | 8 +- .../extractor/wav/WavExtractor.java | 21 +---- .../exoplayer2/extractor/wav/WavHeader.java | 76 ++++++++++++------- 4 files changed, 69 insertions(+), 81 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 30b66d65fd..2da075ff53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -32,7 +32,7 @@ import java.lang.annotation.RetentionPolicy; /** * Extracts data from the FLV container format. */ -public final class FlvExtractor implements Extractor, SeekMap { +public final class FlvExtractor implements Extractor { /** * Factory for {@link FlvExtractor} instances. @@ -70,32 +70,28 @@ public final class FlvExtractor implements Extractor, SeekMap { // FLV container identifier. private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); - // Temporary buffers. private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagData; + private final ScriptTagPayloadReader metadataReader; - // Extractor outputs. private ExtractorOutput extractorOutput; - - // State variables. private @States int state; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; private long tagTimestampUs; - - // Tags readers. + private boolean outputSeekMap; private AudioTagPayloadReader audioReader; private VideoTagPayloadReader videoReader; - private ScriptTagPayloadReader metadataReader; public FlvExtractor() { scratch = new ParsableByteArray(4); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); + metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; } @@ -203,11 +199,7 @@ public final class FlvExtractor implements Extractor, SeekMap { videoReader = new VideoTagPayloadReader( extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } - if (metadataReader == null) { - metadataReader = new ScriptTagPayloadReader(null); - } extractorOutput.endTracks(); - extractorOutput.seekMap(this); // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; @@ -263,11 +255,18 @@ public final class FlvExtractor implements Extractor, SeekMap { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { + ensureOutputSeekMap(); audioReader.consume(prepareTagData(input), tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { + ensureOutputSeekMap(); videoReader.consume(prepareTagData(input), tagTimestampUs); - } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { + } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); + long durationUs = metadataReader.getDurationUs(); + if (durationUs != C.TIME_UNSET) { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + outputSeekMap = true; + } } else { input.skipFully(tagDataSize); wasConsumed = false; @@ -289,21 +288,11 @@ public final class FlvExtractor implements Extractor, SeekMap { return tagData; } - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return false; - } - - @Override - public long getDurationUs() { - return metadataReader.getDurationUs(); - } - - @Override - public long getPosition(long timeUs) { - return 0; + private void ensureOutputSeekMap() { + if (!outputSeekMap) { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + outputSeekMap = true; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 1a4f8f3e88..2dec85ffcc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,11 +43,8 @@ import java.util.Map; private long durationUs; - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public ScriptTagPayloadReader(TrackOutput output) { - super(output); + public ScriptTagPayloadReader() { + super(null); durationUs = C.TIME_UNSET; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index cb9a2653d7..4f2be71a69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; @@ -31,7 +30,7 @@ import java.io.IOException; /** * Extracts data from WAV byte streams. */ -public final class WavExtractor implements Extractor, SeekMap { +public final class WavExtractor implements Extractor { /** * Factory for {@link WavExtractor} instances. @@ -95,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(this); + extractorOutput.seekMap(wavHeader); } int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); @@ -115,20 +114,4 @@ public final class WavExtractor implements Extractor, SeekMap { return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } - // SeekMap implementation. - - @Override - public long getDurationUs() { - return wavHeader.getDurationUs(); - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - return wavHeader.getPosition(timeUs); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index a57060f604..1c1fc97a22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -16,9 +16,10 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; /** Header for a WAV file. */ -/*package*/ final class WavHeader { +/* package */ final class WavHeader implements SeekMap { /** Number of audio chanels. */ private final int numChannels; @@ -49,12 +50,56 @@ import com.google.android.exoplayer2.C; this.encoding = encoding; } - /** Returns the duration in microseconds of this WAV. */ + // Setting bounds. + + /** + * Sets the data start position and size in bytes of sample data in this WAV. + * + * @param dataStartPosition The data start position in bytes. + * @param dataSize The data size in bytes. + */ + public void setDataBounds(long dataStartPosition, long dataSize) { + this.dataStartPosition = dataStartPosition; + this.dataSize = dataSize; + } + + /** Returns whether the data start position and size have been set. */ + public boolean hasDataBounds() { + return dataStartPosition != 0 && dataSize != 0; + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override public long getDurationUs() { long numFrames = dataSize / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } + @Override + public long getPosition(long timeUs) { + long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Round down to nearest frame. + long position = (unroundedPosition / blockAlignment) * blockAlignment; + return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + } + + // Misc getters. + + /** + * Returns the time in microseconds for the given position in bytes. + * + * @param position The position in bytes. + */ + public long getTimeUs(long position) { + return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + } + /** Returns the bytes per frame of this WAV. */ public int getBytesPerFrame() { return blockAlignment; @@ -75,33 +120,8 @@ import com.google.android.exoplayer2.C; return numChannels; } - /** Returns the position in bytes in this WAV for the given time in microseconds. */ - public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; - } - - /** Returns the time in microseconds for the given position in bytes in this WAV. */ - public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; - } - - /** Returns true if the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; - } - - /** Sets the start position and size in bytes of sample data in this WAV. */ - public void setDataBounds(long dataStartPosition, long dataSize) { - this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; - } - /** Returns the PCM encoding. **/ - @C.PcmEncoding - public int getEncoding() { + public @C.PcmEncoding int getEncoding() { return encoding; } From 2282527821a0232e5d80f554d417828e0f7cf771 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Nov 2017 09:53:30 -0800 Subject: [PATCH 130/417] Allow setting supported formats on AdsLoaders ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177175377 --- .../android/exoplayer2/castdemo/DemoUtil.java | 6 ++--- .../exoplayer2/ext/ima/ImaAdsLoader.java | 24 +++++++++++++++++++ .../exoplayer2/source/ads/AdsLoader.java | 10 ++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 1 + .../android/exoplayer2/util/MimeTypes.java | 3 +++ .../source/dash/DashMediaSource.java | 3 +-- 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index d36f8c319e..26ab5eb0dd 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -26,9 +26,9 @@ import java.util.List; */ /* package */ final class DemoUtil { - public static final String MIME_TYPE_DASH = "application/dash+xml"; - public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl"; - public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml"; + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; + public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; + public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; /** 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 fe6a6d6196..4bf88fe18f 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 @@ -49,10 +49,13 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -117,6 +120,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private List supportedMimeTypes; private EventListener eventListener; private Player player; private ViewGroup adUiViewGroup; @@ -238,6 +242,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // AdsLoader implementation. + @Override + public void setSupportedContentTypes(@C.ContentType int... contentTypes) { + List supportedMimeTypes = new ArrayList<>(); + for (@C.ContentType int contentType : contentTypes) { + if (contentType == C.TYPE_DASH) { + supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); + } else if (contentType == C.TYPE_HLS) { + supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); + } else if (contentType == C.TYPE_OTHER) { + supportedMimeTypes.addAll(Arrays.asList( + MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG, + MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); + } else if (contentType == C.TYPE_SS) { + // IMA does not support SmoothStreaming ad media. + } + } + this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); + } + @Override public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; @@ -296,6 +319,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(true); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with preloading"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 241750a21f..99feccd2f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.ads; import android.view.ViewGroup; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import java.io.IOException; @@ -71,6 +72,15 @@ public interface AdsLoader { } + /** + * Sets the supported content types for ad media. Must be called before the first call to + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored. + * + * @param contentTypes The supported content types for ad media. Each element must be one of + * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}. + */ + void setSupportedContentTypes(@C.ContentType int... contentTypes); + /** * Attaches a player that will play ads loaded using this instance. Called on the main thread by * {@link AdsMediaSource}. 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 397b8effd3..202e31cba1 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 @@ -132,6 +132,7 @@ public final class AdsMediaSource implements MediaSource { period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; + adsLoader.setSupportedContentTypes(C.TYPE_OTHER); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index a68e0142d6..8307e998a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -36,6 +36,7 @@ public final class MimeTypes { public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @@ -70,7 +71,9 @@ public final class MimeTypes { public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; 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 e2143b4bf5..2562b27237 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 @@ -905,8 +905,7 @@ public final class DashMediaSource implements MediaSource { } - private final class ManifestCallback implements - Loader.Callback> { + private final class ManifestCallback implements Loader.Callback> { @Override public void onLoadCompleted(ParsingLoadable loadable, From 9e8f50a9c06e4e7cbae6b31eca9b3acee761f7d0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 10:45:38 -0800 Subject: [PATCH 131/417] Allow multiple video and audio debug listeners in SimpleExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177184331 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../android/exoplayer2/SimpleExoPlayer.java | 82 +++++++++++++++---- .../exoplayer2/testutil/ExoHostedTest.java | 32 +++++++- 3 files changed, 97 insertions(+), 21 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 efde775176..cf0f8b8dc8 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 @@ -298,8 +298,8 @@ public class PlayerActivity extends Activity implements OnClickListener, player.addListener(new PlayerEventListener()); player.addListener(eventLogger); player.addMetadataOutput(eventLogger); - player.setAudioDebugListener(eventLogger); - player.setVideoDebugListener(eventLogger); + player.addAudioDebugListener(eventLogger); + player.addVideoDebugListener(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index a153e4ed43..909a5d0fd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet videoDebugListeners; + private final CopyOnWriteArraySet audioDebugListeners; private final int videoRendererCount; private final int audioRendererCount; @@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private AudioRendererEventListener audioDebugListener; - private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; @@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer { videoListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -579,18 +581,64 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive debug events from the video renderer. * * @param listener The listener. + * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}. */ + @Deprecated public void setVideoDebugListener(VideoRendererEventListener listener) { - videoDebugListener = listener; + videoDebugListeners.clear(); + if (listener != null) { + addVideoDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void addVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void removeVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.remove(listener); } /** * Sets a listener to receive debug events from the audio renderer. * * @param listener The listener. + * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}. */ + @Deprecated public void setAudioDebugListener(AudioRendererEventListener listener) { - audioDebugListener = listener; + audioDebugListeners.clear(); + if (listener != null) { + addAudioDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void addAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void removeAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.remove(listener); } // ExoPlayer implementation @@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoEnabled(DecoderCounters counters) { videoDecoderCounters = counters; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoEnabled(counters); } } @@ -893,7 +941,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -902,14 +950,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoInputFormatChanged(Format format) { videoFormat = format; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoInputFormatChanged(format); } } @Override public void onDroppedFrames(int count, long elapsed) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onDroppedFrames(count, elapsed); } } @@ -921,7 +969,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -934,14 +982,14 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onRenderedFirstFrame(); } } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onRenderedFirstFrame(surface); } } @Override public void onVideoDisabled(DecoderCounters counters) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDisabled(counters); } videoFormat = null; @@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioEnabled(DecoderCounters counters) { audioDecoderCounters = counters; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioEnabled(counters); } } @@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSessionId(int sessionId) { audioSessionId = sessionId; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } } @@ -969,7 +1017,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -978,7 +1026,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioInputFormatChanged(Format format) { audioFormat = format; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioInputFormatChanged(format); } } @@ -986,14 +1034,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } @Override public void onAudioDisabled(DecoderCounters counters) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDisabled(counters); } audioFormat = null; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ab31238983..ab63087f95 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -78,6 +78,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private Surface surface; private ExoPlaybackException playerError; private Player.EventListener playerEventListener; + private VideoRendererEventListener videoDebugListener; + private AudioRendererEventListener audioDebugListener; private boolean playerWasPrepared; private boolean playing; @@ -140,6 +142,26 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen } } + /** + * Sets an {@link VideoRendererEventListener} to listen for video debug events during the test. + */ + public final void setVideoDebugListener(VideoRendererEventListener videoDebugListener) { + this.videoDebugListener = videoDebugListener; + if (player != null) { + player.addVideoDebugListener(videoDebugListener); + } + } + + /** + * Sets an {@link AudioRendererEventListener} to listen for audio debug events during the test. + */ + public final void setAudioDebugListener(AudioRendererEventListener audioDebugListener) { + this.audioDebugListener = audioDebugListener; + if (player != null) { + player.addAudioDebugListener(audioDebugListener); + } + } + // HostedTest implementation @Override @@ -155,9 +177,15 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen if (playerEventListener != null) { player.addListener(playerEventListener); } + if (videoDebugListener != null) { + player.addVideoDebugListener(videoDebugListener); + } + if (audioDebugListener != null) { + player.addAudioDebugListener(audioDebugListener); + } player.addListener(this); - player.setAudioDebugListener(this); - player.setVideoDebugListener(this); + player.addAudioDebugListener(this); + player.addVideoDebugListener(this); player.setPlayWhenReady(true); actionHandler = new Handler(); // Schedule any pending actions. From 835b6382acf17835798883a518376a5a2c382407 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 29 Nov 2017 02:07:44 -0800 Subject: [PATCH 132/417] Move external timeline and start position overwrites to ExoPlayerImpl. Makes it less error-prone to accidentatly forget to set the right overwrites. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177282089 --- .../android/exoplayer2/ExoPlayerImpl.java | 11 ++++++- .../exoplayer2/ExoPlayerImplInternal.java | 30 ++++--------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 37fccafd08..ee96cb0c47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -502,10 +502,19 @@ import java.util.concurrent.CopyOnWriteArraySet; private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { - Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareOrStopAcks -= prepareOrStopAcks; pendingSeekAcks -= seekAcks; if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { + if (playbackInfo.timeline == null) { + // Replace internal null timeline with externally visible empty timeline. + playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest); + } + if (playbackInfo.startPositionUs == C.TIME_UNSET) { + // Replace internal unset start position with externally visible start position of zero. + playbackInfo = + playbackInfo.fromNewPosition( + playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); + } boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; 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 3bd1d2b00f..b0ef675e71 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 @@ -632,11 +632,7 @@ import java.io.IOException; if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; eventHandler - .obtainMessage( - MSG_SEEK_ACK, - /* seekAdjusted */ 0, - 0, - timeline == null ? playbackInfo.copyWithTimeline(Timeline.EMPTY, null) : playbackInfo) + .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 0, 0, playbackInfo) .sendToTarget(); return; } @@ -649,10 +645,8 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, - playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs= */ 0, - /* contentPositionUs= */ C.TIME_UNSET)) + eventHandler + .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, playbackInfo) .sendToTarget(); return; } @@ -774,21 +768,9 @@ import java.io.IOException; private void stopInternal(boolean reset) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - PlaybackInfo publicPlaybackInfo = playbackInfo; - if (playbackInfo.timeline == null) { - // Resetting the state sets the timeline to null. Use Timeline.EMPTY for notifying the - // eventHandler. - publicPlaybackInfo = publicPlaybackInfo.copyWithTimeline(Timeline.EMPTY, null); - } - if (playbackInfo.startPositionUs == C.TIME_UNSET) { - // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for - // notifying the eventHandler. - publicPlaybackInfo = - publicPlaybackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET); - } int prepareOrStopAcks = pendingPrepareCount + 1; pendingPrepareCount = 0; - notifySourceInfoRefresh(prepareOrStopAcks, publicPlaybackInfo); + notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1187,9 +1169,7 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - notifySourceInfoRefresh(prepareAcks, - playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); + notifySourceInfoRefresh(prepareAcks, playbackInfo); } private void notifySourceInfoRefresh() { From 21ea9a821df397df9e52bfb61f18829c1c7333e9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 08:53:27 -0800 Subject: [PATCH 133/417] Fix weird XingSeeker indexing There are still things broken about the seeker, but this cleans up some of the weird bits. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177315136 --- .../exoplayer2/extractor/mp3/XingSeeker.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) 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 9b1158dfa8..55888066e7 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 @@ -58,9 +58,8 @@ import com.google.android.exoplayer2.util.Util; } long sizeBytes = frame.readUnsignedIntToInt(); - frame.skipBytes(1); - long[] tableOfContents = new long[99]; - for (int i = 0; i < 99; i++) { + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); } @@ -105,30 +104,20 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable()) { return firstFramePosition; } - float percent = timeUs * 100f / durationUs; - float fx; - if (percent <= 0f) { - fx = 0f; - } else if (percent >= 100f) { - fx = 256f; + double percent = (timeUs * 100d) / durationUs; + double fx; + if (percent <= 0) { + fx = 0; + } else if (percent >= 100) { + fx = 256; } else { int a = (int) percent; - float fa; - if (a == 0) { - fa = 0f; - } else { - fa = tableOfContents[a - 1]; - } - float fb; - if (a < 99) { - fb = tableOfContents[a]; - } else { - fb = 256f; - } + float fa = tableOfContents[a]; + float fb = a == 99 ? 256 : tableOfContents[a + 1]; fx = fa + (fb - fa) * (percent - a); } - long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; + long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 : firstFramePosition - headerSize + sizeBytes - 1; return Math.min(position, maximumPosition); @@ -139,14 +128,14 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable() || position < firstFramePosition) { return 0L; } - double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; + double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; + Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); long previousTime = getTimeUsForTocPosition(previousTocPosition); // Linearly interpolate the time taking into account the next entry. - long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; + long previousByte = tableOfContents[previousTocPosition]; + long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte)); @@ -163,7 +152,7 @@ import com.google.android.exoplayer2.util.Util; * interpreted as a percentage of the stream's duration between 0 and 100. */ private long getTimeUsForTocPosition(int tocPosition) { - return durationUs * tocPosition / 100; + return (durationUs * tocPosition) / 100; } } From a99ef01d3a01dc9619fe2b6751cb44a107c58c33 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 09:09:17 -0800 Subject: [PATCH 134/417] Optimize seeking for unseekable SeekMaps - Avoid re-downloading data prior to the first mdat box when seeking back to the start of an unseekable FMP4. - Avoid re-downloading data prior to the first frame for constant bitrate MP3. - Update SeekMap.getPosition documentation to allow a non-zero position for the unseekable case. Note that XingSeeker was already returning a non-zero position if unseekable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177317256 --- .../android/exoplayer2/extractor/SeekMap.java | 16 ++++++++++++++-- .../extractor/mp3/ConstantBitrateSeeker.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 3 ++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 778aa4d715..964c43a45a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -28,13 +28,24 @@ public interface SeekMap { final class Unseekable implements SeekMap { private final long durationUs; + private final long startPosition; /** * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if * the duration is unknown. */ public Unseekable(long durationUs) { + this(durationUs, 0); + } + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if + * the duration is unknown. + * @param startPosition The position (byte offset) of the start of the media. + */ + public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; + this.startPosition = startPosition; } @Override @@ -49,7 +60,7 @@ public interface SeekMap { @Override public long getPosition(long timeUs) { - return 0; + return startPosition; } } @@ -78,7 +89,8 @@ public interface SeekMap { * * @param timeUs A seek position in microseconds. * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor, or 0 if {@code #isSeekable()} returns false. + * to the extractor. If {@link #isSeekable()} returns false then the returned value will be + * independent of {@code timeUs}, and will indicate the start of the media in the stream. */ long getPosition(long timeUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index df7748a910..47e12161a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (durationUs == C.TIME_UNSET) { - return 0; + return firstFramePosition; } timeUs = Util.constrainValue(timeUs, 0, durationUs); return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); 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 4bc1b04418..28a1ffaa7b 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 @@ -345,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor { currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { - extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); haveOutputSeekMap = true; } parserState = STATE_READING_ENCRYPTION_DATA; From 3afdb24f25d7a224e5963384d0955e3393548527 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 29 Nov 2017 09:23:57 -0800 Subject: [PATCH 135/417] Add support for outputing Emsg messages to a sideloaded track output. Currently FragmentedMp4Extractor only parses and outputs emsg messages if the flag FLAG_ENABLE_EMSG_TRACK is set (when there's a metadata renderer that handles emsg messages). Since there are emsg messages that only targets the player, which we want to handle independently from MetadateRenderer, this CL adds the ability for FragmentedMp4Extractor to output emsg messages to an additional TrackOutput if provided, independently from FLAG_ENABLED_EMSG_TRACK. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177318983 --- .../extractor/mp4/FragmentedMp4Extractor.java | 98 ++++++++++++++----- 1 file changed, 73 insertions(+), 25 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 28a1ffaa7b..9a70dfbf90 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -44,10 +45,10 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Stack; import java.util.UUID; @@ -108,6 +109,8 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); // Parser states. private static final int STATE_READING_ATOM_HEADER = 0; @@ -141,7 +144,8 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray atomHeader; private final byte[] extendedTypeScratch; private final Stack containerAtoms; - private final LinkedList pendingMetadataSampleInfos; + private final ArrayDeque pendingMetadataSampleInfos; + private final @Nullable TrackOutput additionalEmsgTrackOutput; private int parserState; private int atomType; @@ -161,7 +165,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; - private TrackOutput eventMessageTrackOutput; + private TrackOutput[] emsgTrackOutputs; private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. @@ -212,11 +216,32 @@ public final class FragmentedMp4Extractor implements Extractor { */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { + this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, + closedCaptionFormats, null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages + * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special + * handling of emsg messages for players is not required. + */ + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats, + @Nullable TrackOutput additionalEmsgTrackOutput) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); + this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -225,7 +250,7 @@ public final class FragmentedMp4Extractor implements Extractor { defaultInitializationVector = new ParsableByteArray(); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); - pendingMetadataSampleInfos = new LinkedList<>(); + pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; @@ -494,10 +519,21 @@ public final class FragmentedMp4Extractor implements Extractor { } private void maybeInitExtraTracks() { - if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { - eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); - eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, - Format.OFFSET_SAMPLE_RELATIVE)); + if (emsgTrackOutputs == null) { + emsgTrackOutputs = new TrackOutput[2]; + int emsgTrackOutputCount = 0; + if (additionalEmsgTrackOutput != null) { + emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput; + } + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) { + emsgTrackOutputs[emsgTrackOutputCount++] = + extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + } + emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); + + for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { + eventMessageTrackOutput.format(EMSG_FORMAT); + } } if (cea608TrackOutputs == null) { cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; @@ -510,29 +546,34 @@ public final class FragmentedMp4Extractor implements Extractor { } /** - * Handles an emsg atom (defined in 23009-1). + * Parses an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { - if (eventMessageTrackOutput == null) { + if (emsgTrackOutputs.length == 0) { return; } - // Parse the event's presentation time delta. + atom.setPosition(Atom.FULL_HEADER_SIZE); + int sampleSize = atom.bytesLeft(); atom.readNullTerminatedString(); // schemeIdUri atom.readNullTerminatedString(); // value long timescale = atom.readUnsignedInt(); long presentationTimeDeltaUs = Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + // Output the sample data. - atom.setPosition(Atom.FULL_HEADER_SIZE); - int sampleSize = atom.bytesLeft(); - eventMessageTrackOutput.sampleData(atom, sampleSize); + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + atom.setPosition(Atom.FULL_HEADER_SIZE); + emsgTrackOutput.sampleData(atom, sampleSize); + } + // Output the sample metadata. if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { - // We can output the sample metadata immediately. - eventMessageTrackOutput.sampleMetadata( - segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, + C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); + } } else { // We need the first sample timestamp in the segment before we can output the metadata. pendingMetadataSampleInfos.addLast( @@ -1194,13 +1235,8 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); - while (!pendingMetadataSampleInfos.isEmpty()) { - MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); - pendingMetadataSampleBytes -= sampleInfo.size; - eventMessageTrackOutput.sampleMetadata( - sampleTimeUs + sampleInfo.presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); - } + // After we have the sampleTimeUs, we can commit all the pending metadata samples + outputPendingMetadataSamples(sampleTimeUs); currentTrackBundle.currentSampleIndex++; currentTrackBundle.currentSampleInTrackRun++; @@ -1214,6 +1250,18 @@ public final class FragmentedMp4Extractor implements Extractor { return true; } + private void outputPendingMetadataSamples(long sampleTimeUs) { + while (!pendingMetadataSampleInfos.isEmpty()) { + MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= sampleInfo.size; + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + sampleTimeUs + sampleInfo.presentationTimeDeltaUs, + C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); + } + } + } + /** * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those * yet to be consumed, or null if all have been consumed. From 079a5b3d8cfce739971ba08dd13ed351020c77ad Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Wed, 29 Nov 2017 16:59:41 -0800 Subject: [PATCH 136/417] Add manifestless captions support. This code fits into the pre-existing captions fetcher architecture. 1. ManifestlessCaptionsMetadata Other captions fetchers must first fetch a manifest (HLS or manifest) to discover captions tracks. This process does not exist for manifestless. All we need to do is scan the FormatStream's for the right itag, so this is an all-static class. 2. ManifestlessSubtitleWindowProvider Once a captions track is selected, a subtitles provider is instantiated. This is the main interface used by the player to retrieve captions according to playback position. This class stores fetched captions in a tree index by time for efficient lookups. Background captions fetches are used to populate the tree. 3. ManifestlessCaptionsFetch Captions are fetched one segment at a time. One instance of this object is required per fetch. It performs a blocking fetch on call(), and is intended to be submitted to a background-thread executor. 4. ManifestlessCaptionsFetch.CaptionSegment This is the result of the caption fetch. These values are used to populate the captions tree. Manifestlessness The initial request is always a headm request. There is a separate tree of every segment indexed by start time. This tree is used to improve manifestless sequence number calculation. Once we have data for the current timestamp, we walk forward through the tree to find the next unfetched sequence number, and fetch that. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177385094 --- .../google/android/exoplayer2/text/webvtt/WebvttCssStyle.java | 4 +++- .../com/google/android/exoplayer2/text/webvtt/WebvttCue.java | 4 ++-- .../android/exoplayer2/text/webvtt/WebvttCueParser.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 10c17e2888..a78c5afa78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -31,10 +31,11 @@ import java.util.List; * @see W3C specification - Apply * CSS properties */ -/* package */ final class WebvttCssStyle { +public final class WebvttCssStyle { public static final int UNSPECIFIED = -1; + /** Style flag enum */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) @@ -44,6 +45,7 @@ import java.util.List; public static final int STYLE_ITALIC = Typeface.ITALIC; public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + /** Font size unit enum */ @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) public @interface FontSizeUnit {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index 295fdc656f..e16b231f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.text.Cue; /** * A representation of a WebVTT cue. */ -/* package */ final class WebvttCue extends Cue { +public final class WebvttCue extends Cue { public final long startTime; public final long endTime; @@ -59,7 +59,7 @@ import com.google.android.exoplayer2.text.Cue; * Builder for WebVTT cues. */ @SuppressWarnings("hiding") - public static final class Builder { + public static class Builder { private static final String TAG = "WebvttCueBuilder"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 54af4dbf63..80ebecdc0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -45,7 +45,7 @@ import java.util.regex.Pattern; /** * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ -/* package */ final class WebvttCueParser { +public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); @@ -90,7 +90,7 @@ import java.util.regex.Pattern; * @param styles List of styles defined by the CSS style blocks preceeding the cues. * @return Whether a valid Cue was found. */ - /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, + public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); if (firstLine == null) { From 882d698d5f64d3cf735f97deec09cd124ed50572 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 00:33:10 -0800 Subject: [PATCH 137/417] Log load errors from AdsMediaSource in the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177419981 --- .../android/exoplayer2/demo/EventLogger.java | 21 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 3 ++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 473a0d3441..68a10343e6 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -57,7 +58,8 @@ import java.util.Locale; */ /* package */ final class EventLogger implements Player.EventListener, MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { + ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -371,6 +373,23 @@ import java.util.Locale; // Do nothing. } + // AdsMediaSource.EventListener + + @Override + public void onAdLoadError(IOException error) { + printInternalError("loadError", error); + } + + @Override + public void onAdClicked() { + // Do nothing. + } + + @Override + public void onAdTapped() { + // Do nothing. + } + // Internal methods private void printInternalError(String type, Exception e) { 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 cf0f8b8dc8..7d0975a750 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 @@ -471,7 +471,8 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup); + return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, + mainHandler, eventLogger); } private void releaseAdsLoader() { From 5865f1fe408dcd6df1be6cd72ce88cc7a7b6f7f1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:25:28 -0800 Subject: [PATCH 138/417] Use a MediaSource factory internally in AdsMediaSource Support ad MediaSources that aren't prepared immediately by using DeferredMediaPeriod, moved up from DynamicConcatenatingMediaSource. In a later change the new interfaces will be made public so that apps can provide their own MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424172 --- .../source/DeferredMediaPeriod.java | 139 +++++++++++++++++ .../DynamicConcatenatingMediaSource.java | 107 ------------- .../exoplayer2/source/ads/AdsMediaSource.java | 143 +++++++++++++++--- 3 files changed, 259 insertions(+), 130 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java new file mode 100644 index 0000000000..bc29b2fdf1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 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; + +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; + +/** + * Media period that wraps a media source and defers calling its + * {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()} + * has been called. This is useful if you need to return a media period immediately but the media + * source that should create it is not yet prepared. + */ +public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + /** + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then + * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} + * to release the period. + */ + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + /** + * Releases the period. + */ + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + // MediaPeriod.Callback implementation + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 6b5c8b2637..c410456e7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -758,111 +757,5 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } - /** - * Media period used for periods created from unprepared media sources exposed through - * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes - * available. - */ - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - public final MediaSource mediaSource; - - private final MediaPeriodId id; - private final Allocator allocator; - - private MediaPeriod mediaPeriod; - private Callback callback; - private long preparePositionUs; - - public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { - this.id = id; - this.allocator = allocator; - this.mediaSource = mediaSource; - } - - public void createPeriod() { - mediaPeriod = mediaSource.createPeriod(id, allocator); - if (callback != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - public void releasePeriod() { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - } - } - - @Override - public void prepare(Callback callback, long preparePositionUs) { - this.callback = callback; - this.preparePositionUs = preparePositionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } else { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); - } - } - } 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 202e31cba1..47a2540c38 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.ads; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; @@ -23,15 +24,19 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -68,12 +73,12 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; private final Handler mainHandler; private final ComponentListener componentListener; - private final Map adMediaSourceByMediaPeriod; + private final AdMediaSourceFactory adMediaSourceFactory; + private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @Nullable private final Handler eventHandler; @@ -95,6 +100,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

      + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -109,6 +117,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

      + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -121,18 +132,18 @@ public final class AdsMediaSource implements MediaSource { AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable AdsListener eventListener) { this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); + adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - adsLoader.setSupportedContentTypes(C.TYPE_OTHER); + adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @Override @@ -173,10 +184,9 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource.Builder( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) - .setEventListener(mainHandler, componentListener) - .build(); + Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; + final MediaSource adMediaSource = + adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -186,30 +196,37 @@ public final class AdsMediaSource implements MediaSource { Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { + deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList()); + adMediaSource.prepareSource(player, false, new MediaSource.Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + @Nullable Object manifest) { + onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline); } }); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + if (mediaPeriods == null) { + deferredMediaPeriod.createPeriod(); + } else { + // Keep track of the deferred media period so it can be populated with the real media period + // when the source's info becomes available. + mediaPeriods.add(deferredMediaPeriod); + } + return deferredMediaPeriod; } else { - return contentMediaSource.createPeriod(id, allocator); + DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + mediaPeriod.createPeriod(); + return mediaPeriod; } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); } @Override @@ -264,9 +281,17 @@ public final class AdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, + int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) { + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + for (int i = 0; i < mediaPeriods.size(); i++) { + mediaPeriods.get(i).createPeriod(); + } + deferredMediaPeriodByAdMediaSource.remove(mediaSource); + } maybeUpdateSourceInfo(); } @@ -285,7 +310,7 @@ public final class AdsMediaSource implements MediaSource { * Listener for component events. All methods are called on the main thread. */ private final class ComponentListener implements AdsLoader.EventListener, - ExtractorMediaSource.EventListener { + AdMediaSourceLoadErrorListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -349,4 +374,76 @@ public final class AdsMediaSource implements MediaSource { } + /** + * Listener for errors while loading an ad {@link MediaSource}. + */ + private interface AdMediaSourceLoadErrorListener { + + /** + * Called when an error occurs loading media data. + * + * @param error The load error. + */ + void onLoadError(IOException error); + + } + + /** + * Factory for {@link MediaSource}s for loading ad media. + */ + private interface AdMediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the ad. + * @param handler A handler for listener events. + * @param listener A listener for ad load errors. To have ad media source load errors notified + * via the ads media source's listener, call this listener's onLoadError method from your + * new media source's load error listener using the specified {@code handler}. Otherwise, + * this parameter can be ignored. + * @return The new media source. + */ + MediaSource createAdMediaSource(Uri uri, Handler handler, + AdMediaSourceLoadErrorListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or + * {@link C#TYPE_OTHER}. + * + * @return The content types supported by the factory. + */ + int[] getSupportedTypes(); + + } + + private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public MediaSource createAdMediaSource(Uri uri, Handler handler, + final AdMediaSourceLoadErrorListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, + new EventListener() { + @Override + public void onLoadError(IOException error) { + listener.onLoadError(error); + } + }).build(); + } + + @Override + public int[] getSupportedTypes() { + // Only ExtractorMediaSource is supported. + return new int[] {C.TYPE_OTHER}; + } + + } + } From a9c33590dfa0297a3e281c0d17bacf2d2032b158 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:27:07 -0800 Subject: [PATCH 139/417] Update getPosition(0) positions for FragmentedMp4Extractor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424314 --- .../src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump | 2 +- .../src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index bf822d9db4..95f6528fd6 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index 9d3755b23b..ebd33133e2 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 3 track 0: format: From a367ae0d2bbde0dc9feea9a4295070f653b788fb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 02:57:07 -0800 Subject: [PATCH 140/417] Add a notice that NDK <= version 15c is required for VP9 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177430827 --- extensions/vp9/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 941b413c09..649e4a6ee2 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` -* Download the [Android NDK][] and set its location in an environment variable: +* Download the [Android NDK][] and set its location in an environment variable. +Only versions up to NDK 15c are supported currently (see [#3520][]). ``` NDK_PATH="" @@ -70,6 +71,7 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#3520]: https://github.com/google/ExoPlayer/issues/3520 ## Notes ## From ef8fa28163e6a72d4519c21542484be7785cff06 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Nov 2017 03:39:17 -0800 Subject: [PATCH 141/417] Use VideoRendererEventListener to resolve unknown resolution. Some streams don't have the new video resolution in the primary format. Use the subsequent call to videoListener.onVideoInputFormatChanged to resolve this unknown resolution. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177433618 --- .../testutil/ExoPlayerTestRunner.java | 100 +++++++++++++----- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 62e950091b..fddeb60bf0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -102,6 +102,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private RenderersFactory renderersFactory; private ActionSchedule actionSchedule; private Player.EventListener eventListener; + private VideoRendererEventListener videoRendererEventListener; + private AudioRendererEventListener audioRendererEventListener; private Integer expectedPlayerEndedCount; /** @@ -258,6 +260,28 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } + /** + * Sets a {@link VideoRendererEventListener} to be registered. + * + * @param eventListener A {@link VideoRendererEventListener} to be registered. + * @return This builder. + */ + public Builder setVideoRendererEventListener(VideoRendererEventListener eventListener) { + this.videoRendererEventListener = eventListener; + return this; + } + + /** + * Sets an {@link AudioRendererEventListener} to be registered. + * + * @param eventListener An {@link AudioRendererEventListener} to be registered. + * @return This builder. + */ + public Builder setAudioRendererEventListener(AudioRendererEventListener eventListener) { + this.audioRendererEventListener = eventListener; + return this; + } + /** * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED} * or {@link Player#STATE_IDLE}. The default is 1. This affects how long @@ -319,8 +343,17 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; } - return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector, - loadControl, actionSchedule, eventListener, expectedPlayerEndedCount); + return new ExoPlayerTestRunner( + playerFactory, + mediaSource, + renderersFactory, + trackSelector, + loadControl, + actionSchedule, + eventListener, + videoRendererEventListener, + audioRendererEventListener, + expectedPlayerEndedCount); } } @@ -331,6 +364,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private final LoadControl loadControl; private final @Nullable ActionSchedule actionSchedule; private final @Nullable Player.EventListener eventListener; + private final @Nullable VideoRendererEventListener videoRendererEventListener; + private final @Nullable AudioRendererEventListener audioRendererEventListener; private final HandlerThread playerThread; private final Handler handler; @@ -347,10 +382,17 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private TrackGroupArray trackGroups; private boolean playerWasPrepared; - private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, - RenderersFactory renderersFactory, MappingTrackSelector trackSelector, - LoadControl loadControl, @Nullable ActionSchedule actionSchedule, - @Nullable Player.EventListener eventListener, int expectedPlayerEndedCount) { + private ExoPlayerTestRunner( + PlayerFactory playerFactory, + MediaSource mediaSource, + RenderersFactory renderersFactory, + MappingTrackSelector trackSelector, + LoadControl loadControl, + @Nullable ActionSchedule actionSchedule, + @Nullable Player.EventListener eventListener, + @Nullable VideoRendererEventListener videoRendererEventListener, + @Nullable AudioRendererEventListener audioRendererEventListener, + int expectedPlayerEndedCount) { this.playerFactory = playerFactory; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -358,6 +400,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener this.loadControl = loadControl; this.actionSchedule = actionSchedule; this.eventListener = eventListener; + this.videoRendererEventListener = videoRendererEventListener; + this.audioRendererEventListener = audioRendererEventListener; this.timelines = new ArrayList<>(); this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); @@ -380,25 +424,33 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener * @return This test runner. */ public ExoPlayerTestRunner start() { - handler.post(new Runnable() { - @Override - public void run() { - try { - player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); - player.addListener(ExoPlayerTestRunner.this); - if (eventListener != null) { - player.addListener(eventListener); + handler.post( + new Runnable() { + @Override + public void run() { + try { + player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player.addListener(ExoPlayerTestRunner.this); + if (eventListener != null) { + player.addListener(eventListener); + } + if (videoRendererEventListener != null) { + player.addVideoDebugListener(videoRendererEventListener); + } + if (audioRendererEventListener != null) { + player.addAudioDebugListener(audioRendererEventListener); + } + player.setPlayWhenReady(true); + if (actionSchedule != null) { + actionSchedule.start( + player, trackSelector, null, handler, ExoPlayerTestRunner.this); + } + player.prepare(mediaSource); + } catch (Exception e) { + handleException(e); + } } - player.setPlayWhenReady(true); - if (actionSchedule != null) { - actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); - } - player.prepare(mediaSource); - } catch (Exception e) { - handleException(e); - } - } - }); + }); return this; } From ce8736c71ace969920a49f94ef3e8a88b5f939fd Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 04:12:48 -0800 Subject: [PATCH 142/417] Avoid concurrent read/write access to sampleQueues in HlsSampleStreamWrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177435977 --- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 87585a52da..6cb3f854c8 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 @@ -16,12 +16,15 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -67,6 +70,8 @@ import java.util.Arrays; } + private static final String TAG = "HlsSampleStreamWrapper"; + private static final int PRIMARY_TYPE_NONE = 0; private static final int PRIMARY_TYPE_TEXT = 1; private static final int PRIMARY_TYPE_AUDIO = 2; @@ -588,13 +593,17 @@ import java.util.Arrays; // ExtractorOutput implementation. Called by the loading thread. @Override - public SampleQueue track(int id, int type) { + public TrackOutput track(int id, int type) { int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { if (sampleQueueTrackIds[i] == id) { return sampleQueues[i]; } } + if (sampleQueuesBuilt) { + Log.w(TAG, "Unmapped track with id " + id + " of type " + type); + return new DummyTrackOutput(); + } SampleQueue trackOutput = new SampleQueue(allocator); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setUpstreamFormatChangeListener(this); From 58e60e1f9d4506e5e537b095840c6dcfa678fac9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 04:48:24 -0800 Subject: [PATCH 143/417] Use the Builder pattern for DefaultTrackSelector#Parameters ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177438430 --- RELEASENOTES.md | 1 + .../trackselection/DefaultTrackSelector.java | 487 +++++++++--------- .../DefaultTrackSelectorTest.java | 63 +-- 3 files changed, 279 insertions(+), 272 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2c07ad6118..90dbdb6b00 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, 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 49b8e8964b..b4fa64c8fd 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 @@ -74,11 +74,252 @@ import java.util.concurrent.atomic.AtomicReference; */ public class DefaultTrackSelector extends MappingTrackSelector { + /** + * A builder for {@link Parameters}. + */ + public static final class ParametersBuilder { + + private String preferredAudioLanguage; + private String preferredTextLanguage; + private boolean selectUndeterminedTextLanguage; + private boolean forceLowestBitrate; + private boolean allowMixedMimeAdaptiveness; + private boolean allowNonSeamlessAdaptiveness; + private int maxVideoWidth; + private int maxVideoHeight; + private int maxVideoBitrate; + private boolean exceedVideoConstraintsIfNecessary; + private boolean exceedRendererCapabilitiesIfNecessary; + private int viewportWidth; + private int viewportHeight; + private boolean viewportOrientationMayChange; + + /** + * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}. + */ + public ParametersBuilder() { + this(Parameters.DEFAULT); + } + + /** + * @param initialValues The {@link Parameters} from which the initial values of the builder are + * obtained. + */ + private ParametersBuilder(Parameters initialValues) { + preferredAudioLanguage = initialValues.preferredAudioLanguage; + preferredTextLanguage = initialValues.preferredTextLanguage; + selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; + forceLowestBitrate = initialValues.forceLowestBitrate; + allowMixedMimeAdaptiveness = initialValues.allowMixedMimeAdaptiveness; + allowNonSeamlessAdaptiveness = initialValues.allowNonSeamlessAdaptiveness; + maxVideoWidth = initialValues.maxVideoWidth; + maxVideoHeight = initialValues.maxVideoHeight; + maxVideoBitrate = initialValues.maxVideoBitrate; + exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary; + exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; + viewportWidth = initialValues.viewportWidth; + viewportHeight = initialValues.viewportHeight; + viewportOrientationMayChange = initialValues.viewportOrientationMayChange; + } + + /** + * See {@link Parameters#preferredAudioLanguage}. + * + * @return This builder. + */ + public ParametersBuilder setPreferredAudioLanguage(String preferredAudioLanguage) { + this.preferredAudioLanguage = preferredAudioLanguage; + return this; + } + + /** + * See {@link Parameters#preferredTextLanguage}. + * + * @return This builder. + */ + public ParametersBuilder setPreferredTextLanguage(String preferredTextLanguage) { + this.preferredTextLanguage = preferredTextLanguage; + return this; + } + + /** + * See {@link Parameters#selectUndeterminedTextLanguage}. + * + * @return This builder. + */ + public ParametersBuilder setSelectUndeterminedTextLanguage( + boolean selectUndeterminedTextLanguage) { + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + return this; + } + + /** + * See {@link Parameters#forceLowestBitrate}. + * + * @return This builder. + */ + public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { + this.forceLowestBitrate = forceLowestBitrate; + return this; + } + + /** + * See {@link Parameters#allowMixedMimeAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { + this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; + return this; + } + + /** + * See {@link Parameters#allowNonSeamlessAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { + this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; + return this; + } + + /** + * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(1279, 719)}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoSizeSd() { + return setMaxVideoSize(1279, 719); + } + + /** + * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. + * + * @return This builder. + */ + public ParametersBuilder clearVideoSizeConstraints() { + return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * See {@link Parameters#maxVideoWidth} and {@link Parameters#maxVideoHeight}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { + this.maxVideoWidth = maxVideoWidth; + this.maxVideoHeight = maxVideoHeight; + return this; + } + + /** + * See {@link Parameters#maxVideoBitrate}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { + this.maxVideoBitrate = maxVideoBitrate; + return this; + } + + /** + * See {@link Parameters#exceedVideoConstraintsIfNecessary}. + * + * @return This builder. + */ + public ParametersBuilder setExceedVideoConstraintsIfNecessary( + boolean exceedVideoConstraintsIfNecessary) { + this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + return this; + } + + /** + * See {@link Parameters#exceedRendererCapabilitiesIfNecessary}. + * + * @return This builder. + */ + public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( + boolean exceedRendererCapabilitiesIfNecessary) { + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; + return this; + } + + /** + * Equivalent to invoking {@link #setViewportSize} with the viewport size values obtained from + * the provided {@link Context}. + * + * @param context The context to obtain the viewport size from. + * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. + * @return This builder. + */ + public ParametersBuilder setViewportSizeFromContext(Context context, + boolean viewportOrientationMayChange) { + // Assume the viewport is fullscreen. + Point viewportSize = Util.getPhysicalDisplaySize(context); + return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); + } + + /** + * Equivalent to + * {@link #setViewportSize setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. + * + * @return This builder. + */ + public ParametersBuilder clearViewportSizeConstraints() { + return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); + } + + /** + * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and + * {@link Parameters#viewportOrientationMayChange}. + * + * @return This builder. + */ + public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight, + boolean viewportOrientationMayChange) { + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + this.viewportOrientationMayChange = viewportOrientationMayChange; + return this; + } + + /** + * Builds a {@link Parameters} instance with the selected values. + */ + public Parameters build() { + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + } + /** * Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters { + /** + * An instance with default values: + *

        + *
      • No preferred audio language.
      • + *
      • No preferred text language.
      • + *
      • Text tracks with undetermined language are not selected if no track with + * {@link #preferredTextLanguage} is available.
      • + *
      • Lowest bitrate track selections are not forced.
      • + *
      • Adaptation between different mime types is not allowed.
      • + *
      • Non seamless adaptation is allowed.
      • + *
      • No max limit for video width/height.
      • + *
      • No max video bitrate.
      • + *
      • Video constraints are exceeded if no supported selection can be made otherwise.
      • + *
      • Renderer capabilities are exceeded if no supported selection can be made.
      • + *
      • No viewport constraints.
      • + *
      + */ + public static final Parameters DEFAULT = new Parameters(); + // Audio /** * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. @@ -150,52 +391,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedRendererCapabilitiesIfNecessary; - /** - * Default parameters. The default values are: - *
        - *
      • No preferred audio language is set.
      • - *
      • No preferred text language is set.
      • - *
      • Text tracks with undetermined language are not selected if no track with - * {@link #preferredTextLanguage} is available.
      • - *
      • Lowest bitrate track selections are not forced.
      • - *
      • Adaptation between different mime types is not allowed.
      • - *
      • Non seamless adaptation is allowed.
      • - *
      • No max limit for video width/height.
      • - *
      • No max video bitrate.
      • - *
      • Video constraints are exceeded if no supported selection can be made otherwise.
      • - *
      • Renderer capabilities are exceeded if no supported selection can be made.
      • - *
      • No viewport constraints are set.
      • - *
      - */ - public Parameters() { + private Parameters() { this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } - /** - * @param preferredAudioLanguage See {@link #preferredAudioLanguage} - * @param preferredTextLanguage See {@link #preferredTextLanguage} - * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. - * @param forceLowestBitrate See {@link #forceLowestBitrate}. - * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} - * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} - * @param maxVideoWidth See {@link #maxVideoWidth} - * @param maxVideoHeight See {@link #maxVideoHeight} - * @param maxVideoBitrate See {@link #maxVideoBitrate} - * @param exceedVideoConstraintsIfNecessary See {@link #exceedVideoConstraintsIfNecessary} - * @param exceedRendererCapabilitiesIfNecessary See {@link #preferredTextLanguage} - * @param viewportWidth See {@link #viewportWidth} - * @param viewportHeight See {@link #viewportHeight} - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} - */ - public Parameters(String preferredAudioLanguage, String preferredTextLanguage, + private Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { - this.preferredAudioLanguage = preferredAudioLanguage; - this.preferredTextLanguage = preferredTextLanguage; + this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); + this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; @@ -211,205 +419,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided {@link #preferredAudioLanguage}. + * Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */ - public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { - preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); - if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #preferredTextLanguage}. - */ - public Parameters withPreferredTextLanguage(String preferredTextLanguage) { - preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); - if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. - */ - public Parameters withSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { - if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #forceLowestBitrate}. - */ - public Parameters withForceLowestBitrate(boolean forceLowestBitrate) { - if (forceLowestBitrate == this.forceLowestBitrate) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #allowMixedMimeAdaptiveness}. - */ - public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { - if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #allowNonSeamlessAdaptiveness}. - */ - public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { - if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #maxVideoWidth} and {@link #maxVideoHeight}. - */ - public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { - if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #maxVideoBitrate}. - */ - public Parameters withMaxVideoBitrate(int maxVideoBitrate) { - if (maxVideoBitrate == this.maxVideoBitrate) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Equivalent to {@code withMaxVideoSize(1279, 719)}. - * - * @return An instance with maximum standard definition as maximum video size. - */ - public Parameters withMaxVideoSizeSd() { - return withMaxVideoSize(1279, 719); - } - - /** - * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. - * - * @return An instance without video size constraints. - */ - public Parameters withoutVideoSizeConstraints() { - return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); - } - - /** - * Returns an instance with the provided {@link #exceedVideoConstraintsIfNecessary}. - */ - public Parameters withExceedVideoConstraintsIfNecessary( - boolean exceedVideoConstraintsIfNecessary) { - if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #exceedRendererCapabilitiesIfNecessary}. - */ - public Parameters withExceedRendererCapabilitiesIfNecessary( - boolean exceedRendererCapabilitiesIfNecessary) { - if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #viewportWidth}, {@link #viewportHeight} and - * {@link #viewportOrientationMayChange}. - */ - public Parameters withViewportSize(int viewportWidth, int viewportHeight, - boolean viewportOrientationMayChange) { - if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight - && viewportOrientationMayChange == this.viewportOrientationMayChange) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance where the viewport size is obtained from the provided {@link Context}. - * - * @param context The context to obtain the viewport size from. - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. - * @return An instance where the viewport size is obtained from the provided {@link Context}. - */ - public Parameters withViewportSizeFromContext(Context context, - boolean viewportOrientationMayChange) { - // Assume the viewport is fullscreen. - Point viewportSize = Util.getPhysicalDisplaySize(context); - return withViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); - } - - /** - * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. - * - * @return An instance without viewport size constraints. - */ - public Parameters withoutViewportSizeConstraints() { - return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); + public ParametersBuilder buildUpon() { + return new ParametersBuilder(this); } @Override @@ -492,7 +505,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - paramsReference = new AtomicReference<>(new Parameters()); + paramsReference = new AtomicReference<>(Parameters.DEFAULT); } /** @@ -882,7 +895,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { return isSupported(formatSupport, false) && format.channelCount == configuration.channelCount && format.sampleRate == configuration.sampleRate && (configuration.mimeType == null - || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); + || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); } // Text track selection implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 6b14d139ae..1eff48b730 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -15,6 +15,7 @@ import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashMap; @@ -33,7 +34,6 @@ import org.robolectric.annotation.Config; @Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class DefaultTrackSelectorTest { - private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = @@ -61,7 +61,6 @@ public final class DefaultTrackSelectorTest { public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() throws Exception { trackSelector.init(invalidationListener); - trackSelector.setParameters(DEFAULT_PARAMETERS); verify(invalidationListener, never()).onTrackSelectionsInvalidated(); } @@ -73,7 +72,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + Parameters parameters = new ParametersBuilder().setPreferredAudioLanguage("eng").build(); trackSelector.init(invalidationListener); trackSelector.setParameters(parameters); @@ -88,10 +87,10 @@ public final class DefaultTrackSelectorTest { @Test public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + ParametersBuilder builder = new ParametersBuilder().setPreferredAudioLanguage("eng"); trackSelector.init(invalidationListener); - trackSelector.setParameters(parameters); - trackSelector.setParameters(parameters); + trackSelector.setParameters(builder.build()); + trackSelector.setParameters(builder.build()); verify(invalidationListener, times(1)).onTrackSelectionsInvalidated(); } @@ -122,15 +121,14 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksSelectPreferredAudioLanguage() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format frAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); Format enAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -146,19 +144,18 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksSelectPreferredAudioLanguageOverSelectionFlag() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format frAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "fr"); + Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "fra"); Format enAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, - singleTrackGroup(frAudioFormat, enAudioFormat)); + wrapFormats(frAudioFormat, enAudioFormat)); assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(enAudioFormat); } @@ -168,8 +165,7 @@ public final class DefaultTrackSelectorTest { * track that exceed renderer's capabilities. */ @Test - public void testSelectTracksPreferTrackWithinCapabilities() - throws Exception { + public void testSelectTracksPreferTrackWithinCapabilities() throws Exception { Format supportedFormat = Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -197,7 +193,6 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNoTrackWithinCapabilitiesSelectExceededCapabilityTrack() throws Exception { - Format audioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -216,8 +211,8 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoSelection() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withExceedRendererCapabilitiesIfNecessary(false); - trackSelector.setParameters(parameters); + trackSelector.setParameters( + new ParametersBuilder().setExceedRendererCapabilitiesIfNecessary(false).build()); Format audioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, @@ -264,15 +259,14 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format supportedFrFormat = Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); Format exceededEnFormat = Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); Map mappedCapabilities = new HashMap<>(); mappedCapabilities.put(exceededEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES); @@ -295,15 +289,14 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferredLanguage() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format supportedFrFormat = Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); Format exceededDefaultSelectionEnFormat = - Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "en"); + Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "eng"); Map mappedCapabilities = new HashMap<>(); mappedCapabilities.put(exceededDefaultSelectionEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES); @@ -561,12 +554,14 @@ public final class DefaultTrackSelectorTest { wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters(DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguage(true)); + trackSelector.setParameters( + new ParametersBuilder().setSelectUndeterminedTextLanguage(true).build()); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); - trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + ParametersBuilder builder = new ParametersBuilder().setPreferredTextLanguage("spa"); + trackSelector.setParameters(builder.build()); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); @@ -575,8 +570,7 @@ public final class DefaultTrackSelectorTest { wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters( - trackSelector.getParameters().withSelectUndeterminedTextLanguage(true)); + trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true).build()); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); @@ -596,8 +590,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBitrate() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withForceLowestBitrate(true); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setForceLowestBitrate(true).build()); Format lowerBitrateFormat = Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, From 21d55d4ebabc11206be1546ea9610d64e656cdf8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 05:17:49 -0800 Subject: [PATCH 144/417] Rename DefaultTrackSelector.ParameterBuilder.setViewportSize{FromContext->ToPhysicalDisplaySize} ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177440699 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b4fa64c8fd..2f0dc8f04e 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 @@ -245,14 +245,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Equivalent to invoking {@link #setViewportSize} with the viewport size values obtained from - * the provided {@link Context}. + * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from + * {@link Util#getPhysicalDisplaySize(Context)}. * * @param context The context to obtain the viewport size from. * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSizeFromContext(Context context, + public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); From cc54d4d3e6902acf127a41ab88d724d024b95dab Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 05:34:51 -0800 Subject: [PATCH 145/417] Snap to frame boundary in ConstantBitrateSeeker - This change snaps the seek position for constant bitrate MP3s to the nearest frame boundary, avoiding the need to skip one byte at a time to re-synchronize (this may still happen if the MP3 does not really have fixed size frames). - Tweaked both ConstantBitrateSeeker and WavHeader to ensure the returned positions are valid. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177441798 --- .../assets/mp3/play-trimmed.mp3.1.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.2.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.3.dump | 6 +++- .../extractor/mp3/ConstantBitrateSeeker.java | 32 +++++++++++++++---- .../extractor/mp3/Mp3Extractor.java | 4 +-- .../exoplayer2/extractor/wav/WavHeader.java | 11 ++++--- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index 47e12161a8..e02e99e139 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -26,27 +26,47 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; + private final long dataSize; + private final int frameSize; private final int bitrate; private final long durationUs; - public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) { + /** + * @param firstFramePosition The position (byte offset) of the first frame. + * @param inputLength The length of the stream. + * @param frameSize The size of a single frame in the stream. + * @param bitrate The stream's bitrate. + */ + public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, + int bitrate) { this.firstFramePosition = firstFramePosition; + this.frameSize = frameSize; this.bitrate = bitrate; - durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength); + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFramePosition; + durationUs = getTimeUs(inputLength); + } } @Override public boolean isSeekable() { - return durationUs != C.TIME_UNSET; + return dataSize != C.LENGTH_UNSET; } @Override public long getPosition(long timeUs) { - if (durationUs == C.TIME_UNSET) { + if (dataSize == C.LENGTH_UNSET) { return firstFramePosition; } - timeUs = Util.constrainValue(timeUs, 0, durationUs); - return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); + // Add data start position. + return firstFramePosition + positionOffset; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index dc7d21851a..7c579504c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -393,8 +393,8 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, - input.getLength()); + return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), + synchronizedHeader.frameSize, synchronizedHeader.bitrate); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index 1c1fc97a22..2cdd31cb6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ /* package */ final class WavHeader implements SeekMap { @@ -83,10 +84,12 @@ import com.google.android.exoplayer2.extractor.SeekMap; @Override public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / blockAlignment) * blockAlignment; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); + // Add data start position. + return dataStartPosition + positionOffset; } // Misc getters. From 7b0889981822a2e3f6e8d6145491827a7a4fa1e2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 05:39:50 -0800 Subject: [PATCH 146/417] Move resetting audio processors to initialize() The set of active audio processors was only updated on reconfiguration and when draining playback parameters completed. Draining playback parameters are cleared in reset(), so if parameters were set while paused then the sink was quickly reset, without draining completing, the set of active audio processors wouldn't be updated. This means that a switch to or from speed or pitch = 1 would not be handled correctly if made while paused and followed by a seek. Move resetting active audio processors from configure (where if the active audio processors were reset we'd always initialize a new AudioTrack) to initialize(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177442098 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90dbdb6b00..3a42311b26 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * Add optional parameter to `Player.stop` to reset the player when stopping. +* Fix handling of playback parameters changes while paused when followed by a + seek. ### 2.6.0 ### 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 3b14b69916..ab4564e2c3 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 @@ -364,9 +364,6 @@ public final class DefaultAudioSink implements AudioSink { encoding = audioProcessor.getOutputEncoding(); } } - if (flush) { - resetAudioProcessors(); - } } int channelConfig; @@ -492,6 +489,9 @@ public final class DefaultAudioSink implements AudioSink { // The old playback parameters may no longer be applicable so try to reset them now. setPlaybackParameters(playbackParameters); + // Flush and reset active audio processors. + resetAudioProcessors(); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { From e28adb00ff500cdc35e3078f8695b0a46aa0e34c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Nov 2017 05:53:45 -0800 Subject: [PATCH 147/417] Use Handler to post action schedule finished callback. Calling it directly might skip other callbacks. For example: ActionSchedule.Builder().waitForTimelineChanged(...).build(). is currently immediately calling through to callback.onActionScheduleFinished when the timeline changes. Depending on the position of the action schedule listener in the listener set, it may skip other listeners also listening to timeline changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177442975 --- .../exoplayer2/testutil/ActionSchedule.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 7a2ce9270c..477071f91f 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 @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuit import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; /** @@ -487,13 +488,30 @@ public final class ActionSchedule { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionAndScheduleNextImpl( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + Handler handler, + ActionNode nextAction) { + Assertions.checkArgument(nextAction == null); if (callback != null) { - callback.onActionScheduleFinished(); + handler.post( + new Runnable() { + @Override + public void run() { + callback.onActionScheduleFinished(); + } + }); } } + @Override + protected void doActionImpl( + SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + // Not triggered. + } + } } From 754260e9441c9dc32a320689503e3adcb08d580c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 07:20:45 -0800 Subject: [PATCH 148/417] Fix VBRI and XING seekers - Remove skipping of the VBRI/XING frame before calculating position offsets. This was incorrect. Instead, a constraint is used to ensure we don't return positions within these frames, the difference being that the constraint adjusts only positions that would fall within the frames, where-as the previous approach shifted positions through the whole stream. - Excluded last entry in the VBRI table because it has an invalid position (the length of the stream). - Give variables in XingSeeker descriptive names. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177451295 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 308 +++++++++--------- .../androidTest/assets/mp3/bear.mp3.2.dump | 76 ++--- .../extractor/mp3/ConstantBitrateSeeker.java | 18 +- .../extractor/mp3/Mp3Extractor.java | 7 +- .../exoplayer2/extractor/mp3/VbriSeeker.java | 33 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 112 ++++--- .../extractor/mp3/XingSeekerTest.java | 26 +- 7 files changed, 298 insertions(+), 282 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 2e0b21050c..7b6fe9db37 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -25,309 +25,313 @@ track 0: language = null drmInitData = - initializationData: - sample count = 76 + sample count = 77 sample 0: - time = 945782 + time = 928567 + flags = 1 + data = length 384, hash F7E344F4 + sample 1: + time = 952567 flags = 1 data = length 384, hash 14EF6AFD - sample 1: - time = 969782 + sample 2: + time = 976567 flags = 1 data = length 384, hash 61C9B92C - sample 2: - time = 993782 + sample 3: + time = 1000567 flags = 1 data = length 384, hash ABE1368 - sample 3: - time = 1017782 + sample 4: + time = 1024567 flags = 1 data = length 384, hash 6A3B8547 - sample 4: - time = 1041782 + sample 5: + time = 1048567 flags = 1 data = length 384, hash 30E905FA - sample 5: - time = 1065782 + sample 6: + time = 1072567 flags = 1 data = length 384, hash 21A267CD - sample 6: - time = 1089782 + sample 7: + time = 1096567 flags = 1 data = length 384, hash D96A2651 - sample 7: - time = 1113782 + sample 8: + time = 1120567 flags = 1 data = length 384, hash 72340177 - sample 8: - time = 1137782 + sample 9: + time = 1144567 flags = 1 data = length 384, hash 9345E744 - sample 9: - time = 1161782 + sample 10: + time = 1168567 flags = 1 data = length 384, hash FDE39E3A - sample 10: - time = 1185782 + sample 11: + time = 1192567 flags = 1 data = length 384, hash F0B7465 - sample 11: - time = 1209782 + sample 12: + time = 1216567 flags = 1 data = length 384, hash 3693AB86 - sample 12: - time = 1233782 + sample 13: + time = 1240567 flags = 1 data = length 384, hash F39719B1 - sample 13: - time = 1257782 + sample 14: + time = 1264567 flags = 1 data = length 384, hash DA3958DC - sample 14: - time = 1281782 + sample 15: + time = 1288567 flags = 1 data = length 384, hash FDC7599F - sample 15: - time = 1305782 + sample 16: + time = 1312567 flags = 1 data = length 384, hash AEFF8471 - sample 16: - time = 1329782 + sample 17: + time = 1336567 flags = 1 data = length 384, hash 89C92C19 - sample 17: - time = 1353782 + sample 18: + time = 1360567 flags = 1 data = length 384, hash 5C786A4B - sample 18: - time = 1377782 + sample 19: + time = 1384567 flags = 1 data = length 384, hash 5ACA8B - sample 19: - time = 1401782 + sample 20: + time = 1408567 flags = 1 data = length 384, hash 7755974C - sample 20: - time = 1425782 + sample 21: + time = 1432567 flags = 1 data = length 384, hash 3934B73C - sample 21: - time = 1449782 + sample 22: + time = 1456567 flags = 1 data = length 384, hash DDD70A2F - sample 22: - time = 1473782 + sample 23: + time = 1480567 flags = 1 data = length 384, hash 8FACE2EF - sample 23: - time = 1497782 + sample 24: + time = 1504567 flags = 1 data = length 384, hash 4A602591 - sample 24: - time = 1521782 + sample 25: + time = 1528567 flags = 1 data = length 384, hash D019AA2D - sample 25: - time = 1545782 + sample 26: + time = 1552567 flags = 1 data = length 384, hash 8A680B9D - sample 26: - time = 1569782 + sample 27: + time = 1576567 flags = 1 data = length 384, hash B655C959 - sample 27: - time = 1593782 + sample 28: + time = 1600567 flags = 1 data = length 384, hash 2168336B - sample 28: - time = 1617782 + sample 29: + time = 1624567 flags = 1 data = length 384, hash D77F6D31 - sample 29: - time = 1641782 + sample 30: + time = 1648567 flags = 1 data = length 384, hash 524B4B2F - sample 30: - time = 1665782 + sample 31: + time = 1672567 flags = 1 data = length 384, hash 4752DDFC - sample 31: - time = 1689782 + sample 32: + time = 1696567 flags = 1 data = length 384, hash E786727F - sample 32: - time = 1713782 + sample 33: + time = 1720567 flags = 1 data = length 384, hash 5DA6FB8C - sample 33: - time = 1737782 + sample 34: + time = 1744567 flags = 1 data = length 384, hash 92F24269 - sample 34: - time = 1761782 + sample 35: + time = 1768567 flags = 1 data = length 384, hash CD0A3BA1 - sample 35: - time = 1785782 + sample 36: + time = 1792567 flags = 1 data = length 384, hash 7D00409F - sample 36: - time = 1809782 + sample 37: + time = 1816567 flags = 1 data = length 384, hash D7ADB5FA - sample 37: - time = 1833782 + sample 38: + time = 1840567 flags = 1 data = length 384, hash 4A140209 - sample 38: - time = 1857782 + sample 39: + time = 1864567 flags = 1 data = length 384, hash E801184A - sample 39: - time = 1881782 + sample 40: + time = 1888567 flags = 1 data = length 384, hash 53C6CF9C - sample 40: - time = 1905782 + sample 41: + time = 1912567 flags = 1 data = length 384, hash 19A8D99F - sample 41: - time = 1929782 + sample 42: + time = 1936567 flags = 1 data = length 384, hash E47EB43F - sample 42: - time = 1953782 + sample 43: + time = 1960567 flags = 1 data = length 384, hash 4EA329E7 - sample 43: - time = 1977782 + sample 44: + time = 1984567 flags = 1 data = length 384, hash 1CCAAE62 - sample 44: - time = 2001782 + sample 45: + time = 2008567 flags = 1 data = length 384, hash ED3F8C66 - sample 45: - time = 2025782 + sample 46: + time = 2032567 flags = 1 data = length 384, hash D3D646B6 - sample 46: - time = 2049782 + sample 47: + time = 2056567 flags = 1 data = length 384, hash 68CD1574 - sample 47: - time = 2073782 + sample 48: + time = 2080567 flags = 1 data = length 384, hash 8CEAB382 - sample 48: - time = 2097782 + sample 49: + time = 2104567 flags = 1 data = length 384, hash D54B1C48 - sample 49: - time = 2121782 + sample 50: + time = 2128567 flags = 1 data = length 384, hash FFE2EE90 - sample 50: - time = 2145782 + sample 51: + time = 2152567 flags = 1 data = length 384, hash BFE8A673 - sample 51: - time = 2169782 + sample 52: + time = 2176567 flags = 1 data = length 384, hash 978B1C92 - sample 52: - time = 2193782 + sample 53: + time = 2200567 flags = 1 data = length 384, hash 810CC71E - sample 53: - time = 2217782 + sample 54: + time = 2224567 flags = 1 data = length 384, hash 44FE42D9 - sample 54: - time = 2241782 + sample 55: + time = 2248567 flags = 1 data = length 384, hash 2F5BB02C - sample 55: - time = 2265782 + sample 56: + time = 2272567 flags = 1 data = length 384, hash 77DDB90 - sample 56: - time = 2289782 + sample 57: + time = 2296567 flags = 1 data = length 384, hash 24FB5EDA - sample 57: - time = 2313782 + sample 58: + time = 2320567 flags = 1 data = length 384, hash E73203C6 - sample 58: - time = 2337782 + sample 59: + time = 2344567 flags = 1 data = length 384, hash 14B525F1 - sample 59: - time = 2361782 + sample 60: + time = 2368567 flags = 1 data = length 384, hash 5E0F4E2E - sample 60: - time = 2385782 + sample 61: + time = 2392567 flags = 1 data = length 384, hash 67EE4E31 - sample 61: - time = 2409782 + sample 62: + time = 2416567 flags = 1 data = length 384, hash 2E04EC4C - sample 62: - time = 2433782 + sample 63: + time = 2440567 flags = 1 data = length 384, hash 852CABA7 - sample 63: - time = 2457782 + sample 64: + time = 2464567 flags = 1 data = length 384, hash 19928903 - sample 64: - time = 2481782 + sample 65: + time = 2488567 flags = 1 data = length 384, hash 5DA42021 - sample 65: - time = 2505782 + sample 66: + time = 2512567 flags = 1 data = length 384, hash 45B20B7C - sample 66: - time = 2529782 + sample 67: + time = 2536567 flags = 1 data = length 384, hash D108A215 - sample 67: - time = 2553782 + sample 68: + time = 2560567 flags = 1 data = length 384, hash BD25DB7C - sample 68: - time = 2577782 + sample 69: + time = 2584567 flags = 1 data = length 384, hash DA7F9861 - sample 69: - time = 2601782 + sample 70: + time = 2608567 flags = 1 data = length 384, hash CCD576F - sample 70: - time = 2625782 + sample 71: + time = 2632567 flags = 1 data = length 384, hash 405C1EB5 - sample 71: - time = 2649782 + sample 72: + time = 2656567 flags = 1 data = length 384, hash 6640B74E - sample 72: - time = 2673782 + sample 73: + time = 2680567 flags = 1 data = length 384, hash B4E5937A - sample 73: - time = 2697782 + sample 74: + time = 2704567 flags = 1 data = length 384, hash CEE17733 - sample 74: - time = 2721782 + sample 75: + time = 2728567 flags = 1 data = length 384, hash 2A0DA733 - sample 75: - time = 2745782 + sample 76: + time = 2752567 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index b3cb117cb2..3f393e768e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -27,155 +27,155 @@ track 0: initializationData: sample count = 38 sample 0: - time = 1858196 + time = 1871586 flags = 1 data = length 384, hash E801184A sample 1: - time = 1882196 + time = 1895586 flags = 1 data = length 384, hash 53C6CF9C sample 2: - time = 1906196 + time = 1919586 flags = 1 data = length 384, hash 19A8D99F sample 3: - time = 1930196 + time = 1943586 flags = 1 data = length 384, hash E47EB43F sample 4: - time = 1954196 + time = 1967586 flags = 1 data = length 384, hash 4EA329E7 sample 5: - time = 1978196 + time = 1991586 flags = 1 data = length 384, hash 1CCAAE62 sample 6: - time = 2002196 + time = 2015586 flags = 1 data = length 384, hash ED3F8C66 sample 7: - time = 2026196 + time = 2039586 flags = 1 data = length 384, hash D3D646B6 sample 8: - time = 2050196 + time = 2063586 flags = 1 data = length 384, hash 68CD1574 sample 9: - time = 2074196 + time = 2087586 flags = 1 data = length 384, hash 8CEAB382 sample 10: - time = 2098196 + time = 2111586 flags = 1 data = length 384, hash D54B1C48 sample 11: - time = 2122196 + time = 2135586 flags = 1 data = length 384, hash FFE2EE90 sample 12: - time = 2146196 + time = 2159586 flags = 1 data = length 384, hash BFE8A673 sample 13: - time = 2170196 + time = 2183586 flags = 1 data = length 384, hash 978B1C92 sample 14: - time = 2194196 + time = 2207586 flags = 1 data = length 384, hash 810CC71E sample 15: - time = 2218196 + time = 2231586 flags = 1 data = length 384, hash 44FE42D9 sample 16: - time = 2242196 + time = 2255586 flags = 1 data = length 384, hash 2F5BB02C sample 17: - time = 2266196 + time = 2279586 flags = 1 data = length 384, hash 77DDB90 sample 18: - time = 2290196 + time = 2303586 flags = 1 data = length 384, hash 24FB5EDA sample 19: - time = 2314196 + time = 2327586 flags = 1 data = length 384, hash E73203C6 sample 20: - time = 2338196 + time = 2351586 flags = 1 data = length 384, hash 14B525F1 sample 21: - time = 2362196 + time = 2375586 flags = 1 data = length 384, hash 5E0F4E2E sample 22: - time = 2386196 + time = 2399586 flags = 1 data = length 384, hash 67EE4E31 sample 23: - time = 2410196 + time = 2423586 flags = 1 data = length 384, hash 2E04EC4C sample 24: - time = 2434196 + time = 2447586 flags = 1 data = length 384, hash 852CABA7 sample 25: - time = 2458196 + time = 2471586 flags = 1 data = length 384, hash 19928903 sample 26: - time = 2482196 + time = 2495586 flags = 1 data = length 384, hash 5DA42021 sample 27: - time = 2506196 + time = 2519586 flags = 1 data = length 384, hash 45B20B7C sample 28: - time = 2530196 + time = 2543586 flags = 1 data = length 384, hash D108A215 sample 29: - time = 2554196 + time = 2567586 flags = 1 data = length 384, hash BD25DB7C sample 30: - time = 2578196 + time = 2591586 flags = 1 data = length 384, hash DA7F9861 sample 31: - time = 2602196 + time = 2615586 flags = 1 data = length 384, hash CCD576F sample 32: - time = 2626196 + time = 2639586 flags = 1 data = length 384, hash 405C1EB5 sample 33: - time = 2650196 + time = 2663586 flags = 1 data = length 384, hash 6640B74E sample 34: - time = 2674196 + time = 2687586 flags = 1 data = length 384, hash B4E5937A sample 35: - time = 2698196 + time = 2711586 flags = 1 data = length 384, hash CEE17733 sample 36: - time = 2722196 + time = 2735586 flags = 1 data = length 384, hash 2A0DA733 sample 37: - time = 2746196 + time = 2759586 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index e02e99e139..442e62deca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.Util; /** @@ -26,22 +27,21 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; - private final long dataSize; private final int frameSize; + private final long dataSize; private final int bitrate; private final long durationUs; /** - * @param firstFramePosition The position (byte offset) of the first frame. - * @param inputLength The length of the stream. - * @param frameSize The size of a single frame in the stream. - * @param bitrate The stream's bitrate. + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFramePosition The position of the first frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the first frame. */ - public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, - int bitrate) { + public ConstantBitrateSeeker(long inputLength, long firstFramePosition, + MpegAudioHeader mpegAudioHeader) { this.firstFramePosition = firstFramePosition; - this.frameSize = frameSize; - this.bitrate = bitrate; + this.frameSize = mpegAudioHeader.frameSize; + this.bitrate = mpegAudioHeader.bitrate; if (inputLength == C.LENGTH_UNSET) { dataSize = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 7c579504c3..5c56dc460a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor { int seekHeader = getSeekFrameHeader(frame, xingBase); Seeker seeker; if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { - seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor { return getConstantBitrateSeeker(input); } } else if (seekHeader == SEEK_HEADER_VBRI) { - seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); input.skipFully(synchronizedHeader.frameSize); } else { // seekerHeader == SEEK_HEADER_UNSET // This frame doesn't contain seeking information, so reset the peek position. @@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), - synchronizedHeader.frameSize, synchronizedHeader.bitrate); + return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index c43f065592..cc631d9f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class VbriSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "VbriSeeker"; + /** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { @@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util; int entrySize = frame.readUnsignedShort(); frame.skipBytes(2); - // Skip the frame containing the VBRI header. - position += mpegAudioHeader.frameSize; - + long minPosition = position + mpegAudioHeader.frameSize; // Read table of contents entries. - long[] timesUs = new long[entryCount + 1]; - long[] positions = new long[entryCount + 1]; - timesUs[0] = 0L; - positions[0] = position; - for (int index = 1; index < timesUs.length; index++) { + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); int segmentSize; switch (entrySize) { case 1: @@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util; return null; } position += segmentSize * scale; - timesUs[index] = index * durationUs / entryCount; - positions[index] = - inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); + } + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); } return new VbriSeeker(timesUs, positions, durationUs); } 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 55888066e7..e532249a64 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class XingSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "XingSeeker"; + /** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'Xing' or 'Info' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; - long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); int frameCount; @@ -54,10 +56,10 @@ import com.google.android.exoplayer2.util.Util; sampleRate); if ((flags & 0x06) != 0x06) { // If the size in bytes or table of contents is missing, the stream is not seekable. - return new XingSeeker(firstFramePosition, durationUs, inputLength); + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long sizeBytes = frame.readUnsignedIntToInt(); + long dataSize = frame.readUnsignedIntToInt(); long[] tableOfContents = new long[100]; for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); @@ -66,32 +68,37 @@ import com.google.android.exoplayer2.util.Util; // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); - return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, - sizeBytes, mpegAudioHeader.frameSize); + + if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) { + Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize)); + } + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize, + tableOfContents); } - private final long firstFramePosition; + private final long dataStartPosition; + private final int xingFrameSize; private final long durationUs; - private final long inputLength; + /** + * Data size, including the XING frame. + */ + private final long dataSize; /** * Entries are in the range [0, 255], but are stored as long integers for convenience. */ private final long[] tableOfContents; - private final long sizeBytes; - private final int headerSize; - private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { - this(firstFramePosition, durationUs, inputLength, null, 0, 0); + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null); } - private XingSeeker(long firstFramePosition, long durationUs, long inputLength, - long[] tableOfContents, long sizeBytes, int headerSize) { - this.firstFramePosition = firstFramePosition; + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize, + long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.inputLength = inputLength; + this.dataSize = dataSize; this.tableOfContents = tableOfContents; - this.sizeBytes = sizeBytes; - this.headerSize = headerSize; } @Override @@ -102,44 +109,45 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (!isSeekable()) { - return firstFramePosition; + return dataStartPosition + xingFrameSize; } double percent = (timeUs * 100d) / durationUs; - double fx; + double scaledPosition; if (percent <= 0) { - fx = 0; + scaledPosition = 0; } else if (percent >= 100) { - fx = 256; + scaledPosition = 256; } else { - int a = (int) percent; - float fa = tableOfContents[a]; - float fb = a == 99 ? 256 : tableOfContents[a + 1]; - fx = fa + (fb - fa) * (percent - a); + int prevTableIndex = (int) percent; + double prevScaledPosition = tableOfContents[prevTableIndex]; + double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two scaled positions. + double interpolateFraction = percent - prevTableIndex; + scaledPosition = prevScaledPosition + + (interpolateFraction * (nextScaledPosition - prevScaledPosition)); } - - long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; - long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 - : firstFramePosition - headerSize + sizeBytes - 1; - return Math.min(position, maximumPosition); + long positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return dataStartPosition + positionOffset; } @Override public long getTimeUs(long position) { - if (!isSeekable() || position < firstFramePosition) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; - int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); - long previousTime = getTimeUsForTocPosition(previousTocPosition); - - // Linearly interpolate the time taking into account the next entry. - long previousByte = tableOfContents[previousTocPosition]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; - long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); - long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) - * (offsetByte - previousByte) / (nextByte - previousByte)); - return previousTime + timeOffset; + double scaledPosition = (positionOffset * 256d) / dataSize; + int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); + long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); + long prevScaledPosition = tableOfContents[prevTableIndex]; + long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1); + long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two table entries. + double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0 + : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition)); + return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs)); } @Override @@ -148,11 +156,13 @@ import com.google.android.exoplayer2.util.Util; } /** - * Returns the time in microseconds corresponding to a table of contents position, which is - * interpreted as a percentage of the stream's duration between 0 and 100. + * Returns the time in microseconds for a given table index. + * + * @param tableIndex A table index in the range [0, 100]. + * @return The corresponding time in microseconds. */ - private long getTimeUsForTocPosition(int tocPosition) { - return (durationUs * tocPosition) / 100; + private long getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index b43949b7c2..e644abc7ef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -43,17 +43,17 @@ public final class XingSeekerTest { private static final int XING_FRAME_POSITION = 157; /** - * Size of the audio stream, encoded in {@link #XING_FRAME_PAYLOAD}. + * Data size, as encoded in {@link #XING_FRAME_PAYLOAD}. */ - private static final int STREAM_SIZE_BYTES = 948505; + private static final int DATA_SIZE_BYTES = 948505; /** * Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}. */ private static final int STREAM_DURATION_US = 59271836; /** - * The length of the file in bytes. + * The length of the stream in bytes. */ - private static final int INPUT_LENGTH = 948662; + private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES; private XingSeeker seeker; private XingSeeker seekerWithInputLength; @@ -63,10 +63,10 @@ public final class XingSeekerTest { public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); - seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), - XING_FRAME_POSITION, C.LENGTH_UNSET); - seekerWithInputLength = XingSeeker.create(xingFrameHeader, - new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); + seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader, + new ParsableByteArray(XING_FRAME_PAYLOAD)); + seekerWithInputLength = XingSeeker.create(STREAM_LENGTH, + XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)); xingFrameSize = xingFrameHeader.frameSize; } @@ -84,10 +84,10 @@ public final class XingSeekerTest { @Test public void testGetTimeUsAtEndOfStream() { - assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + assertThat(seeker.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); assertThat( - seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + seekerWithInputLength.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); } @@ -100,14 +100,14 @@ public final class XingSeekerTest { @Test public void testGetPositionAtEndOfStream() { assertThat(seeker.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); } @Test public void testGetTimeForAllPositions() { - for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { + for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; long timeUs = seeker.getTimeUs(position); assertThat(seeker.getPosition(timeUs)).isEqualTo(position); From 3a6b7a346cbd45bbf499955aa76d633c01edc55f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 08:36:03 -0800 Subject: [PATCH 149/417] Fix mp3 extractor test ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177458840 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 7b6fe9db37..a57894e81e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -27,311 +27,311 @@ track 0: initializationData: sample count = 77 sample 0: - time = 928567 + time = 928568 flags = 1 data = length 384, hash F7E344F4 sample 1: - time = 952567 + time = 952568 flags = 1 data = length 384, hash 14EF6AFD sample 2: - time = 976567 + time = 976568 flags = 1 data = length 384, hash 61C9B92C sample 3: - time = 1000567 + time = 1000568 flags = 1 data = length 384, hash ABE1368 sample 4: - time = 1024567 + time = 1024568 flags = 1 data = length 384, hash 6A3B8547 sample 5: - time = 1048567 + time = 1048568 flags = 1 data = length 384, hash 30E905FA sample 6: - time = 1072567 + time = 1072568 flags = 1 data = length 384, hash 21A267CD sample 7: - time = 1096567 + time = 1096568 flags = 1 data = length 384, hash D96A2651 sample 8: - time = 1120567 + time = 1120568 flags = 1 data = length 384, hash 72340177 sample 9: - time = 1144567 + time = 1144568 flags = 1 data = length 384, hash 9345E744 sample 10: - time = 1168567 + time = 1168568 flags = 1 data = length 384, hash FDE39E3A sample 11: - time = 1192567 + time = 1192568 flags = 1 data = length 384, hash F0B7465 sample 12: - time = 1216567 + time = 1216568 flags = 1 data = length 384, hash 3693AB86 sample 13: - time = 1240567 + time = 1240568 flags = 1 data = length 384, hash F39719B1 sample 14: - time = 1264567 + time = 1264568 flags = 1 data = length 384, hash DA3958DC sample 15: - time = 1288567 + time = 1288568 flags = 1 data = length 384, hash FDC7599F sample 16: - time = 1312567 + time = 1312568 flags = 1 data = length 384, hash AEFF8471 sample 17: - time = 1336567 + time = 1336568 flags = 1 data = length 384, hash 89C92C19 sample 18: - time = 1360567 + time = 1360568 flags = 1 data = length 384, hash 5C786A4B sample 19: - time = 1384567 + time = 1384568 flags = 1 data = length 384, hash 5ACA8B sample 20: - time = 1408567 + time = 1408568 flags = 1 data = length 384, hash 7755974C sample 21: - time = 1432567 + time = 1432568 flags = 1 data = length 384, hash 3934B73C sample 22: - time = 1456567 + time = 1456568 flags = 1 data = length 384, hash DDD70A2F sample 23: - time = 1480567 + time = 1480568 flags = 1 data = length 384, hash 8FACE2EF sample 24: - time = 1504567 + time = 1504568 flags = 1 data = length 384, hash 4A602591 sample 25: - time = 1528567 + time = 1528568 flags = 1 data = length 384, hash D019AA2D sample 26: - time = 1552567 + time = 1552568 flags = 1 data = length 384, hash 8A680B9D sample 27: - time = 1576567 + time = 1576568 flags = 1 data = length 384, hash B655C959 sample 28: - time = 1600567 + time = 1600568 flags = 1 data = length 384, hash 2168336B sample 29: - time = 1624567 + time = 1624568 flags = 1 data = length 384, hash D77F6D31 sample 30: - time = 1648567 + time = 1648568 flags = 1 data = length 384, hash 524B4B2F sample 31: - time = 1672567 + time = 1672568 flags = 1 data = length 384, hash 4752DDFC sample 32: - time = 1696567 + time = 1696568 flags = 1 data = length 384, hash E786727F sample 33: - time = 1720567 + time = 1720568 flags = 1 data = length 384, hash 5DA6FB8C sample 34: - time = 1744567 + time = 1744568 flags = 1 data = length 384, hash 92F24269 sample 35: - time = 1768567 + time = 1768568 flags = 1 data = length 384, hash CD0A3BA1 sample 36: - time = 1792567 + time = 1792568 flags = 1 data = length 384, hash 7D00409F sample 37: - time = 1816567 + time = 1816568 flags = 1 data = length 384, hash D7ADB5FA sample 38: - time = 1840567 + time = 1840568 flags = 1 data = length 384, hash 4A140209 sample 39: - time = 1864567 + time = 1864568 flags = 1 data = length 384, hash E801184A sample 40: - time = 1888567 + time = 1888568 flags = 1 data = length 384, hash 53C6CF9C sample 41: - time = 1912567 + time = 1912568 flags = 1 data = length 384, hash 19A8D99F sample 42: - time = 1936567 + time = 1936568 flags = 1 data = length 384, hash E47EB43F sample 43: - time = 1960567 + time = 1960568 flags = 1 data = length 384, hash 4EA329E7 sample 44: - time = 1984567 + time = 1984568 flags = 1 data = length 384, hash 1CCAAE62 sample 45: - time = 2008567 + time = 2008568 flags = 1 data = length 384, hash ED3F8C66 sample 46: - time = 2032567 + time = 2032568 flags = 1 data = length 384, hash D3D646B6 sample 47: - time = 2056567 + time = 2056568 flags = 1 data = length 384, hash 68CD1574 sample 48: - time = 2080567 + time = 2080568 flags = 1 data = length 384, hash 8CEAB382 sample 49: - time = 2104567 + time = 2104568 flags = 1 data = length 384, hash D54B1C48 sample 50: - time = 2128567 + time = 2128568 flags = 1 data = length 384, hash FFE2EE90 sample 51: - time = 2152567 + time = 2152568 flags = 1 data = length 384, hash BFE8A673 sample 52: - time = 2176567 + time = 2176568 flags = 1 data = length 384, hash 978B1C92 sample 53: - time = 2200567 + time = 2200568 flags = 1 data = length 384, hash 810CC71E sample 54: - time = 2224567 + time = 2224568 flags = 1 data = length 384, hash 44FE42D9 sample 55: - time = 2248567 + time = 2248568 flags = 1 data = length 384, hash 2F5BB02C sample 56: - time = 2272567 + time = 2272568 flags = 1 data = length 384, hash 77DDB90 sample 57: - time = 2296567 + time = 2296568 flags = 1 data = length 384, hash 24FB5EDA sample 58: - time = 2320567 + time = 2320568 flags = 1 data = length 384, hash E73203C6 sample 59: - time = 2344567 + time = 2344568 flags = 1 data = length 384, hash 14B525F1 sample 60: - time = 2368567 + time = 2368568 flags = 1 data = length 384, hash 5E0F4E2E sample 61: - time = 2392567 + time = 2392568 flags = 1 data = length 384, hash 67EE4E31 sample 62: - time = 2416567 + time = 2416568 flags = 1 data = length 384, hash 2E04EC4C sample 63: - time = 2440567 + time = 2440568 flags = 1 data = length 384, hash 852CABA7 sample 64: - time = 2464567 + time = 2464568 flags = 1 data = length 384, hash 19928903 sample 65: - time = 2488567 + time = 2488568 flags = 1 data = length 384, hash 5DA42021 sample 66: - time = 2512567 + time = 2512568 flags = 1 data = length 384, hash 45B20B7C sample 67: - time = 2536567 + time = 2536568 flags = 1 data = length 384, hash D108A215 sample 68: - time = 2560567 + time = 2560568 flags = 1 data = length 384, hash BD25DB7C sample 69: - time = 2584567 + time = 2584568 flags = 1 data = length 384, hash DA7F9861 sample 70: - time = 2608567 + time = 2608568 flags = 1 data = length 384, hash CCD576F sample 71: - time = 2632567 + time = 2632568 flags = 1 data = length 384, hash 405C1EB5 sample 72: - time = 2656567 + time = 2656568 flags = 1 data = length 384, hash 6640B74E sample 73: - time = 2680567 + time = 2680568 flags = 1 data = length 384, hash B4E5937A sample 74: - time = 2704567 + time = 2704568 flags = 1 data = length 384, hash CEE17733 sample 75: - time = 2728567 + time = 2728568 flags = 1 data = length 384, hash 2A0DA733 sample 76: - time = 2752567 + time = 2752568 flags = 1 data = length 384, hash 97F4129B tracksEnded = true From 23cc102151fbfd1c18e10e7ec7b6f448a1094a5b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 09:08:11 -0800 Subject: [PATCH 150/417] Move internal HlsSampleStreamWrapper methods under internal methods ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177462449 --- .../source/hls/HlsSampleStreamWrapper.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) 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 6cb3f854c8..8f063a38f1 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 @@ -410,24 +410,6 @@ import java.util.Arrays; } } - private boolean finishedReadingChunk(HlsMediaChunk chunk) { - int chunkUid = chunk.uid; - int sampleQueueCount = sampleQueues.length; - for (int i = 0; i < sampleQueueCount; i++) { - if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { - return false; - } - } - return true; - } - - private void resetSampleQueues() { - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(pendingResetUpstreamFormats); - } - pendingResetUpstreamFormats = false; - } - // SequenceableLoader implementation @Override @@ -650,6 +632,24 @@ import java.util.Arrays; // Internal methods. + private boolean finishedReadingChunk(HlsMediaChunk chunk) { + int chunkUid = chunk.uid; + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { + return false; + } + } + return true; + } + + private void resetSampleQueues() { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(pendingResetUpstreamFormats); + } + pendingResetUpstreamFormats = false; + } + private void maybeFinishPrepare() { if (released || prepared || !sampleQueuesBuilt) { return; From 80fff0b7ceba2835cf103f52c16ee4cbcbf4ba3c Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 1 Dec 2017 03:33:49 -0800 Subject: [PATCH 151/417] Discard buffer in FakeExoPlayer. This is in line with a recent change in ExoPlayerImplInternal. Not discarding the buffer causes OOM when running simulated playbacks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177573930 --- .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 8 ++++++++ .../android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 1 + 2 files changed, 9 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 3dcf551943..a4c9abb24e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -84,6 +84,14 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod return returnPositionUs; } + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + super.discardBuffer(positionUs, toKeyframe); + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardBuffer(positionUs, toKeyframe); + } + } + @Override public long getBufferedPositionUs() { super.getBufferedPositionUs(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 0358e5d980..1e7e0cd933 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -364,6 +364,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { public void run() { try { maybeContinueLoading(); + mediaPeriod.discardBuffer(rendererPositionUs, /* toKeyframe= */ false); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; if (playbackState == Player.STATE_READY) { From 03b0d9d46c48ff4ef03928ca6cb4cc6fe571224d Mon Sep 17 00:00:00 2001 From: tonihei Date: Sun, 3 Dec 2017 11:25:51 -0800 Subject: [PATCH 152/417] Fix flaky testEmptyTimeline again. Waiting for the timeline change didn't work correctly because the timeline was already equal to Timeline.EMPTY (due to the masking). Now waiting explicitly for the empty Timeline exposed by the source. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177749292 --- .../java/com/google/android/exoplayer2/ExoPlayerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2443f8b892..714dfff676 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -54,7 +54,7 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - Timeline timeline = Timeline.EMPTY; + Timeline timeline = new FakeTimeline(/* windowCount= */ 0); FakeRenderer renderer = new FakeRenderer(); // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline // update happens after the transition to STATE_ENDED and the test runner may already have been From fbccdf594a589abd01d0b0e6fc5dca7229b66b8f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 01:18:58 -0800 Subject: [PATCH 153/417] Use AdaptiveMediaSourceEventListener for ExtractorMediaSource This is a step towards harmonizing the MediaSource Builders and (potentially) providing MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177783157 --- RELEASENOTES.md | 2 + .../android/exoplayer2/demo/EventLogger.java | 24 +- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 14 +- .../AdaptiveMediaSourceEventListener.java | 305 +---------- .../source/ExtractorMediaPeriod.java | 62 ++- .../source/ExtractorMediaSource.java | 159 +++++- .../source/MediaSourceEventListener.java | 487 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 81 ++- .../android/exoplayer2/upstream/DataSpec.java | 19 +- 9 files changed, 753 insertions(+), 400 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3a42311b26..f6adf560b9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ * Add optional parameter to `Player.stop` to reset the player when stopping. * Fix handling of playback parameters changes while paused when followed by a seek. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 68a10343e6..0635944640 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -53,13 +52,15 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; -/** - * Logs player events using {@link Log}. - */ -/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, - DefaultDrmSessionManager.EventListener { +/** Logs player events using {@link Log}. */ +/* package */ final class EventLogger + implements Player.EventListener, + MetadataOutput, + AudioRendererEventListener, + VideoRendererEventListener, + AdaptiveMediaSourceEventListener, + AdsMediaSource.EventListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -324,13 +325,6 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // ExtractorMediaSource.EventListener - - @Override - public void onLoadError(IOException error) { - printInternalError("loadError", error); - } - // AdaptiveMediaSourceEventListener @Override diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 02aa4807a5..cd646daf42 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -62,9 +62,13 @@ public final class ImaAdsMediaSource implements MediaSource { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsMediaSource.AdsListener eventListener) { + public ImaAdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + ImaAdsLoader imaAdsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable AdsMediaSource.EventListener eventListener) { adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, eventHandler, eventListener); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index be07cbb5dc..2bc9d48726 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -16,306 +16,39 @@ package com.google.android.exoplayer2.source; import android.os.Handler; -import android.os.SystemClock; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; +import android.support.annotation.Nullable; /** - * Interface for callbacks to be notified of adaptive {@link MediaSource} events. + * Interface for callbacks to be notified of {@link MediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener} */ -public interface AdaptiveMediaSourceEventListener { +@Deprecated +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - /** - * Called when a load begins. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. - */ - void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs); - - /** - * Called when a load ends. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. - * @param bytesLoaded The number of bytes that were loaded. - */ - void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load is canceled. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was - * canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. - * @param bytesLoaded The number of bytes that were loaded prior to cancelation. - */ - void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load error occurs. - *

      - * The error may or may not have resulted in the load being canceled, as indicated by the - * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will - * not be called in addition to this method. - *

      - * This method being called does not indicate that playback has failed, or that it will fail. The - * player may be able to recover from the error and continue. Hence applications should - * not implement this method to display a user visible error or initiate an application - * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement - * such behavior). This method is called to provide the application with an opportunity to log the - * error if it wishes to do so. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error - * occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. - * @param bytesLoaded The number of bytes that were loaded prior to the error. - * @param error The load error. - * @param wasCanceled Whether the load was canceled as a result of the error. - */ - void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled); - - /** - * Called when data is removed from the back of a media buffer, typically so that it can be - * re-buffered in a different format. - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param mediaStartTimeMs The start time of the media being discarded. - * @param mediaEndTimeMs The end time of the media being discarded. - */ - void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); - - /** - * Called when a downstream format change occurs (i.e. when the format of the media being read - * from one or more {@link SampleStream}s provided by the source changes). - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaTimeMs The media time at which the change occurred. - */ - void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs); - - /** - * Dispatches events to a {@link AdaptiveMediaSourceEventListener}. - */ - final class EventDispatcher { + /** Dispatches events to a {@link MediaSourceEventListener}. */ + final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { private final Handler handler; - private final AdaptiveMediaSourceEventListener listener; - private final long mediaTimeOffsetMs; + private final MediaSourceEventListener listener; - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) { + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { this(handler, listener, 0); } - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener, + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, long mediaTimeOffsetMs) { - this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + super(handler, listener, mediaTimeOffsetMs); + this.handler = handler; this.listener = listener; - this.mediaTimeOffsetMs = mediaTimeOffsetMs; } - public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { - return new EventDispatcher(handler, listener, mediaTimeOffsetMs); - } - - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { - loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs); - } - - public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs); - } - }); - } - } - - public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { - loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - - public void loadError(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded, final IOException error, - final boolean wasCanceled) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - }); - } - } - - public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs, - final long mediaEndTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs)); - } - }); - } - } - - public void downstreamFormatChanged(final int trackType, final Format trackFormat, - final int trackSelectionReason, final Object trackSelectionData, - final long mediaTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaTimeUs)); - } - }); - } - } - - private long adjustMediaTime(long mediaTimeUs) { - long mediaTimeMs = C.usToMs(mediaTimeUs); - return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( + long mediaTimeOffsetMs) { + return new AdaptiveMediaSourceEventListener.EventDispatcher( + handler, listener, mediaTimeOffsetMs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 900ba5bd37..c0586b3a28 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -74,11 +76,10 @@ import java.util.Arrays; private final Uri uri; private final DataSource dataSource; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final ExtractorMediaSource.EventListener eventListener; + private final EventDispatcher eventDispatcher; private final Listener listener; private final Allocator allocator; - private final String customCacheKey; + @Nullable private final String customCacheKey; private final long continueLoadingCheckIntervalBytes; private final Loader loader; private final ExtractorHolder extractorHolder; @@ -117,8 +118,7 @@ import java.util.Arrays; * @param dataSource The data source to read the media. * @param extractors The extractors to use to read the data source. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventDispatcher A dispatcher to notify of events. * @param listener A listener to notify when information about the period changes. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache @@ -126,15 +126,20 @@ import java.util.Arrays; * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. */ - public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, - int minLoadableRetryCount, Handler eventHandler, - ExtractorMediaSource.EventListener eventListener, Listener listener, - Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaPeriod( + Uri uri, + DataSource dataSource, + Extractor[] extractors, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + Listener listener, + Allocator allocator, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = eventDispatcher; this.listener = listener; this.allocator = allocator; this.customCacheKey = customCacheKey; @@ -430,8 +435,22 @@ import java.util.Arrays; public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { copyLengthFromLoader(loadable); - notifyLoadError(error); - if (isLoadableExceptionFatal(error)) { + boolean isErrorFatal = isLoadableExceptionFatal(error); + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded, + error, + /* wasCanceled= */ isErrorFatal); + if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } int extractedSamplesCount = getExtractedSamplesCount(); @@ -606,17 +625,6 @@ import java.util.Arrays; return e instanceof UnrecognizedInputFormatException; } - private void notifyLoadError(final IOException error) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(error); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private final int track; @@ -663,7 +671,9 @@ import java.util.Arrays; private boolean pendingExtractorSeek; private long seekTimeUs; + private DataSpec dataSpec; private long length; + private long bytesLoaded; public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, ConditionVariable loadCondition) { @@ -699,7 +709,8 @@ import java.util.Arrays; ExtractorInput input = null; try { long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey)); + dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey); + length = dataSource.open(dataSpec); if (length != C.LENGTH_UNSET) { length += position; } @@ -723,6 +734,7 @@ import java.util.Arrays; result = Extractor.RESULT_CONTINUE; } else if (input != null) { positionHolder.position = input.getPosition(); + bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition; } Util.closeQuietly(dataSource); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 351416df6a..4ea20e242e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -17,14 +17,18 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -40,10 +44,12 @@ import java.io.IOException; * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { - /** * Listener of {@link ExtractorMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -89,8 +95,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; + private final EventDispatcher eventDispatcher; private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -108,9 +113,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private ExtractorsFactory extractorsFactory; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private String customCacheKey; + @Nullable private Handler eventHandler; + @Nullable private MediaSourceEventListener eventListener; + @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; private boolean isBuildCalled; @@ -187,8 +192,24 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventHandler A handler for events. * @param eventListener A listener of events. * @return This builder. + * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. */ + @Deprecated public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -270,12 +291,31 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + this( + uri, + dataSourceFactory, + extractorsFactory, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener), + customCacheKey, + continueLoadingCheckIntervalBytes); + } + + private ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; } @@ -294,9 +334,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), - extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); + return new ExtractorMediaPeriod( + uri, + dataSourceFactory.createDataSource(), + extractorsFactory.createExtractors(), + minLoadableRetryCount, + eventDispatcher, + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); } @Override @@ -332,4 +379,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null); } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + private final EventListener eventListener; + + public EventListenerWrapper(EventListener eventListener) { + this.eventListener = Assertions.checkNotNull(eventListener); + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java new file mode 100644 index 0000000000..82e8781d70 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2017 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; + +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** Interface for callbacks to be notified of {@link MediaSource} events. */ +public interface MediaSourceEventListener { + /** + * Called when a load begins. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. + */ + void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs); + + /** + * Called when a load ends. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. + * @param loadDurationMs The duration of the load. + * @param bytesLoaded The number of bytes that were loaded. + */ + void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load is canceled. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was + * canceled. + * @param loadDurationMs The duration of the load up to the point at which it was canceled. + * @param bytesLoaded The number of bytes that were loaded prior to cancelation. + */ + void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load error occurs. + * + *

      The error may or may not have resulted in the load being canceled, as indicated by the + * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will + * not be called in addition to this method. + * + *

      This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error + * occurred. + * @param loadDurationMs The duration of the load up to the point at which the error occurred. + * @param bytesLoaded The number of bytes that were loaded prior to the error. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled); + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param mediaStartTimeMs The start time of the media being discarded. + * @param mediaEndTimeMs The end time of the media being discarded. + */ + void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); + + /** + * Called when a downstream format change occurs (i.e. when the format of the media being read + * from one or more {@link SampleStream}s provided by the source changes). + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaTimeMs The media time at which the change occurred. + */ + void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs); + + /** Dispatches events to a {@link MediaSourceEventListener}. */ + class EventDispatcher { + + @Nullable private final Handler handler; + @Nullable private final MediaSourceEventListener listener; + private final long mediaTimeOffsetMs; + + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + this(handler, listener, 0); + } + + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, + long mediaTimeOffsetMs) { + this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + this.listener = listener; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + loadStarted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs); + } + + public void loadStarted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadStarted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs); + } + }); + } + } + + public void loadCompleted( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCompleted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCompleted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadCanceled( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCanceled( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCanceled( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadError( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + + public void loadError( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded, + final IOException error, + final boolean wasCanceled) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadError( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + }); + } + } + + public void upstreamDiscarded( + final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onUpstreamDiscarded( + trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs)); + } + }); + } + } + + public void downstreamFormatChanged( + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onDownstreamFormatChanged( + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaTimeUs)); + } + }); + } + } + + private long adjustMediaTime(long mediaTimeUs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + } +} 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 47a2540c38..54a8fd96ae 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 @@ -26,9 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; @@ -44,10 +44,8 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { - /** - * Listener for events relating to ad loading. - */ - public interface AdsListener { + /** Listener for ads media source events. */ + public interface EventListener extends MediaSourceEventListener { /** * Called if there was an error loading ads. The media source will load the content without ads @@ -75,15 +73,13 @@ public final class AdsMediaSource implements MediaSource { private final MediaSource contentMediaSource; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; + @Nullable private final Handler eventHandler; + @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; private final AdMediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; - @Nullable - private final Handler eventHandler; - @Nullable - private final AdsListener eventListener; private Handler playerHandler; private ExoPlayer player; @@ -115,10 +111,10 @@ public final class AdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

      - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + *

      Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. @@ -128,9 +124,13 @@ public final class AdsMediaSource implements MediaSource { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsListener eventListener) { + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; @@ -186,7 +186,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); + adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -306,11 +306,8 @@ public final class AdsMediaSource implements MediaSource { } } - /** - * Listener for component events. All methods are called on the main thread. - */ - private final class ComponentListener implements AdsLoader.EventListener, - AdMediaSourceLoadErrorListener { + /** Listener for component events. All methods are called on the main thread. */ + private final class ComponentListener implements AdsLoader.EventListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -374,20 +371,6 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Listener for errors while loading an ad {@link MediaSource}. - */ - private interface AdMediaSourceLoadErrorListener { - - /** - * Called when an error occurs loading media data. - * - * @param error The load error. - */ - void onLoadError(IOException error); - - } - /** * Factory for {@link MediaSource}s for loading ad media. */ @@ -397,15 +380,13 @@ public final class AdsMediaSource implements MediaSource { * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. * * @param uri The URI of the ad. - * @param handler A handler for listener events. - * @param listener A listener for ad load errors. To have ad media source load errors notified - * via the ads media source's listener, call this listener's onLoadError method from your - * new media source's load error listener using the specified {@code handler}. Otherwise, - * this parameter can be ignored. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. * @return The new media source. */ - MediaSource createAdMediaSource(Uri uri, Handler handler, - AdMediaSourceLoadErrorListener listener); + MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); /** * Returns the content types supported by media sources created by this factory. Each element @@ -427,15 +408,11 @@ public final class AdsMediaSource implements MediaSource { } @Override - public MediaSource createAdMediaSource(Uri uri, Handler handler, - final AdMediaSourceLoadErrorListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, - new EventListener() { - @Override - public void onLoadError(IOException error) { - listener.onLoadError(error); - } - }).build(); + public MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory) + .setEventListener(handler, listener) + .build(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index ab1542c7a6..cbe971bc5d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Retention; @@ -79,7 +80,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * {@link DataSpec} is not intended to be used in conjunction with a cache. */ - public final String key; + @Nullable public final String key; /** * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. @@ -113,7 +114,7 @@ public final class DataSpec { * @param length {@link #length}. * @param key {@link #key}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); } @@ -147,8 +148,8 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} may differ from - * {@link #absoluteStreamPosition}. + * Construct a {@link DataSpec} where {@link #position} may differ from {@link + * #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param postBody {@link #postBody}. @@ -158,8 +159,14 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, - String key, @Flags int flags) { + public DataSpec( + Uri uri, + byte[] postBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); From fd938fb4545dddb9016c80cb126f4fa6f9d51d43 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 02:06:42 -0800 Subject: [PATCH 154/417] Update internal usages of deprecated AdaptiveMediaSourceEventListener ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177786580 --- .../android/exoplayer2/demo/EventLogger.java | 21 +++-- .../AdaptiveMediaSourceEventListener.java | 36 +-------- .../source/ExtractorMediaSource.java | 2 +- .../source/MediaSourceEventListener.java | 6 +- .../source/chunk/ChunkSampleStream.java | 3 +- .../source/dash/DashMediaPeriod.java | 2 +- .../source/dash/DashMediaSource.java | 80 ++++++++++++------- .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 64 +++++++++------ .../source/hls/HlsSampleStreamWrapper.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 2 +- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 68 ++++++++++------ .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../testutil/FakeAdaptiveMediaSource.java | 14 ++-- 15 files changed, 170 insertions(+), 136 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 0635944640..fa22130eea 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -58,7 +58,7 @@ import java.util.Locale; MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, - AdaptiveMediaSourceEventListener, + MediaSourceEventListener, AdsMediaSource.EventListener, DefaultDrmSessionManager.EventListener { @@ -325,12 +325,19 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // AdaptiveMediaSourceEventListener + // MediaSourceEventListener @Override - public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs) { + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { // Do nothing. } @@ -371,7 +378,7 @@ import java.util.Locale; @Override public void onAdLoadError(IOException error) { - printInternalError("loadError", error); + printInternalError("adLoadError", error); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index 2bc9d48726..ccc3beac55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -15,42 +15,10 @@ */ package com.google.android.exoplayer2.source; -import android.os.Handler; -import android.support.annotation.Nullable; - /** * Interface for callbacks to be notified of {@link MediaSource} events. * - * @deprecated Use {@link MediaSourceEventListener} + * @deprecated Use {@link MediaSourceEventListener}. */ @Deprecated -public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - - /** Dispatches events to a {@link MediaSourceEventListener}. */ - final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { - - private final Handler handler; - private final MediaSourceEventListener listener; - - public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - this(handler, listener, 0); - } - - public EventDispatcher( - @Nullable Handler handler, - @Nullable MediaSourceEventListener listener, - long mediaTimeOffsetMs) { - super(handler, listener, mediaTimeOffsetMs); - this.handler = handler; - this.listener = listener; - } - - public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( - long mediaTimeOffsetMs) { - return new AdaptiveMediaSourceEventListener.EventDispatcher( - handler, listener, mediaTimeOffsetMs); - } - - } - -} +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 4ea20e242e..b97d957ec4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 82e8781d70..4d500f94bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -211,7 +211,7 @@ public interface MediaSourceEventListener { long mediaTimeMs); /** Dispatches events to a {@link MediaSourceEventListener}. */ - class EventDispatcher { + final class EventDispatcher { @Nullable private final Handler handler; @Nullable private final MediaSourceEventListener listener; @@ -230,6 +230,10 @@ public interface MediaSourceEventListener { this.mediaTimeOffsetMs = mediaTimeOffsetMs; } + public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { + return new EventDispatcher(handler, listener, mediaTimeOffsetMs); + } + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, 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 e352ba551e..20b56e7807 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 @@ -16,12 +16,11 @@ package com.google.android.exoplayer2.source.chunk; import android.util.Log; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; 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 70fba4dd00..8fe10e94ee 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 @@ -20,10 +20,10 @@ import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; 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 2562b27237..11f2c68698 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 @@ -26,12 +26,12 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; @@ -72,7 +72,7 @@ public final class DashMediaSource implements MediaSource { private final DashChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -155,8 +155,7 @@ public final class DashMediaSource implements MediaSource { * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -284,8 +283,11 @@ public final class DashMediaSource implements MediaSource { * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + public DashMediaSource( + DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -301,9 +303,12 @@ public final class DashMediaSource implements MediaSource { * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener - eventListener) { + public DashMediaSource( + DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -322,9 +327,12 @@ public final class DashMediaSource implements MediaSource { * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public DashMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -340,18 +348,22 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public DashMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -367,31 +379,39 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + public DashMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } - private DashMediaSource(DashManifest manifest, Uri manifestUri, + private DashMediaSource( + DashManifest manifest, + Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this.manifest = manifest; this.manifestUri = manifestUri; this.manifestDataSourceFactory = manifestDataSourceFactory; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index b6c74d61bb..dd596878d2 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -19,9 +19,9 @@ import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; 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 a412b8c3e9..4e5783698a 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 @@ -21,12 +21,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -60,7 +60,7 @@ public final class HlsMediaSource implements MediaSource, private HlsExtractorFactory extractorFactory; private ParsingLoadable.Parser playlistParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -136,8 +136,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -219,13 +218,16 @@ public final class HlsMediaSource implements MediaSource, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public HlsMediaSource( + Uri manifestUri, + DataSource.Factory dataSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -234,17 +236,20 @@ public final class HlsMediaSource implements MediaSource, * @param manifestUri The {@link Uri} of the HLS manifest. * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. + * @param minLoadableRetryCount The minimum number of times loads must be retried before errors + * are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public HlsMediaSource( + Uri manifestUri, + DataSource.Factory dataSourceFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); @@ -255,29 +260,36 @@ public final class HlsMediaSource implements MediaSource, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments. - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. + * @param minLoadableRetryCount The minimum number of times loads must be retried before errors + * are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @deprecated Use {@link Builder} instead. */ @Deprecated - public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + public HlsMediaSource( + Uri manifestUri, + HlsDataSourceFactory dataSourceFactory, + HlsExtractorFactory extractorFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this(manifestUri, dataSourceFactory, extractorFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler, eventListener, playlistParser); } - private HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + private HlsMediaSource( + Uri manifestUri, + HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; 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 8f063a38f1..f4ba9a6eac 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 @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleStream; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 355a8575ca..0677ff7ca0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -20,7 +20,7 @@ import android.os.Handler; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index c079a36d62..d418a21dff 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; 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 a4b601aafe..10772ba36c 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 @@ -24,12 +24,12 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; @@ -66,7 +66,7 @@ public final class SsMediaSource implements MediaSource, private final SsChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -147,8 +147,7 @@ public final class SsMediaSource implements MediaSource, * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -256,8 +255,11 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -273,9 +275,12 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -294,9 +299,12 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -318,10 +326,14 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -343,23 +355,31 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + public SsMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } - private SsMediaSource(SsManifest manifest, Uri manifestUri, + private SsMediaSource( + SsManifest manifest, + Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index a4c9abb24e..ff2a9b23cd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.testutil; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroupArray; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 59bcaf3e7c..fbb2a83027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; @@ -33,9 +33,13 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { private final EventDispatcher eventDispatcher; private final FakeChunkSource.Factory chunkSourceFactory; - public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + public FakeAdaptiveMediaSource( + Timeline timeline, + Object manifest, + TrackGroupArray trackGroupArray, + Handler eventHandler, + MediaSourceEventListener eventListener, + FakeChunkSource.Factory chunkSourceFactory) { super(timeline, manifest, trackGroupArray); this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.chunkSourceFactory = chunkSourceFactory; From a9c3ca1cfe0fd9295a6472265a9ac71b7f8c64bd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:08:33 -0800 Subject: [PATCH 155/417] Tentative fix for roll-up row count Issue: #3513 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177804505 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f6adf560b9..2335f8f15e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ seek. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index e2c592be6b..0483f909b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; /** @@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder { private final ParsableByteArray ccData; private final int packetLength; private final int selectedField; - private final LinkedList cueBuilders; + private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; private List cues; @@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder { public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - cueBuilders = new LinkedList<>(); + cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { @@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder { cues = null; lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder { private void handleMiscCode(byte cc2) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: - captionRowCount = 2; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(2); return; case CTRL_ROLL_UP_CAPTIONS_3_ROWS: - captionRowCount = 3; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(3); return; case CTRL_ROLL_UP_CAPTIONS_4_ROWS: - captionRowCount = 4; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(4); return; case CTRL_RESUME_CAPTION_LOADING: setCaptionMode(CC_MODE_POP_ON); @@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_RESUME_DIRECT_CAPTIONING: setCaptionMode(CC_MODE_PAINT_ON); return; + default: + // Fall through. + break; } if (captionMode == CC_MODE_UNKNOWN) { @@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_DELETE_TO_END_OF_ROW: // TODO: implement break; + default: + // Fall through. + break; } } @@ -515,8 +520,13 @@ public final class Cea608Decoder extends CeaDecoder { } } + private void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + currentCueBuilder.setCaptionRowCount(captionRowCount); + } + private void resetCueBuilders() { - currentCueBuilder.reset(captionMode, captionRowCount); + currentCueBuilder.reset(captionMode); cueBuilders.clear(); cueBuilders.add(currentCueBuilder); } @@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder { public CueBuilder(int captionMode, int captionRowCount) { preambleStyles = new ArrayList<>(); midrowStyles = new ArrayList<>(); - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); - reset(captionMode, captionRowCount); + reset(captionMode); + setCaptionRowCount(captionRowCount); } - public void reset(int captionMode, int captionRowCount) { + public void reset(int captionMode) { + this.captionMode = captionMode; preambleStyles.clear(); midrowStyles.clear(); rolledUpCaptions.clear(); @@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder { row = BASE_ROW; indent = 0; tabOffset = 0; - this.captionMode = captionMode; - this.captionRowCount = captionRowCount; underlineStartPosition = POSITION_UNSET; } + public void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + } + public boolean isEmpty() { return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; From 9c63d377917b45ca9f37f0c41516dbf9f7fffde8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:56:04 -0800 Subject: [PATCH 156/417] Support timezone offsets in ISO8601 timestamps Issue: #3524 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177808106 --- RELEASENOTES.md | 2 + .../source/dash/DashMediaSource.java | 56 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2335f8f15e..0372191683 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ `DashMediaSource`, `SingleSampleMediaSource`. * DASH: * Support in-MPD EventStream. + * Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by custom `LoadControl` implementations. 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 11f2c68698..af1a445b9f 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 @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -51,6 +52,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A DASH {@link MediaSource}. @@ -979,41 +982,42 @@ public final class DashMediaSource implements MediaSource { } - private static final class Iso8601Parser implements ParsingLoadable.Parser { + /* package */ static final class Iso8601Parser implements ParsingLoadable.Parser { - private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; + private static final Pattern TIMESTAMP_WITH_TIMEZONE_PATTERN = + Pattern.compile("(.+?)(Z|((\\+|-|−)(\\d\\d)(:?(\\d\\d))?))"); @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - - if (firstLine != null) { - //determine format pattern - String formatPattern; - if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; - } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + try { + Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); + if (!matcher.matches()) { + throw new ParserException("Couldn't parse timestamp: " + firstLine); + } + // Parse the timestamp. + String timestampWithoutTimezone = matcher.group(1); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + long timestampMs = format.parse(timestampWithoutTimezone).getTime(); + // Parse the timezone. + String timezone = matcher.group(2); + if ("Z".equals(timezone)) { + // UTC (no offset). } else { - formatPattern = ISO_8601_FORMAT; + long sign = "+".equals(matcher.group(4)) ? 1 : -1; + long hours = Long.parseLong(matcher.group(5)); + String minutesString = matcher.group(7); + long minutes = TextUtils.isEmpty(minutesString) ? 0 : Long.parseLong(minutesString); + long timestampOffsetMs = sign * (((hours * 60) + minutes) * 60 * 1000); + timestampMs -= timestampOffsetMs; } - //parse - try { - SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); - } - - } else { - throw new ParserException("Unable to parse ISO 8601. Input value is null"); + return timestampMs; + } catch (ParseException e) { + throw new ParserException(e); } } } - + } From 7792f667d4474da3c13f4cf29baf69655d22ff90 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 07:29:56 -0800 Subject: [PATCH 157/417] Fix setting supported ad MIME types without preloading ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177810991 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 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 4bf88fe18f..743a428020 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 @@ -315,20 +315,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - if (ENABLE_PRELOADING) { - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with preloading"); - } - } else { - adsManager.init(); - if (DEBUG) { - Log.d(TAG, "Initialized without preloading"); - } + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + adsManager.init(adsRenderingSettings); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); adPlaybackState = new AdPlaybackState(adGroupTimesUs); From bb0fae3ee8243ecd4633b07e5469ad47f251e5c6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 07:35:50 -0800 Subject: [PATCH 158/417] Fix playback of FLV live streams with no audio track Issue: #3188 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177811487 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/flv/FlvExtractor.java | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0372191683..9d98f2aae0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ * Add optional parameter to `Player.stop` to reset the player when stopping. * Fix handling of playback parameters changes while paused when followed by a seek. +* Fix playback of live FLV streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. * CEA-608: Fix handling of row count changes in roll-up mode diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 2da075ff53..d908f28945 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -78,6 +78,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; @@ -93,6 +94,7 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -134,6 +136,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; bytesToNextTagHeader = 0; } @@ -255,11 +258,11 @@ public final class FlvExtractor implements Extractor { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { - ensureOutputSeekMap(); - audioReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { - ensureOutputSeekMap(); - videoReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); long durationUs = metadataReader.getDurationUs(); @@ -288,11 +291,15 @@ public final class FlvExtractor implements Extractor { return tagData; } - private void ensureOutputSeekMap() { + private void ensureReadyForMediaOutput() { if (!outputSeekMap) { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } + if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } } } From fbfa43f5a3ef11f5da115c439092b521564fb4fa Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 08:12:35 -0800 Subject: [PATCH 159/417] Enhance SeekMaps to return SeekPoints Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177814974 --- .../src/androidTest/assets/bear.flac.0.dump | 2 +- .../src/androidTest/assets/bear.flac.1.dump | 2 +- .../src/androidTest/assets/bear.flac.2.dump | 2 +- .../src/androidTest/assets/bear.flac.3.dump | 2 +- .../exoplayer2/ext/flac/FlacExtractor.java | 52 ++++--- .../androidTest/assets/flv/sample.flv.0.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.0.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.1.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.2.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.3.dump | 2 +- .../subsample_encrypted_altref.webm.0.dump | 2 +- .../subsample_encrypted_noaltref.webm.0.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.0.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.1.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.2.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.3.dump | 2 +- .../assets/mp3/play-trimmed.mp3.0.dump | 2 +- .../assets/mp3/play-trimmed.mp3.1.dump | 2 +- .../assets/mp3/play-trimmed.mp3.2.dump | 2 +- .../assets/mp3/play-trimmed.mp3.3.dump | 2 +- .../assets/mp3/play-trimmed.mp3.unklen.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.0.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.1.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.2.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.3.dump | 2 +- .../assets/mp4/sample_fragmented.mp4.0.dump | 2 +- .../mp4/sample_fragmented_sei.mp4.0.dump | 2 +- .../androidTest/assets/ogg/bear.opus.0.dump | 2 +- .../androidTest/assets/ogg/bear.opus.1.dump | 2 +- .../androidTest/assets/ogg/bear.opus.2.dump | 2 +- .../androidTest/assets/ogg/bear.opus.3.dump | 2 +- .../assets/ogg/bear.opus.unklen.dump | 2 +- .../assets/ogg/bear_flac.ogg.0.dump | 2 +- .../assets/ogg/bear_flac.ogg.1.dump | 2 +- .../assets/ogg/bear_flac.ogg.2.dump | 2 +- .../assets/ogg/bear_flac.ogg.3.dump | 2 +- .../assets/ogg/bear_flac.ogg.unklen.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.0.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.1.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.2.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.3.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.unklen.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.0.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.1.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.2.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.3.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.unklen.dump | 2 +- .../assets/rawcc/sample.rawcc.0.dump | 2 +- .../androidTest/assets/ts/sample.ac3.0.dump | 2 +- .../androidTest/assets/ts/sample.adts.0.dump | 2 +- .../androidTest/assets/ts/sample.ps.0.dump | 2 +- .../androidTest/assets/ts/sample.ts.0.dump | 2 +- .../androidTest/assets/wav/sample.wav.0.dump | 8 +- .../androidTest/assets/wav/sample.wav.1.dump | 6 +- .../androidTest/assets/wav/sample.wav.2.dump | 4 +- .../androidTest/assets/wav/sample.wav.3.dump | 4 +- .../exoplayer2/extractor/ChunkIndex.java | 11 +- .../android/exoplayer2/extractor/SeekMap.java | 91 ++++++++--- .../exoplayer2/extractor/SeekPoint.java | 62 ++++++++ .../extractor/mp3/ConstantBitrateSeeker.java | 18 ++- .../exoplayer2/extractor/mp3/VbriSeeker.java | 12 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 8 +- .../extractor/mp4/Mp4Extractor.java | 146 ++++++++++++++---- .../extractor/ogg/DefaultOggSeeker.java | 8 +- .../exoplayer2/extractor/ogg/FlacReader.java | 15 +- .../exoplayer2/extractor/wav/WavHeader.java | 19 ++- .../source/ExtractorMediaPeriod.java | 3 +- .../extractor/mp3/XingSeekerTest.java | 34 ++-- .../exoplayer2/testutil/ExtractorAsserts.java | 2 +- .../testutil/FakeExtractorOutput.java | 7 +- 70 files changed, 439 insertions(+), 173 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump index b03636f2bb..6908f5cc93 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump index 4e8388dba8..1414443187 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump index 0860c36cef..e343241650 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump index 6f7f72b806..95ab255bd0 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index a2f141a712..b630298c6e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor { } metadataParsed = true; - extractorOutput.seekMap(new SeekMap() { - final boolean isSeekable = decoderJni.getSeekPosition(0) != -1; - final long durationUs = streamInfo.durationUs(); - - @Override - public boolean isSeekable() { - return isSeekable; - } - - @Override - public long getPosition(long timeUs) { - return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0; - } - - @Override - public long getDurationUs() { - return durationUs; - } - - }); + boolean isSeekable = decoderJni.getSeekPosition(0) != -1; + extractorOutput.seekMap( + isSeekable + ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) + : new SeekMap.Unseekable(streamInfo.durationUs(), 0)); Format mediaFormat = Format.createAudioSampleFormat( null, @@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor { } } + private static final class FlacSeekMap implements SeekMap { + + private final long durationUs; + private final FlacDecoderJni decoderJni; + + public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) { + this.durationUs = durationUs; + this.decoderJni = decoderJni; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + // TODO: Access the seek table via JNI to return two seek points when appropriate. + return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs))); + } + + @Override + public long getDurationUs() { + return durationUs; + } + } } diff --git a/library/core/src/androidTest/assets/flv/sample.flv.0.dump b/library/core/src/androidTest/assets/flv/sample.flv.0.dump index b4129ecb88..7a4a74770c 100644 --- a/library/core/src/androidTest/assets/flv/sample.flv.0.dump +++ b/library/core/src/androidTest/assets/flv/sample.flv.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = 1136000 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 8: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump index 34bad9b82a..0f005ee5a9 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump index 546c934eff..378f5d7f2a 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump index ec84908172..80caf24a93 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump index ac8d9a2c1c..c9672ba9c4 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump index f533e14c3f..abc07dc503 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = 1000 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index d84c549dea..c43a43b576 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = 1000 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 1: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump index b66d263c84..eca3a6687d 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index a57894e81e..12abf149c4 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index 3f393e768e..3568616e76 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump index a2387eb887..8a31fe5e7d 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump index b75aefd91b..2c0ac67561 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump index be0a16681c..7cd3486505 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump index a759e4250a..fcf9402cba 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump index 59ee715255..5dbb6e1561 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump index a81a4189d9..bac707446d 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index 95f6528fd6..736e57693c 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 1828 + getPosition(0) = [[timeUs=0, position=1828]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index ebd33133e2..8186a2b9ce 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 1828 + getPosition(0) = [[timeUs=0, position=1828]] numberOfTracks = 3 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump index 8033ce8089..4d09067f3b 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump index f9aceae68a..821351e989 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump index f2f07f3e2f..3aea1e8d74 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump index 905055797c..b49af29f2c 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump index cd29da3e27..b2d5a9f3d2 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump index 5ba8cc29ae..572d1da891 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump index f698fd28cf..d53f257fd2 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump index 8d803d0bac..cdfd6efab8 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump index 09f6267270..9b029d3301 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump index 5ba8cc29ae..572d1da891 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump index 73e537f8c8..1c02c1bbef 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump index 3b7dc3fd1e..81d79b8674 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump index b6a6741fcc..f8b00bcb3a 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump index 738002f7ef..b020618488 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index a237fd0dfc..bf135434f4 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump index 8e2c5125a3..860e8a3b5b 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump index aa25303ac3..11afeb9665 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump index 58969058fa..f2f97ebcfa 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump index 4c789a8431..5d5f284cf2 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump index 2f163572bf..ee1176773e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump index 3e84813162..d430d1d8d4 100644 --- a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump +++ b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump index 1b6c77efb6..bedffcf198 100644 --- a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ts/sample.adts.0.dump b/library/core/src/androidTest/assets/ts/sample.adts.0.dump index 0a7427d3f1..a97cf860d1 100644 --- a/library/core/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.adts.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 98f3c6a85a..41db704d56 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 192: format: diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 91e48b1722..e900b94673 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 256: format: diff --git a/library/core/src/androidTest/assets/wav/sample.wav.0.dump b/library/core/src/androidTest/assets/wav/sample.wav.0.dump index 9ad01284b7..5d0f4d77f0 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.0.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,15 +27,15 @@ track 0: initializationData: sample count = 3 sample 0: - time = 884 + time = 0 flags = 1 data = length 32768, hash 9A8CEEBA sample 1: - time = 372403 + time = 371519 flags = 1 data = length 32768, hash C1717317 sample 2: - time = 743922 + time = 743038 flags = 1 data = length 22664, hash 819F5F62 tracksEnded = true diff --git a/library/core/src/androidTest/assets/wav/sample.wav.1.dump b/library/core/src/androidTest/assets/wav/sample.wav.1.dump index ca98cc5cf5..e59239bff8 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.1.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,11 +27,11 @@ track 0: initializationData: sample count = 2 sample 0: - time = 334195 + time = 333310 flags = 1 data = length 32768, hash 42D6E860 sample 1: - time = 705714 + time = 704829 flags = 1 data = length 26034, hash 62692C38 tracksEnded = true diff --git a/library/core/src/androidTest/assets/wav/sample.wav.2.dump b/library/core/src/androidTest/assets/wav/sample.wav.2.dump index da212b220a..c80a260385 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.2.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,7 +27,7 @@ track 0: initializationData: sample count = 1 sample 0: - time = 667528 + time = 666643 flags = 1 data = length 29402, hash 4241604E tracksEnded = true diff --git a/library/core/src/androidTest/assets/wav/sample.wav.3.dump b/library/core/src/androidTest/assets/wav/sample.wav.3.dump index 3275ba6ef5..9f25028923 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.3.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,7 +27,7 @@ track 0: initializationData: sample count = 1 sample 0: - time = 1000861 + time = 999977 flags = 1 data = length 2, hash 116 tracksEnded = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java index baa5589f4b..d0c66f930a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -91,8 +91,15 @@ public final class ChunkIndex implements SeekMap { } @Override - public long getPosition(long timeUs) { - return offsets[getChunkIndex(timeUs)]; + public SeekPoints getSeekPoints(long timeUs) { + int chunkIndex = getChunkIndex(timeUs); + SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]); + if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 964c43a45a..aa718c23e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -16,36 +16,36 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; /** * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream. */ public interface SeekMap { - /** - * A {@link SeekMap} that does not support seeking. - */ + /** A {@link SeekMap} that does not support seeking. */ final class Unseekable implements SeekMap { private final long durationUs; - private final long startPosition; + private final SeekPoints startSeekPoints; /** - * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if - * the duration is unknown. + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. */ public Unseekable(long durationUs) { this(durationUs, 0); } /** - * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if - * the duration is unknown. + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. * @param startPosition The position (byte offset) of the start of the media. */ public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; - this.startPosition = startPosition; + startSeekPoints = + new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition)); } @Override @@ -59,17 +59,58 @@ public interface SeekMap { } @Override - public long getPosition(long timeUs) { - return startPosition; + public SeekPoints getSeekPoints(long timeUs) { + return startSeekPoints; + } + } + + /** Contains one or two {@link SeekPoint}s. */ + final class SeekPoints { + + /** The first seek point. */ + public final SeekPoint first; + /** The second seek point, or {@link #first} if there's only one seek point. */ + public final SeekPoint second; + + /** @param point The single seek point. */ + public SeekPoints(SeekPoint point) { + this(point, point); } + /** + * @param first The first seek point. + * @param second The second seek point. + */ + public SeekPoints(SeekPoint first, SeekPoint second) { + this.first = Assertions.checkNotNull(first); + this.second = Assertions.checkNotNull(second); + } + + @Override + public String toString() { + return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoints other = (SeekPoints) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return (31 * first.hashCode()) + second.hashCode(); + } } /** * Returns whether seeking is supported. - *

      - * If seeking is not supported then the only valid seek position is the start of the file, and so - * {@link #getPosition(long)} will return 0 for all input values. * * @return Whether seeking is supported. */ @@ -78,20 +119,22 @@ public interface SeekMap { /** * Returns the duration of the stream in microseconds. * - * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the - * duration is unknown. + * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is + * unknown. */ long getDurationUs(); /** - * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream - * from which data can be provided to the extractor. + * Obtains seek points for the specified seek time in microseconds. The returned {@link + * SeekPoints} will contain one or two distinct seek points. * - * @param timeUs A seek position in microseconds. - * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor. If {@link #isSeekable()} returns false then the returned value will be - * independent of {@code timeUs}, and will indicate the start of the media in the stream. + *

      Two seek points [A, B] are returned in the case that seeking can only be performed to + * discrete points in time, there does not exist a seek point at exactly the requested time, and + * there exist seek points on both sides of it. In this case A and B are the closest seek points + * before and after the requested time. A single seek point is returned in all other cases. + * + * @param timeUs A seek time in microseconds. + * @return The corresponding seek points. */ - long getPosition(long timeUs); - + SeekPoints getSeekPoints(long timeUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java new file mode 100644 index 0000000000..93cfbd9200 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 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.extractor; + +/** Defines a seek point in a media stream. */ +public final class SeekPoint { + + /** A {@link SeekPoint} whose time and byte offset are both set to 0. */ + public static final SeekPoint START = new SeekPoint(0, 0); + + /** The time of the seek point, in microseconds. */ + public final long timeUs; + + /** The byte offset of the seek point. */ + public final long position; + + /** + * @param timeUs The time of the seek point, in microseconds. + * @param position The byte offset of the seek point. + */ + public SeekPoint(long timeUs, long position) { + this.timeUs = timeUs; + this.position = position; + } + + @Override + public String toString() { + return "[timeUs=" + timeUs + ", position=" + position + "]"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoint other = (SeekPoint) obj; + return timeUs == other.timeUs && position == other.position; + } + + @Override + public int hashCode() { + int result = (int) timeUs; + result = 31 * result + (int) position; + return result; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index 442e62deca..d358c0cae1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Util; /** @@ -57,16 +58,25 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (dataSize == C.LENGTH_UNSET) { - return firstFramePosition; + return new SeekPoints(new SeekPoint(0, firstFramePosition)); } long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / frameSize) * frameSize; positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); - // Add data start position. - return firstFramePosition + positionOffset; + long seekPosition = firstFramePosition + positionOffset; + long seekTimeUs = getTimeUs(seekPosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || positionOffset == dataSize - frameSize) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekPosition + frameSize; + long secondSeekTimeUs = getTimeUs(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index cc631d9f7e..f918b5c43d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -106,8 +107,15 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { - return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)]; + public SeekPoints getSeekPoints(long timeUs) { + int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true); + SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]); + if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } } @Override 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 e532249a64..a3bd5a2da2 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -107,10 +108,11 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (!isSeekable()) { - return dataStartPosition + xingFrameSize; + return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize)); } + timeUs = Util.constrainValue(timeUs, 0, durationUs); double percent = (timeUs * 100d) / durationUs; double scaledPosition; if (percent <= 0) { @@ -129,7 +131,7 @@ import com.google.android.exoplayer2.util.Util; long positionOffset = Math.round((scaledPosition / 256) * dataSize); // Ensure returned positions skip the frame containing the XING header. positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); - return dataStartPosition + positionOffset; + return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset)); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index f2412bf4ba..50fc0aec80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.metadata.Metadata; @@ -108,6 +109,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Extractor outputs. private ExtractorOutput extractorOutput; private Mp4Track[] tracks; + private int firstVideoTrackIndex; private long durationUs; private boolean isQuickTime; @@ -196,21 +198,56 @@ public final class Mp4Extractor implements Extractor, SeekMap { } @Override - public long getPosition(long timeUs) { - long earliestSamplePosition = Long.MAX_VALUE; - for (Mp4Track track : tracks) { - TrackSampleTable sampleTable = track.sampleTable; - int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + public SeekPoints getSeekPoints(long timeUs) { + if (tracks.length == 0) { + return new SeekPoints(SeekPoint.START); + } + + long firstTimeUs; + long firstOffset; + long secondTimeUs = C.TIME_UNSET; + long secondOffset = C.POSITION_UNSET; + + // If we have a video track, use it to establish one or two seek points. + if (firstVideoTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); if (sampleIndex == C.INDEX_UNSET) { - // Handle the case where the requested time is before the first synchronization sample. - sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + return new SeekPoints(SeekPoint.START); } - long offset = sampleTable.offsets[sampleIndex]; - if (offset < earliestSamplePosition) { - earliestSamplePosition = offset; + long sampleTimeUs = sampleTable.timestampsUs[sampleIndex]; + firstTimeUs = sampleTimeUs; + firstOffset = sampleTable.offsets[sampleIndex]; + if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) { + int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) { + secondTimeUs = sampleTable.timestampsUs[secondSampleIndex]; + secondOffset = sampleTable.offsets[secondSampleIndex]; + } + } + } else { + firstTimeUs = timeUs; + firstOffset = Long.MAX_VALUE; + } + + // Take into account other tracks. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } } } - return earliestSamplePosition; + + SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset); + if (secondTimeUs == C.TIME_UNSET) { + return new SeekPoints(firstSeekPoint); + } else { + SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset); + return new SeekPoints(firstSeekPoint, secondSeekPoint); + } } // Private methods. @@ -326,31 +363,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } - /** - * Process an ftyp atom to determine whether the media is QuickTime. - * - * @param atomData The ftyp atom data. - * @return Whether the media is QuickTime. - */ - private static boolean processFtypAtom(ParsableByteArray atomData) { - atomData.setPosition(Atom.HEADER_SIZE); - int majorBrand = atomData.readInt(); - if (majorBrand == BRAND_QUICKTIME) { - return true; - } - atomData.skipBytes(4); // minor_version - while (atomData.bytesLeft() > 0) { - if (atomData.readInt() == BRAND_QUICKTIME) { - return true; - } - } - return false; - } - /** * Updates the stored track metadata to reflect the contents of the specified moov atom. */ private void processMoovAtom(ContainerAtom moov) throws ParserException { + int firstVideoTrackIndex = C.INDEX_UNSET; long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; @@ -402,6 +419,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { mp4Track.trackOutput.format(format); durationUs = Math.max(durationUs, track.durationUs); + if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { + firstVideoTrackIndex = tracks.size(); + } tracks.add(mp4Track); long firstSampleOffset = trackSampleTable.offsets[0]; @@ -409,8 +429,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { earliestSampleOffset = firstSampleOffset; } } + this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); + extractorOutput.endTracks(); extractorOutput.seekMap(this); } @@ -538,6 +560,66 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } + /** + * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, + * for a given {@code seekTimeUs}. + * + * @param sampleTable The sample table to use. + * @param seekTimeUs The seek time in microseconds. + * @param offset The current offset. + * @return The adjusted offset. + */ + private static long maybeAdjustSeekOffset( + TrackSampleTable sampleTable, long seekTimeUs, long offset) { + int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs); + if (sampleIndex == C.INDEX_UNSET) { + return offset; + } + long sampleOffset = sampleTable.offsets[sampleIndex]; + return Math.min(sampleOffset, offset); + } + + /** + * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if + * there are no synchronization samples in the table. + * + * @param sampleTable The sample table in which to locate a synchronization sample. + * @param timeUs A time in microseconds. + * @return The index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} + * if there are no synchronization samples in the table. + */ + private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) { + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + return sampleIndex; + } + + /** + * Process an ftyp atom to determine whether the media is QuickTime. + * + * @param atomData The ftyp atom data. + * @return Whether the media is QuickTime. + */ + private static boolean processFtypAtom(ParsableByteArray atomData) { + atomData.setPosition(Atom.HEADER_SIZE); + int majorBrand = atomData.readInt(); + if (majorBrand == BRAND_QUICKTIME) { + return true; + } + atomData.skipBytes(4); // minor_version + while (atomData.bytesLeft() > 0) { + if (atomData.readInt() == BRAND_QUICKTIME) { + return true; + } + } + return false; + } + /** * Returns whether the extractor should decode a leaf atom with type {@code atom}. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 77def57275..042ab681f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; @@ -219,12 +220,13 @@ import java.io.IOException; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (timeUs == 0) { - return startPosition; + return new SeekPoints(new SeekPoint(0, startPosition)); } long granule = streamReader.convertTimeToGranule(timeUs); - return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); + long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 304fb3dd96..5eb0727908 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -192,10 +193,20 @@ import java.util.List; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { long granule = convertTimeToGranule(timeUs); int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); - return firstFrameOffset + seekPointOffsets[index]; + long seekTimeUs = convertGranuleToTime(seekPointGranules[index]); + long seekPosition = firstFrameOffset + seekPointOffsets[index]; + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) { + return new SeekPoints(seekPoint); + } else { + long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]); + long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1]; + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index 2cdd31cb6f..33db6c1e6c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ @@ -83,13 +84,22 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / blockAlignment) * blockAlignment; positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); - // Add data start position. - return dataStartPosition + positionOffset; + long seekPosition = dataStartPosition + positionOffset; + long seekTimeUs = getTimeUs(seekPosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekPosition + blockAlignment; + long secondSeekTimeUs = getTimeUs(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } } // Misc getters. @@ -100,7 +110,8 @@ import com.google.android.exoplayer2.util.Util; * @param position The position in bytes. */ public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + long positionOffset = Math.max(0, position - dataStartPosition); + return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond; } /** Returns the bytes per frame of this WAV. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index c0586b3a28..344286ed3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -549,7 +549,8 @@ import java.util.Arrays; pendingResetPositionUs = C.TIME_UNSET; return; } - loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs); + loadable.setLoadPosition( + seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs); pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index e644abc7ef..46cd7a2451 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import org.junit.Before; @@ -92,27 +94,39 @@ public final class XingSeekerTest { } @Test - public void testGetPositionAtStartOfStream() { - assertThat(seeker.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); - assertThat(seekerWithInputLength.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); + public void testGetSeekPointsAtStartOfStream() { + SeekPoints seekPoints = seeker.getSeekPoints(0); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.timeUs).isEqualTo(0); + assertThat(seekPoint.position).isEqualTo(XING_FRAME_POSITION + xingFrameSize); } @Test - public void testGetPositionAtEndOfStream() { - assertThat(seeker.getPosition(STREAM_DURATION_US)) - .isEqualTo(STREAM_LENGTH - 1); - assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) - .isEqualTo(STREAM_LENGTH - 1); + public void testGetSeekPointsAtEndOfStream() { + SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.timeUs).isEqualTo(STREAM_DURATION_US); + assertThat(seekPoint.position).isEqualTo(STREAM_LENGTH - 1); } @Test public void testGetTimeForAllPositions() { for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; + // Test seeker. long timeUs = seeker.getTimeUs(position); - assertThat(seeker.getPosition(timeUs)).isEqualTo(position); + SeekPoints seekPoints = seeker.getSeekPoints(timeUs); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.position).isEqualTo(position); + // Test seekerWithInputLength. timeUs = seekerWithInputLength.getTimeUs(position); - assertThat(seekerWithInputLength.getPosition(timeUs)).isEqualTo(position); + seekPoints = seekerWithInputLength.getSeekPoints(timeUs); + seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.position).isEqualTo(position); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index db63662c45..8c419ce1a0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -143,7 +143,7 @@ public final class ExtractorAsserts { long durationUs = seekMap.getDurationUs(); for (int j = 0; j < 4; j++) { long timeUs = (durationUs * j) / 3; - long position = seekMap.getPosition(timeUs); + long position = seekMap.getSeekPoints(timeUs).first.position; input.setPosition((int) position); for (int i = 0; i < extractorOutput.numberOfTracks; i++) { extractorOutput.trackOutputs.valueAt(i).clear(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index ee8927ea21..3f9a7c542f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -78,7 +78,7 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab Assert.assertNotNull(seekMap); Assert.assertEquals(expected.seekMap.getClass(), seekMap.getClass()); Assert.assertEquals(expected.seekMap.isSeekable(), seekMap.isSeekable()); - Assert.assertEquals(expected.seekMap.getPosition(0), seekMap.getPosition(0)); + Assert.assertEquals(expected.seekMap.getSeekPoints(0), seekMap.getSeekPoints(0)); } for (int i = 0; i < numberOfTracks; i++) { Assert.assertEquals(expected.trackOutputs.keyAt(i), trackOutputs.keyAt(i)); @@ -114,10 +114,11 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab @Override public void dump(Dumper dumper) { if (seekMap != null) { - dumper.startBlock("seekMap") + dumper + .startBlock("seekMap") .add("isSeekable", seekMap.isSeekable()) .addTime("duration", seekMap.getDurationUs()) - .add("getPosition(0)", seekMap.getPosition(0)) + .add("getPosition(0)", seekMap.getSeekPoints(0)) .endBlock(); } dumper.add("numberOfTracks", numberOfTracks); From 002df729a558773ca1dc347ad1a05c4cedd449d5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 4 Dec 2017 10:50:33 -0800 Subject: [PATCH 160/417] Allow late HLS sample queue building Issue:#3149 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177836048 --- ISSUE_TEMPLATE | 2 + .../source/hls/HlsSampleStream.java | 38 ++++- .../source/hls/HlsSampleStreamWrapper.java | 140 ++++++++++++------ .../hls/SampleQueueMappingException.java | 29 ++++ 4 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index e85c0c28c7..1b912312d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,3 +1,5 @@ +*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** + Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 0388f354ce..d53db1feaf 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream; @@ -25,33 +26,60 @@ import java.io.IOException; */ /* package */ final class HlsSampleStream implements SampleStream { - public final int sampleQueueIndex; - + private final int trackGroupIndex; private final HlsSampleStreamWrapper sampleStreamWrapper; + private int sampleQueueIndex; - public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int sampleQueueIndex) { + public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) { this.sampleStreamWrapper = sampleStreamWrapper; - this.sampleQueueIndex = sampleQueueIndex; + this.trackGroupIndex = trackGroupIndex; } + public void unbindSampleQueue() { + if (sampleQueueIndex != C.INDEX_UNSET) { + sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); + } + } + + // SampleStream implementation. + @Override public boolean isReady() { - return sampleStreamWrapper.isReady(sampleQueueIndex); + return ensureBoundSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex); } @Override public void maybeThrowError() throws IOException { + if (!ensureBoundSampleQueue()) { + throw new SampleQueueMappingException( + sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); + } sampleStreamWrapper.maybeThrowError(); } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { + if (!ensureBoundSampleQueue()) { + return C.RESULT_NOTHING_READ; + } return sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat); } @Override public int skipData(long positionUs) { + if (!ensureBoundSampleQueue()) { + return 0; + } return sampleStreamWrapper.skipData(sampleQueueIndex, positionUs); } + // Internal methods. + + private boolean ensureBoundSampleQueue() { + if (sampleQueueIndex != C.INDEX_UNSET) { + return true; + } + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); + return sampleQueueIndex != C.INDEX_UNSET; + } } 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 f4ba9a6eac..dbb71329c5 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 @@ -88,13 +88,14 @@ import java.util.Arrays; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final ArrayList mediaChunks; private final Runnable maybeFinishPrepareRunnable; + private final Runnable onTracksEndedRunnable; private final Handler handler; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; - private int enabledSampleQueueCount; + private int enabledTrackGroupCount; private Format downstreamTrackFormat; private boolean released; @@ -108,13 +109,16 @@ import java.util.Arrays; private boolean[] sampleQueuesEnabledStates; private boolean[] sampleQueueIsAudioVideoFlags; - private long sampleOffsetUs; private long lastSeekPositionUs; private long pendingResetPositionUs; private boolean pendingResetUpstreamFormats; private boolean seenFirstTrackSelection; private boolean loadingFinished; + // Accessed only by the loading thread. + private boolean tracksEnded; + private long sampleOffsetUs; + /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param callback A callback for the wrapper. @@ -143,12 +147,20 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); - maybeFinishPrepareRunnable = new Runnable() { - @Override - public void run() { - maybeFinishPrepare(); - } - }; + maybeFinishPrepareRunnable = + new Runnable() { + @Override + public void run() { + maybeFinishPrepare(); + } + }; + onTracksEndedRunnable = + new Runnable() { + @Override + public void run() { + onTracksEnded(); + } + }; handler = new Handler(); lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; @@ -166,8 +178,8 @@ import java.util.Arrays; */ public void prepareSingleTrack(Format format) { track(0, C.TRACK_TYPE_UNKNOWN).format(format); - sampleQueuesBuilt = true; - maybeFinishPrepare(); + tracksEnded = true; + onTracksEnded(); } public void maybeThrowPrepareError() throws IOException { @@ -178,6 +190,19 @@ import java.util.Arrays; return trackGroups; } + public int bindSampleQueueToSampleStream(int trackGroupIndex) { + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + if (sampleQueueIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + setSampleQueueEnabledState(sampleQueueIndex, true); + return sampleQueueIndex; + } + + public void unbindSampleQueue(int trackGroupIndex) { + setSampleQueueEnabledState(trackGroupToSampleQueueIndex[trackGroupIndex], false); + } + /** * Called by the parent {@link HlsMediaPeriod} when a track selection occurs. * @@ -198,20 +223,23 @@ import java.util.Arrays; public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) { Assertions.checkState(prepared); - int oldEnabledSampleQueueCount = enabledSampleQueueCount; + int oldEnabledTrackGroupCount = enabledTrackGroupCount; // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - setSampleQueueEnabledState(((HlsSampleStream) streams[i]).sampleQueueIndex, false); + enabledTrackGroupCount--; + ((HlsSampleStream) streams[i]).unbindSampleQueue(); streams[i] = null; } } // We'll always need to seek if we're being forced to reset, or if this is a first selection to // a position other than the one we started preparing with, or if we're making a selection // having previously disabled all tracks. - boolean seekRequired = forceReset - || (seenFirstTrackSelection ? oldEnabledSampleQueueCount == 0 - : positionUs != lastSeekPositionUs); + boolean seekRequired = + forceReset + || (seenFirstTrackSelection + ? oldEnabledTrackGroupCount == 0 + : positionUs != lastSeekPositionUs); // Get the old (i.e. current before the loop below executes) primary track selection. The new // primary selection will equal the old one unless it's changed in the loop. TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection(); @@ -219,19 +247,18 @@ import java.util.Arrays; // Select new tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { + enabledTrackGroupCount++; TrackSelection selection = selections[i]; int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; - setSampleQueueEnabledState(sampleQueueIndex, true); if (trackGroupIndex == primaryTrackGroupIndex) { primaryTrackSelection = selection; chunkSource.selectTracks(selection); } - streams[i] = new HlsSampleStream(this, sampleQueueIndex); + streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; // If there's still a chance of avoiding a seek, try and seek within the sample queue. - if (!seekRequired) { - SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; + if (sampleQueuesBuilt && !seekRequired) { + SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; sampleQueue.rewind(); // A seek can be avoided if we're able to advance to the current playback position in the // sample queue, or if we haven't read anything from the queue since the previous seek @@ -243,14 +270,16 @@ import java.util.Arrays; } } - if (enabledSampleQueueCount == 0) { + if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; mediaChunks.clear(); if (loader.isLoading()) { - // Discard as much as we can synchronously. - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.discardToEnd(); + if (sampleQueuesBuilt) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } } loader.cancelLoading(); } else { @@ -297,6 +326,9 @@ import java.util.Arrays; } public void discardBuffer(long positionUs, boolean toKeyframe) { + if (!sampleQueuesBuilt) { + return; + } int sampleQueueCount = sampleQueues.length; for (int i = 0; i < sampleQueueCount; i++) { sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]); @@ -314,7 +346,7 @@ import java.util.Arrays; public boolean seekToUs(long positionUs, boolean forceReset) { lastSeekPositionUs = positionUs; // If we're not forced to reset nor have a pending reset, see if we can seek within the buffer. - if (!forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) { + if (sampleQueuesBuilt && !forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) { return false; } // We were unable to seek within the buffer, so need to reset. @@ -426,9 +458,11 @@ import java.util.Arrays; if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - for (SampleQueue sampleQueue : sampleQueues) { - bufferedPositionUs = Math.max(bufferedPositionUs, - sampleQueue.getLargestQueuedTimestampUs()); + if (sampleQueuesBuilt) { + for (SampleQueue sampleQueue : sampleQueues) { + bufferedPositionUs = + Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs()); + } } return bufferedPositionUs; } @@ -513,7 +547,7 @@ import java.util.Arrays; loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { resetSampleQueues(); - if (enabledSampleQueueCount > 0) { + if (enabledTrackGroupCount > 0) { callback.onContinueLoadingRequested(this); } } @@ -582,7 +616,7 @@ import java.util.Arrays; return sampleQueues[i]; } } - if (sampleQueuesBuilt) { + if (tracksEnded) { Log.w(TAG, "Unmapped track with id " + id + " of type " + type); return new DummyTrackOutput(); } @@ -603,8 +637,8 @@ import java.util.Arrays; @Override public void endTracks() { - sampleQueuesBuilt = true; - handler.post(maybeFinishPrepareRunnable); + tracksEnded = true; + handler.post(onTracksEndedRunnable); } @Override @@ -616,9 +650,7 @@ import java.util.Arrays; @Override public void onUpstreamFormatChanged(Format format) { - if (!prepared) { - handler.post(maybeFinishPrepareRunnable); - } + handler.post(maybeFinishPrepareRunnable); } // Called by the loading thread. @@ -650,6 +682,11 @@ import java.util.Arrays; pendingResetUpstreamFormats = false; } + private void onTracksEnded() { + sampleQueuesBuilt = true; + maybeFinishPrepare(); + } + private void maybeFinishPrepare() { if (released || prepared || !sampleQueuesBuilt) { return; @@ -739,14 +776,14 @@ import java.util.Arrays; if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; for (int j = 0; j < chunkSourceTrackCount; j++) { - formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat); + formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true); } trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; - trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); + trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } this.trackGroups = new TrackGroupArray(trackGroups); @@ -761,7 +798,6 @@ import java.util.Arrays; private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) { Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState); sampleQueuesEnabledStates[sampleQueueIndex] = enabledState; - enabledSampleQueueCount = enabledSampleQueueCount + (enabledState ? 1 : -1); } private HlsMediaChunk getLastMediaChunk() { @@ -797,22 +833,30 @@ import java.util.Arrays; } /** - * Derives a track format corresponding to a given container format, by combining it with sample - * level information obtained from the samples. + * Derives a track format using master playlist and sample format information. * - * @param containerFormat The container format for which the track format should be derived. - * @param sampleFormat A sample format from which to obtain sample level information. + * @param playlistFormat The format information obtained from the master playlist. + * @param sampleFormat The format information obtained from the samples. + * @param propagateBitrate Whether the bitrate from the playlist format should be included in the + * derived format. * @return The derived track format. */ - private static Format deriveFormat(Format containerFormat, Format sampleFormat) { - if (containerFormat == null) { + private static Format deriveFormat( + Format playlistFormat, Format sampleFormat, boolean propagateBitrate) { + if (playlistFormat == null) { return sampleFormat; } + int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); - return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, - containerFormat.width, containerFormat.height, containerFormat.selectionFlags, - containerFormat.language); + String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); + return sampleFormat.copyWithContainerInfo( + playlistFormat.id, + codecs, + bitrate, + playlistFormat.width, + playlistFormat.height, + playlistFormat.selectionFlags, + playlistFormat.language); } private static boolean isMediaChunk(Chunk chunk) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java new file mode 100644 index 0000000000..2d430d2c79 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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.hls; + +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.TrackGroup; +import java.io.IOException; + +/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */ +public final class SampleQueueMappingException extends IOException { + + /** @param mimeType The mime type of the track group whose mapping failed. */ + public SampleQueueMappingException(String mimeType) { + super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); + } +} From b3ebdaaed332d2c1f4fa24ee011bbbd23c743deb Mon Sep 17 00:00:00 2001 From: amesbah Date: Mon, 4 Dec 2017 10:50:51 -0800 Subject: [PATCH 161/417] Add @SuppressWarnings("ComparableType") for instances of a class implementing 'Comparable' where T is not compatible with the type of the class. In order to facilitate enabling a compile-time error check, we are suppressing these existing instances. Once the compile-time error is enabled, we will file bugs to clean up any unfixed instances in []. Note that this CL should result in no effective changes to the code, but the code as currently-written might contain a real bug. If you'd prefer to fix the bug now, please either reply with edits, or accept this CL then follow up with a change that fixes the underlying issue. Tested: tap_presubmit: [] Some tests failed; test failures are believed to be unrelated to this CL ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177836122 --- .../exoplayer2/source/hls/playlist/HlsMediaPlaylist.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index b21ecb02d5..1f44607f98 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -29,9 +29,8 @@ import java.util.List; */ public final class HlsMediaPlaylist extends HlsPlaylist { - /** - * Media segment reference. - */ + /** Media segment reference. */ + @SuppressWarnings("ComparableType") public static final class Segment implements Comparable { /** From 59f6b059b0c44c8dcc572ade7fa9f6d815ea0d80 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 00:29:56 -0800 Subject: [PATCH 162/417] Make one ad request in ImaAdsLoader This fixes an issue where quickly detaching and reattaching the player might cause ads to be requested multiple times with both responses handled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177922167 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 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 743a428020..0eba9db2ed 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 @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -120,6 +121,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private Object pendingAdRequestContext; private List supportedMimeTypes; private EventListener eventListener; private Player player; @@ -183,10 +185,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; - /** - * Whether {@link #release()} has been called. - */ - private boolean released; /** * Creates a new IMA ads loader. @@ -296,7 +294,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void release() { - released = true; + pendingAdRequestContext = null; if (adsManager != null) { adsManager.destroy(); adsManager = null; @@ -308,10 +306,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (released) { + if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { adsManager.destroy(); return; } + pendingAdRequestContext = null; this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); @@ -403,6 +402,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } @@ -623,10 +623,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. private void requestAds() { + if (pendingAdRequestContext != null) { + // Ad request already in flight. + return; + } + pendingAdRequestContext = new Object(); AdsRequest request = imaSdkFactory.createAdsRequest(); request.setAdTagUrl(adTagUri.toString()); request.setAdDisplayContainer(adDisplayContainer); request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); } From f0e9dbf148fb6c7090fe6c69eae4beaf89fe6e89 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 5 Dec 2017 00:42:28 -0800 Subject: [PATCH 163/417] Fix HLS (broken on [] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177922948 --- .../google/android/exoplayer2/source/hls/HlsSampleStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index d53db1feaf..d180039b30 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -33,6 +33,7 @@ import java.io.IOException; public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) { this.sampleStreamWrapper = sampleStreamWrapper; this.trackGroupIndex = trackGroupIndex; + sampleQueueIndex = C.INDEX_UNSET; } public void unbindSampleQueue() { From f2f767bc1229cc982d3011b29e90cc0239d1f45a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 03:53:01 -0800 Subject: [PATCH 164/417] Invoke onLoadCanceled/Completed for ExtractorMediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177936271 --- .../source/ExtractorMediaPeriod.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 344286ed3d..17a6c3bcb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -405,14 +405,26 @@ import java.util.Arrays; @Override public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - copyLengthFromLoader(loadable); - loadingFinished = true; if (durationUs == C.TIME_UNSET) { long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); } + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); + copyLengthFromLoader(loadable); + loadingFinished = true; callback.onContinueLoadingRequested(this); } @@ -422,6 +434,18 @@ import java.util.Arrays; if (released) { return; } + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); @@ -434,7 +458,6 @@ import java.util.Arrays; @Override public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - copyLengthFromLoader(loadable); boolean isErrorFatal = isLoadableExceptionFatal(error); eventDispatcher.loadError( loadable.dataSpec, @@ -450,6 +473,7 @@ import java.util.Arrays; loadable.bytesLoaded, error, /* wasCanceled= */ isErrorFatal); + copyLengthFromLoader(loadable); if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } From 2b317234346bacfe8d77e53cfb3ad6b9760db5e1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 04:21:18 -0800 Subject: [PATCH 165/417] Remove self @link ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177938212 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 909a5d0fd5..17cf118ea3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -873,15 +873,15 @@ public class SimpleExoPlayer implements ExoPlayer { // Internal methods. /** - * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}. + * Creates the {@link ExoPlayer} implementation used by this instance. * * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @return A new {@link ExoPlayer} instance. */ - protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + protected ExoPlayer createExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return new ExoPlayerImpl(renderers, trackSelector, loadControl); } From a155edc5685d8ebe6c15724dfe843a0652693d80 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 05:19:48 -0800 Subject: [PATCH 166/417] Hide subtitles when switching player in SimpleExoPlayerView ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177941993 --- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 3 +++ .../java/com/google/android/exoplayer2/ui/SubtitleView.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b09e80c591..dcc1c62569 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -425,6 +425,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (shutterView != null) { shutterView.setVisibility(VISIBLE); } + if (subtitleView != null) { + subtitleView.setCues(null); + } if (player != null) { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 618f2fa336..d89f82b7c4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -87,9 +88,9 @@ public final class SubtitleView extends View implements TextOutput { /** * Sets the cues to be displayed by the view. * - * @param cues The cues to display. + * @param cues The cues to display, or null to clear the cues. */ - public void setCues(List cues) { + public void setCues(@Nullable List cues) { if (this.cues == cues) { return; } From aebc7da82b38ccb0f796ee50c610c5fc0aecc3f4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Dec 2017 06:57:27 -0800 Subject: [PATCH 167/417] Immediately release temp buffer memory in FakeRenderer. FakeRenderer only needs to allocate real memory because it extends BaseRenderer which uses the actual SampleStream implementation. Immediately release the memory after using it to prevent excessive memory usage when running fast simulations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177949628 --- .../com/google/android/exoplayer2/testutil/FakeRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index c4270eb9c4..75adcf9018 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -59,10 +59,10 @@ public class FakeRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (!isEnded) { - buffer.clear(); // Verify the format matches the expected format. FormatHolder formatHolder = new FormatHolder(); int result = readSource(formatHolder, buffer, false); + buffer.data = null; if (result == C.RESULT_FORMAT_READ) { formatReadCount++; Assert.assertTrue(expectedFormats.contains(formatHolder.format)); From 6606d73b29c3e27184ff59aa94a5d68908bbc30c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 5 Dec 2017 07:12:10 -0800 Subject: [PATCH 168/417] Unset sample queue index in HlsSampleStream#unbindSampleQueue ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177950960 --- .../google/android/exoplayer2/source/hls/HlsSampleStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index d180039b30..301cd2920b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -39,6 +39,7 @@ import java.io.IOException; public void unbindSampleQueue() { if (sampleQueueIndex != C.INDEX_UNSET) { sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); + sampleQueueIndex = C.INDEX_UNSET; } } From 88dea59cd225e424dc360e3fd7f412441ec3dc21 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 5 Dec 2017 08:31:17 -0800 Subject: [PATCH 169/417] Add ability for media period to discard buffered media at the back of the queue In some occasions, we may want to discard a part of the buffered media to improve playback quality. This CL adds this functionality by allowing the loading media period to re-evaluate its buffer periodically (every 2s) and discard chunks as it needs. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177958910 --- RELEASENOTES.md | 2 + .../exoplayer2/ExoPlayerImplInternal.java | 13 +- .../source/ClippingMediaPeriod.java | 5 + .../source/CompositeSequenceableLoader.java | 7 + .../source/DeferredMediaPeriod.java | 5 + .../source/ExtractorMediaPeriod.java | 5 + .../exoplayer2/source/MediaPeriod.java | 106 +++-- .../exoplayer2/source/MergingMediaPeriod.java | 5 + .../exoplayer2/source/SequenceableLoader.java | 11 + .../source/SingleSampleMediaPeriod.java | 5 + .../source/chunk/ChunkSampleStream.java | 90 ++-- .../AdaptiveTrackSelection.java | 176 ++++--- .../CompositeSequenceableLoaderTest.java | 5 + .../AdaptiveTrackSelectionTest.java | 428 ++++++++++++++++++ .../source/dash/DashMediaPeriod.java | 5 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 5 + .../source/hls/HlsSampleStreamWrapper.java | 5 + .../source/smoothstreaming/SsMediaPeriod.java | 5 + .../smoothstreaming/manifest/SsManifest.java | 16 +- .../exoplayer2/testutil/FakeMediaPeriod.java | 5 + 20 files changed, 765 insertions(+), 139 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d98f2aae0..80c55c4706 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add ability for `SequenceableLoader` to reevaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` 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 b0ef675e71..83e7858eaa 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 @@ -1283,6 +1283,7 @@ import java.io.IOException; // Update the loading period if required. maybeUpdateLoadingPeriod(); + if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (loadingPeriodHolder != null && !isLoading) { @@ -1386,6 +1387,7 @@ import java.io.IOException; if (loadingPeriodHolder == null) { info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); } else { + loadingPeriodHolder.reevaluateBuffer(rendererPositionUs); if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered() || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { return; @@ -1440,6 +1442,7 @@ import java.io.IOException; // Stale event. return; } + loadingPeriodHolder.reevaluateBuffer(rendererPositionUs); maybeContinueLoading(); } @@ -1628,13 +1631,18 @@ import java.io.IOException; info = info.copyWithStartPositionUs(newStartPositionUs); } + public void reevaluateBuffer(long rendererPositionUs) { + if (prepared) { + mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs)); + } + } + public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) { long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { return false; } else { - long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); - long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; + long bufferedDurationUs = nextLoadPositionUs - toPeriodTime(rendererPositionUs); return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } } @@ -1694,7 +1702,6 @@ import java.io.IOException; Assertions.checkState(trackSelections.get(i) == null); } } - // The track selection has changed. loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections); return positionUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 36e8e51ffb..539c4841e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -123,6 +123,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb mediaPeriod.discardBuffer(positionUs + startUs, toKeyframe); } + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs + startUs); + } + @Override public long readDiscontinuity() { if (isPendingInitialDiscontinuity()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index e9a187a747..c41933b48b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -52,6 +52,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader { return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs; } + @Override + public final void reevaluateBuffer(long positionUs) { + for (SequenceableLoader loader : loaders) { + loader.reevaluateBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { boolean madeProgress = false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index bc29b2fdf1..32a180b956 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -119,6 +119,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod.getNextLoadPositionUs(); } + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 17a6c3bcb8..6b9aeb39da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -288,6 +288,11 @@ import java.util.Arrays; } } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long playbackPositionUs) { if (loadingFinished || (prepared && enabledTrackCount == 0)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 439562e0ab..54b34bc531 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader { /** * Called when preparation completes. - *

      - * Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect - * for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be - * called with the initial track selection. + * + *

      Called on the playback thread. After invoking this method, the {@link MediaPeriod} can + * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], + * long)} to be called with the initial track selection. * * @param mediaPeriod The prepared {@link MediaPeriod}. */ void onPrepared(MediaPeriod mediaPeriod); - } /** * Prepares this media period asynchronously. - *

      - * {@code callback.onPrepared} is called when preparation completes. If preparation fails, + * + *

      {@code callback.onPrepared} is called when preparation completes. If preparation fails, * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. - *

      - * If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), - * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} will be - * called before {@code callback.onPrepared}. + * + *

      If preparation succeeds and results in a source timeline change (e.g. the period duration + * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, + * Object)} will be called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. @@ -66,8 +64,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Throws an error that's preventing the period from becoming prepared. Does nothing if no such * error exists. - *

      - * This method should only be called before the period has completed preparation. + * + *

      This method should only be called before the period has completed preparation. * * @throws IOException The underlying error. */ @@ -75,8 +73,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the {@link TrackGroup}s exposed by the period. - *

      - * This method should only be called after the period has been prepared. + * + *

      This method should only be called after the period has been prepared. * * @return The {@link TrackGroup}s. */ @@ -84,16 +82,16 @@ public interface MediaPeriod extends SequenceableLoader { /** * Performs a track selection. - *

      - * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} + * + *

      The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} * indicating whether the existing {@code SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. - *

      - * This method should only be called after the period has been prepared. + * + *

      This method should only be called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained @@ -104,16 +102,20 @@ public interface MediaPeriod extends SequenceableLoader { * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that * have been retained but with the requirement that the consuming renderer be reset. * @param positionUs The current playback position in microseconds. If playback of this period has - * not yet started, the value will be the starting position. + * not yet started, the value will be the starting position. * @return The actual position at which the tracks were enabled, in microseconds. */ - long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + long selectTracks( + TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs); /** * Discards buffered media up to the specified position. - *

      - * This method should only be called after the period has been prepared. + * + *

      This method should only be called after the period has been prepared. * * @param positionUs The position in microseconds. * @param toKeyframe If true then for each track discards samples up to the keyframe before or at @@ -123,11 +125,11 @@ public interface MediaPeriod extends SequenceableLoader { /** * Attempts to read a discontinuity. - *

      - * After this method has returned a value other than {@link C#TIME_UNSET}, all - * {@link SampleStream}s provided by the period are guaranteed to start from a key frame. - *

      - * This method should only be called after the period has been prepared. + * + *

      After this method has returned a value other than {@link C#TIME_UNSET}, all {@link + * SampleStream}s provided by the period are guaranteed to start from a key frame. + * + *

      This method should only be called after the period has been prepared. * * @return If a discontinuity was read then the playback position in microseconds after the * discontinuity. Else {@link C#TIME_UNSET}. @@ -136,11 +138,11 @@ public interface MediaPeriod extends SequenceableLoader { /** * Attempts to seek to the specified position in microseconds. - *

      - * After this method has been called, all {@link SampleStream}s provided by the period are + * + *

      After this method has been called, all {@link SampleStream}s provided by the period are * guaranteed to start from a key frame. - *

      - * This method should only be called when at least one track is selected. + * + *

      This method should only be called when at least one track is selected. * * @param positionUs The seek position in microseconds. * @return The actual position to which the period was seeked, in microseconds. @@ -151,8 +153,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns an estimate of the position up to which data is buffered for the enabled tracks. - *

      - * This method should only be called when at least one track is selected. + * + *

      This method should only be called when at least one track is selected. * * @return An estimate of the absolute position in microseconds up to which data is buffered, or * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. @@ -162,19 +164,19 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. - *

      - * This method should only be called after the period has been prepared. It may be called when no - * tracks are selected. + * + *

      This method should only be called after the period has been prepared. It may be called when + * no tracks are selected. */ @Override long getNextLoadPositionUs(); /** * Attempts to continue loading. - *

      - * This method may be called both during and after the period has been prepared. - *

      - * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the + * + *

      This method may be called both during and after the period has been prepared. + * + *

      A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be * called when the period is permitted to continue loading data. A period may do this both during * and after preparation. @@ -182,10 +184,24 @@ public interface MediaPeriod extends SequenceableLoader { * @param positionUs The current playback position in microseconds. If playback of this period has * not yet started, the value will be the starting position in this period minus the duration * of any media in previous periods still to be played. - * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return - * a different value than prior to the call. False otherwise. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a + * different value than prior to the call. False otherwise. */ @Override boolean continueLoading(long positionUs); + /** + * Re-evaluates the buffer given the playback position. + * + *

      This method should only be called after the period has been prepared. + * + *

      A period may choose to discard buffered media so that it can be re-buffered in a different + * quality. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + */ + @Override + void reevaluateBuffer(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index bd37b5efec..5ac9fc8d97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -139,6 +139,11 @@ import java.util.IdentityHashMap; } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 6daa1e847a..182f0f17cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -60,4 +60,15 @@ public interface SequenceableLoader { */ boolean continueLoading(long positionUs); + /** + * Re-evaluates the buffer given the playback position. + * + *

      Re-evaluation may discard buffered media so that it can be re-buffered in a different + * quality. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + */ + void reevaluateBuffer(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 0cea0fad66..7b8b54eedc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -120,6 +120,11 @@ import java.util.Arrays; // Do nothing. } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { 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 20b56e7807..85c4b12241 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 @@ -319,7 +319,9 @@ public class ChunkSampleStream implements SampleStream, S IOException error) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); - boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk(); + int lastChunkIndex = mediaChunks.size() - 1; + boolean cancelable = + bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); boolean canceled = false; if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { if (!cancelable) { @@ -327,12 +329,8 @@ public class ChunkSampleStream implements SampleStream, S } else { canceled = true; if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); + BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex); Assertions.checkState(removed == loadable); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); - } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } @@ -405,35 +403,29 @@ public class ChunkSampleStream implements SampleStream, S } } - // Internal methods - - // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming. - /** - * Discards media chunks from the back of the buffer if conditions have changed such that it's - * preferable to re-buffer the media at a different quality. - * - * @param positionUs The current playback position in microseconds. - */ - @SuppressWarnings("unused") - private void maybeDiscardUpstream(long positionUs) { + @Override + public void reevaluateBuffer(long positionUs) { + if (loader.isLoading() || isPendingReset()) { + return; + } int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - discardUpstreamMediaChunks(Math.max(1, queueSize)); + discardUpstreamMediaChunks(queueSize); } + // Internal methods + private boolean isMediaChunk(Chunk chunk) { return chunk instanceof BaseMediaChunk; } - /** - * Returns whether samples have been read from {@code mediaChunks.getLast()}. - */ - private boolean haveReadFromLastMediaChunk() { - BaseMediaChunk lastChunk = getLastMediaChunk(); - if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) { + /** Returns whether samples have been read from media chunk at given index. */ + private boolean haveReadFromMediaChunk(int mediaChunkIndex) { + BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); + if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) { return true; } for (int i = 0; i < embeddedSampleQueues.length; i++) { - if (embeddedSampleQueues[i].getReadIndex() > lastChunk.getFirstSampleIndex(i + 1)) { + if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) { return true; } } @@ -492,27 +484,51 @@ public class ChunkSampleStream implements SampleStream, S } /** - * Discard upstream media chunks until the queue length is equal to the length specified. + * Discard upstream media chunks until the queue length is equal to the length specified, but + * avoid discarding any chunk whose samples have been read by either primary sample stream or + * embedded sample streams. * - * @param queueLength The desired length of the queue. - * @return Whether chunks were discarded. + * @param desiredQueueSize The desired length of the queue. The final queue size after discarding + * maybe larger than this if there are chunks after the specified position that have been read + * by either primary sample stream or embedded sample streams. */ - private boolean discardUpstreamMediaChunks(int queueLength) { - if (mediaChunks.size() <= queueLength) { - return false; + private void discardUpstreamMediaChunks(int desiredQueueSize) { + if (mediaChunks.size() <= desiredQueueSize) { + return; } + int firstIndexToRemove = desiredQueueSize; + for (int i = firstIndexToRemove; i < mediaChunks.size(); i++) { + if (!haveReadFromMediaChunk(i)) { + firstIndexToRemove = i; + break; + } + } + + if (firstIndexToRemove == mediaChunks.size()) { + return; + } long endTimeUs = getLastMediaChunk().endTimeUs; - BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength); - long startTimeUs = firstRemovedChunk.startTimeUs; - Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size()); + BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove); + loadingFinished = false; + eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); + } + + /** + * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample + * queues. + * + * @param chunkIndex The index of the first chunk to discard. + * @return The chunk at given index. + */ + private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) { + BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex); + Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size()); primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); } - loadingFinished = false; - eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); - return true; + return firstRemovedChunk; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index ba45b2b186..973155c2e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -15,12 +15,12 @@ */ package com.google.android.exoplayer2.trackselection; -import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.util.List; @@ -42,17 +42,23 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final int minDurationToRetainAfterDiscardMs; private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ public Factory(BandwidthMeter bandwidthMeter) { - this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + this( + bandwidthMeter, + DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION, - DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** @@ -74,37 +80,55 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { - this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, - maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, - bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + this( + bandwidthMeter, + maxInitialBitrate, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed - * when a bandwidth estimate is unavailable. - * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for - * the selected track to switch to one of higher quality. - * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for - * the selected track to switch to one of lower quality. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a + * bandwidth estimate is unavailable. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher * quality, the selection may indicate that media already buffered at the lower quality can * be discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. - * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of - * the duration from current playback position to the live edge that has to be buffered - * before the selected track can be switched to one of higher quality. This parameter is - * only applied when the playback position is closer to the live edge than - * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a - * higher quality from happening. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before + * the selected track can be switched to one of higher quality. This parameter is only + * applied when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if + * network conditions have changed. This is the minimum duration between 2 consecutive + * buffer reevaluation calls. + * @param clock A {@link Clock}. */ - public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, - int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, - int minDurationToRetainAfterDiscardMs, float bandwidthFraction, - float bufferedFractionToLiveEdgeForQualityIncrease) { + public Factory( + BandwidthMeter bandwidthMeter, + int maxInitialBitrate, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; @@ -113,14 +137,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; } @Override public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, - minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, bandwidthFraction, - bufferedFractionToLiveEdgeForQualityIncrease); + return new AdaptiveTrackSelection( + group, + tracks, + bandwidthMeter, + maxInitialBitrate, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); } } @@ -131,6 +165,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; + public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; private final BandwidthMeter bandwidthMeter; private final int maxInitialBitrate; @@ -139,10 +174,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long minDurationToRetainAfterDiscardUs; private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; private float playbackSpeed; private int selectedIndex; private int reason; + private long lastBufferEvaluationMs; /** * @param group The {@link TrackGroup}. @@ -152,12 +190,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { */ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) { - this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + this( + group, + tracks, + bandwidthMeter, + DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION, - DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** @@ -172,23 +216,35 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher - * quality, the selection may indicate that media already buffered at the lower quality can - * be discarded to speed up the switch. This is the minimum duration of media that must be + * quality, the selection may indicate that media already buffered at the lower quality can be + * discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. - * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of - * the duration from current playback position to the live edge that has to be buffered - * before the selected track can be switched to one of higher quality. This parameter is - * only applied when the playback position is closer to the live edge than - * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a - * higher quality from happening. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before the + * selected track can be switched to one of higher quality. This parameter is only applied + * when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if network + * condition has changed. This is the minimum duration between 2 consecutive buffer + * reevaluation calls. */ - public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, - int maxInitialBitrate, long minDurationForQualityIncreaseMs, - long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, - float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) { + public AdaptiveTrackSelection( + TrackGroup group, + int[] tracks, + BandwidthMeter bandwidthMeter, + int maxInitialBitrate, + long minDurationForQualityIncreaseMs, + long maxDurationForQualityDecreaseMs, + long minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { super(group, tracks); this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; @@ -198,9 +254,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; playbackSpeed = 1f; selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; + lastBufferEvaluationMs = C.TIME_UNSET; + } + + @Override + public void enable() { + lastBufferEvaluationMs = C.TIME_UNSET; } @Override @@ -211,7 +275,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { - long nowMs = SystemClock.elapsedRealtime(); + long nowMs = clock.elapsedRealtime(); // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; selectedIndex = determineIdealSelectedIndex(nowMs); @@ -258,17 +322,25 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Override public int evaluateQueueSize(long playbackPositionUs, List queue) { + long nowMs = clock.elapsedRealtime(); + if (lastBufferEvaluationMs != C.TIME_UNSET + && nowMs - lastBufferEvaluationMs < minTimeBetweenBufferReevaluationMs) { + return queue.size(); + } + lastBufferEvaluationMs = nowMs; if (queue.isEmpty()) { return 0; } + int queueSize = queue.size(); - long mediaBufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; - long playoutBufferedDurationUs = - Util.getPlayoutDurationForMediaDuration(mediaBufferedDurationUs, playbackSpeed); - if (playoutBufferedDurationUs < minDurationToRetainAfterDiscardUs) { + MediaChunk lastChunk = queue.get(queueSize - 1); + long playoutBufferedDurationBeforeLastChunkUs = + Util.getPlayoutDurationForMediaDuration( + lastChunk.startTimeUs - playbackPositionUs, playbackSpeed); + if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) { return queueSize; } - int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); Format idealFormat = getFormat(idealSelectedIndex); // If the chunks contain video, discard from the first SD chunk beyond // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal @@ -293,8 +365,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * Computes the ideal selected index ignoring buffer health. * - * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or - * {@link Long#MIN_VALUE} to ignore blacklisting. + * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link + * Long#MIN_VALUE} to ignore blacklisting. */ private int determineIdealSelectedIndex(long nowMs) { long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java index e3ac104754..f7e29d2b06 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -265,6 +265,11 @@ public final class CompositeSequenceableLoaderTest { return loaded; } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + private void setNextChunkDurationUs(int nextChunkDurationUs) { this.nextChunkDurationUs = nextChunkDurationUs; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java new file mode 100644 index 0000000000..ea19c72826 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2017 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.trackselection; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit test for {@link AdaptiveTrackSelection}. */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class AdaptiveTrackSelectionTest { + + @Mock private BandwidthMeter mockBandwidthMeter; + private FakeClock fakeClock; + + private AdaptiveTrackSelection adaptiveTrackSelection; + + @Before + public void setUp() { + initMocks(this); + fakeClock = new FakeClock(0); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE); + } + + @Test + public void testSelectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testSelectInitialIndexUseBandwidthEstimateIfAvailable() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testUpdateSelectedTrackDoNotSwitchUpIfNotBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 2000L, which prompts the track selection to switch up if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 9_999_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate + // format. However, since we only buffered 9_999_000 us, which is smaller than + // minDurationForQualityIncreaseMs, we should defer switch up. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testUpdateSelectedTrackSwitchUpIfBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 2000L, which prompts the track selection to switch up if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 10_000_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate + // format. When we have buffered enough (10_000_000 us, which is equal to + // minDurationForQualityIncreaseMs), we should switch up now. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); + } + + @Test + public void testUpdateSelectedTrackDoNotSwitchDownIfBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 500L, which prompts the track selection to switch down if necessary. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 25_000_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate + // format. However, since we have enough buffer at higher quality (25_000_000 us, which is equal + // to maxDurationForQualityDecreaseMs), we should defer switch down. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testUpdateSelectedTrackSwitchDownIfNotBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 500L, which prompts the track selection to switch down if necessary. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 24_999_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate + // format. When we don't have enough buffer at higher quality (24_999_000 us is smaller than + // maxDurationForQualityDecreaseMs), we should switch down now. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); + } + + @Test + public void testEvaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + FakeMediaChunk chunk1 = + new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000); + FakeMediaChunk chunk2 = + new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000); + FakeMediaChunk chunk3 = + new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000); + List queue = new ArrayList<>(); + queue.add(chunk1); + queue.add(chunk2); + queue.add(chunk3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + + int size = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(size).isEqualTo(3); + } + + @Test + public void testEvaluateQueueSizeDoNotReevaluateUntilAfterMinTimeBetweenBufferReevaluation() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + FakeMediaChunk chunk1 = + new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000); + FakeMediaChunk chunk2 = + new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000); + FakeMediaChunk chunk3 = + new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000); + List queue = new ArrayList<>(); + queue.add(chunk1); + queue.add(chunk2); + queue.add(chunk3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( + trackGroup, + /* initialBitrate= */ 1000, + /* durationToRetainAfterDiscardMs= */ 15_000, + /* minTimeBetweenBufferReevaluationMs= */ 2000); + + int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + + fakeClock.advanceTime(1999); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); + + // When bandwidth estimation is updated, we can discard chunks at the end of the queue now. + // However, since min duration between buffer reevaluation = 2000, we will not reevaluate + // queue size if time now is only 1999 ms after last buffer reevaluation. + int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(newSize).isEqualTo(initialQueueSize); + } + + @Test + public void testEvaluateQueueSizeRetainMoreThanMinimumDurationAfterDiscard() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + FakeMediaChunk chunk1 = + new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000); + FakeMediaChunk chunk2 = + new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000); + FakeMediaChunk chunk3 = + new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000); + List queue = new ArrayList<>(); + queue.add(chunk1); + queue.add(chunk2); + queue.add(chunk3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( + trackGroup, + /* initialBitrate= */ 1000, + /* durationToRetainAfterDiscardMs= */ 15_000, + /* minTimeBetweenBufferReevaluationMs= */ 2000); + + int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(initialQueueSize).isEqualTo(3); + + fakeClock.advanceTime(2000); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); + + // When bandwidth estimation is updated and time has advanced enough, we can discard chunks at + // the end of the queue now. + // However, since duration to retain after discard = 15 000 ms, we need to retain at least the + // first 2 chunks + int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(newSize).isEqualTo(2); + } + + private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup, int initialBitrate) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( + TrackGroup trackGroup, int initialBitrate, long minDurationForQualityIncreaseMs) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + minDurationForQualityIncreaseMs, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( + TrackGroup trackGroup, int initialBitrate, long maxDurationForQualityDecreaseMs) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + maxDurationForQualityDecreaseMs, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( + TrackGroup trackGroup, + int initialBitrate, + long durationToRetainAfterDiscardMs, + long minTimeBetweenBufferReevaluationMs) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + durationToRetainAfterDiscardMs, + /* bandwidth fraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + minTimeBetweenBufferReevaluationMs, + fakeClock); + } + + private int[] selectedAllTracksInGroup(TrackGroup trackGroup) { + int[] listIndices = new int[trackGroup.length]; + for (int i = 0; i < trackGroup.length; i++) { + listIndices[i] = i; + } + return listIndices; + } + + private static Format videoFormat(int bitrate, int width, int height) { + return Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_H264, + /* codecs= */ null, + /* bitrate= */ bitrate, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ width, + /* height= */ height, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + } + + private static final class FakeMediaChunk extends MediaChunk { + + private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); + + public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { + super( + DATA_SOURCE, + new DataSpec(Uri.EMPTY), + trackFormat, + C.SELECTION_REASON_ADAPTIVE, + null, + startTimeUs, + endTimeUs, + 0); + } + + @Override + public void cancelLoad() { + // Do nothing. + } + + @Override + public boolean isLoadCanceled() { + return false; + } + + @Override + public void load() throws IOException, InterruptedException { + // Do nothing. + } + + @Override + public boolean isLoadCompleted() { + return true; + } + + @Override + public long bytesLoaded() { + return 0; + } + } +} 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 8fe10e94ee..f320ad2844 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 @@ -270,6 +270,11 @@ import java.util.Map; } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index dd596878d2..fd8f2bdbe9 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -195,6 +195,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); 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 dbb71329c5..2e69e41d30 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 @@ -524,6 +524,11 @@ import java.util.Arrays; return true; } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + // Loader.Callback implementation. @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index d418a21dff..564993befe 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -149,6 +149,11 @@ import java.util.ArrayList; } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index fbc3726a0e..0df180a5a6 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -203,8 +203,20 @@ public class SsManifest { long timescale, String name, int maxWidth, int maxHeight, int displayWidth, int displayHeight, String language, Format[] formats, List chunkStartTimes, long lastChunkDuration) { - this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight, - displayWidth, displayHeight, language, formats, chunkStartTimes, + this( + baseUri, + chunkTemplate, + type, + subType, + timescale, + name, + maxWidth, + maxHeight, + displayWidth, + displayHeight, + language, + formats, + chunkStartTimes, Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale), Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index c1be199b1e..d34c1d1c0c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -151,6 +151,11 @@ public class FakeMediaPeriod implements MediaPeriod { // Do nothing. } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + @Override public long readDiscontinuity() { Assert.assertTrue(prepared); From 8a0a8339e84262e72594cfa192f190bbf60ffc10 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Dec 2017 09:10:05 -0800 Subject: [PATCH 170/417] Change handling of renderer position offset for first media period. This should be a no-op change. And it eliminates the need to use the index variable which will be removed once the MediaPeriodHolderQueue is implemented. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177963360 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 83e7858eaa..dd3ce136d6 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 @@ -1407,9 +1407,10 @@ import java.io.IOException; return; } - long rendererPositionOffsetUs = loadingPeriodHolder == null - ? RENDERER_TIMESTAMP_OFFSET_US - : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); + long rendererPositionOffsetUs = + loadingPeriodHolder == null + ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US) + : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, @@ -1553,8 +1554,8 @@ import java.io.IOException; public final int index; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; - public final long rendererPositionOffsetUs; + public long rendererPositionOffsetUs; public MediaPeriodInfo info; public boolean prepared; public boolean hasEnabledTracks; @@ -1574,7 +1575,7 @@ import java.io.IOException; MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; - this.rendererPositionOffsetUs = rendererPositionOffsetUs; + this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; this.trackSelector = trackSelector; this.loadControl = loadControl; this.mediaSource = mediaSource; @@ -1601,8 +1602,7 @@ import java.io.IOException; } public long getRendererOffset() { - return index == 0 ? rendererPositionOffsetUs - : (rendererPositionOffsetUs - info.startPositionUs); + return rendererPositionOffsetUs; } public boolean isFullyBuffered() { @@ -1628,6 +1628,7 @@ import java.io.IOException; prepared = true; selectTracks(playbackSpeed); long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); + rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); } From 586e657bd7fe9aeb194e40f2bff9dc3d3bbaff8e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 6 Dec 2017 08:06:51 -0800 Subject: [PATCH 171/417] Allow opt-in HLS chunkless preparation If allowed, the media period will try to finish preparation without downloading chunks (similar to what DashMediaPeriod does). To create track groups, HlsMediaPeriod will try to obtain as much information as possible from the master playlist. If any vital information is missing for specific urls, traditional preparation will take place instead. This version does not support tracks with DrmInitData info. This affects tracks with CDM DRM (e.g: Widevine, Clearkey, etc). AES_128 encryption is not affected. This information needs to be obtained from media playlists, and this version only takes the master playlist into account for preparation. Issue:#3149 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178098759 --- RELEASENOTES.md | 4 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 229 ++++++++++++++---- .../exoplayer2/source/hls/HlsMediaSource.java | 60 ++++- .../source/hls/HlsSampleStream.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 89 +++++-- 5 files changed, 298 insertions(+), 86 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80c55c4706..920d80ee48 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### dev-v2 (not yet released) ### +* Add initial support for chunkless preparation in HLS. This allows an HLS media + source to finish preparation without donwloading any chunks, which might + considerably reduce the initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/2980)). * Add ability for `SequenceableLoader` to reevaluate its buffer and discard buffered media so that it can be re-buffered in a different quality. * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index fd8f2bdbe9..24acf0f84d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; @@ -32,6 +31,8 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -55,6 +56,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final boolean allowChunklessPreparation; private Callback callback; private int pendingPrepareCount; @@ -63,10 +65,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private SequenceableLoader compositeSequenceableLoader; - public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, - HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, - EventDispatcher eventDispatcher, Allocator allocator, - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + public HlsMediaPeriod( + HlsExtractorFactory extractorFactory, + HlsPlaylistTracker playlistTracker, + HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + boolean allowChunklessPreparation) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; @@ -74,6 +81,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.allowChunklessPreparation = allowChunklessPreparation; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); @@ -293,15 +301,92 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private void buildAndPrepareSampleStreamWrappers(long positionUs) { HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); - // Build the default stream wrapper. + List audioRenditions = masterPlaylist.audios; + List subtitleRenditions = masterPlaylist.subtitles; + + int wrapperCount = 1 /* variants */ + audioRenditions.size() + subtitleRenditions.size(); + sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount]; + pendingPrepareCount = wrapperCount; + + buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs); + int currentWrapperIndex = 1; + + // TODO: Build video stream wrappers here. + + // Audio sample stream wrappers. + for (int i = 0; i < audioRenditions.size(); i++) { + HlsUrl audioRendition = audioRenditions.get(i); + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_AUDIO, + new HlsUrl[] {audioRendition}, + null, + Collections.emptyList(), + positionUs); + sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + Format renditionFormat = audioRendition.format; + if (allowChunklessPreparation && renditionFormat.codecs != null) { + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(new TrackGroup(audioRendition.format)), 0); + } else { + sampleStreamWrapper.continuePreparing(); + } + } + + // Subtitle stream wrappers. We can always use master playlist information to prepare these. + for (int i = 0; i < subtitleRenditions.size(); i++) { + HlsUrl url = subtitleRenditions.get(i); + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_TEXT, + new HlsUrl[] {url}, + null, + Collections.emptyList(), + positionUs); + sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(new TrackGroup(url.format)), 0); + } + + // All wrappers are enabled during preparation. + enabledSampleStreamWrappers = sampleStreamWrappers; + } + + /** + * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}. + * + *

      The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It + * provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive + * and may contain multiple muxed tracks. + * + *

      If chunkless preparation is allowed, the media period will try preparation without segment + * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional + * preparation with segment downloads will take place. The following points apply to chunkless + * preparation: + * + *

        + *
      • A muxed audio track will be exposed if the codecs list contain an audio entry and the + * master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not + * contain any EXT-X-MEDIA tag. + *
      • Closed captions will only be exposed if they are declared by the master playlist. + *
      • ID3 tracks are not exposed. + *
      + * + * @param masterPlaylist The HLS master playlist. + * @param positionUs If preparation requires any chunk downloads, the position in microseconds at + * which downloading should start. Ignored otherwise. + */ + private void buildAndPrepareMainSampleStreamWrapper( + HlsMasterPlaylist masterPlaylist, long positionUs) { List selectedVariants = new ArrayList<>(masterPlaylist.variants); ArrayList definiteVideoVariants = new ArrayList<>(); ArrayList definiteAudioOnlyVariants = new ArrayList<>(); for (int i = 0; i < selectedVariants.size(); i++) { HlsUrl variant = selectedVariants.get(i); - if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { + Format format = variant.format; + if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { definiteVideoVariants.add(variant); - } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { + } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) { definiteAudioOnlyVariants.add(variant); } } @@ -317,43 +402,56 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } else { // Leave the enabled variants unchanged. They're likely either all video or all audio. } - List audioRenditions = masterPlaylist.audios; - List subtitleRenditions = masterPlaylist.subtitles; - sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size() - + subtitleRenditions.size()]; - int currentWrapperIndex = 0; - pendingPrepareCount = sampleStreamWrappers.length; - Assertions.checkArgument(!selectedVariants.isEmpty()); - HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; - selectedVariants.toArray(variants); + HlsUrl[] variants = selectedVariants.toArray(new HlsUrl[0]); + String codecs = variants[0].format.codecs; HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - sampleStreamWrapper.setIsTimestampMaster(true); - sampleStreamWrapper.continuePreparing(); + sampleStreamWrappers[0] = sampleStreamWrapper; + if (allowChunklessPreparation && codecs != null) { + boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null; + boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null; + List muxedTrackGroups = new ArrayList<>(); + if (variantsContainVideoCodecs) { + Format[] videoFormats = new Format[selectedVariants.size()]; + for (int i = 0; i < videoFormats.length; i++) { + videoFormats[i] = deriveVideoFormat(variants[i].format); + } + muxedTrackGroups.add(new TrackGroup(videoFormats)); - // TODO: Build video stream wrappers here. - - // Build audio stream wrappers. - for (int i = 0; i < audioRenditions.size(); i++) { - sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList(), positionUs); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + if (variantsContainAudioCodecs + && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { + muxedTrackGroups.add( + new TrackGroup( + deriveMuxedAudioFormat( + variants[0].format, masterPlaylist.muxedAudioFormat, Format.NO_VALUE))); + } + List ccFormats = masterPlaylist.muxedCaptionFormats; + if (ccFormats != null) { + for (int i = 0; i < ccFormats.size(); i++) { + muxedTrackGroups.add(new TrackGroup(ccFormats.get(i))); + } + } + } else if (variantsContainAudioCodecs) { + // Variants only contain audio. + Format[] audioFormats = new Format[selectedVariants.size()]; + for (int i = 0; i < audioFormats.length; i++) { + Format variantFormat = variants[i].format; + audioFormats[i] = + deriveMuxedAudioFormat( + variantFormat, masterPlaylist.muxedAudioFormat, variantFormat.bitrate); + } + muxedTrackGroups.add(new TrackGroup(audioFormats)); + } else { + // Variants contain codecs but no video or audio entries could be identified. + throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs); + } + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), 0); + } else { + sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); } - - // Build subtitle stream wrappers. - for (int i = 0; i < subtitleRenditions.size(); i++) { - HlsUrl url = subtitleRenditions.get(i); - sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - Collections.emptyList(), positionUs); - sampleStreamWrapper.prepareSingleTrack(url.format); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - } - - // All wrappers are enabled during preparation. - enabledSampleStreamWrappers = sampleStreamWrappers; } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, @@ -375,18 +473,49 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } - private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { - String codecs = variant.format.codecs; - if (TextUtils.isEmpty(codecs)) { - return false; + private static Format deriveVideoFormat(Format variantFormat) { + String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); + String mimeType = MimeTypes.getMediaMimeType(codecs); + return Format.createVideoSampleFormat( + variantFormat.id, + mimeType, + codecs, + variantFormat.bitrate, + Format.NO_VALUE, + variantFormat.width, + variantFormat.height, + variantFormat.frameRate, + null, + null); + } + + private static Format deriveMuxedAudioFormat( + Format variantFormat, Format mediaTagFormat, int bitrate) { + String codecs; + int channelCount = Format.NO_VALUE; + int selectionFlags = 0; + String language = null; + if (mediaTagFormat != null) { + codecs = mediaTagFormat.codecs; + channelCount = mediaTagFormat.channelCount; + selectionFlags = mediaTagFormat.selectionFlags; + language = mediaTagFormat.language; + } else { + codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); } - String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); - for (String codec : codecArray) { - if (codec.startsWith(prefix)) { - return true; - } - } - return false; + String mimeType = MimeTypes.getMediaMimeType(codecs); + return Format.createAudioSampleFormat( + variantFormat.id, + mimeType, + codecs, + bitrate, + Format.NO_VALUE, + channelCount, + Format.NO_VALUE, + null, + null, + selectionFlags, + language); } } 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 4e5783698a..1cddf6e94e 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 @@ -63,8 +63,9 @@ public final class HlsMediaSource implements MediaSource, private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; + private boolean allowChunklessPreparation; + private boolean isBuildCalled; /** @@ -98,7 +99,6 @@ public final class HlsMediaSource implements MediaSource, private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { this.manifestUri = manifestUri; this.hlsDataSourceFactory = hlsDataSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } @@ -170,6 +170,18 @@ public final class HlsMediaSource implements MediaSource, return this; } + /** + * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads + * will be enabled for streams that provide sufficient information in their master playlist. + * + * @param allowChunklessPreparation Whether chunkless preparation is allowed. + * @return This builder. + */ + public Builder setAllowChunklessPreparation(boolean allowChunklessPreparation) { + this.allowChunklessPreparation = allowChunklessPreparation; + return this; + } + /** * Builds a new {@link HlsMediaSource} using the current parameters. *

      @@ -190,9 +202,16 @@ public final class HlsMediaSource implements MediaSource, if (compositeSequenceableLoaderFactory == null) { compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } - return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - compositeSequenceableLoaderFactory, minLoadableRetryCount, eventHandler, eventListener, - playlistParser); + return new HlsMediaSource( + manifestUri, + hlsDataSourceFactory, + extractorFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser, + allowChunklessPreparation); } } @@ -209,6 +228,7 @@ public final class HlsMediaSource implements MediaSource, private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final ParsingLoadable.Parser playlistParser; + private final boolean allowChunklessPreparation; private HlsPlaylistTracker playlistTracker; private Listener sourceListener; @@ -277,9 +297,16 @@ public final class HlsMediaSource implements MediaSource, Handler eventHandler, MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { - this(manifestUri, dataSourceFactory, extractorFactory, - new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler, - eventListener, playlistParser); + this( + manifestUri, + dataSourceFactory, + extractorFactory, + new DefaultCompositeSequenceableLoaderFactory(), + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser, + false); } private HlsMediaSource( @@ -290,13 +317,15 @@ public final class HlsMediaSource implements MediaSource, int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + ParsingLoadable.Parser playlistParser, + boolean allowChunklessPreparation) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.playlistParser = playlistParser; - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.allowChunklessPreparation = allowChunklessPreparation; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -317,8 +346,15 @@ public final class HlsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory, - minLoadableRetryCount, eventDispatcher, allocator, compositeSequenceableLoaderFactory); + return new HlsMediaPeriod( + extractorFactory, + playlistTracker, + dataSourceFactory, + minLoadableRetryCount, + eventDispatcher, + allocator, + compositeSequenceableLoaderFactory, + allowChunklessPreparation); } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 301cd2920b..6563a5fba0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -52,7 +52,7 @@ import java.io.IOException; @Override public void maybeThrowError() throws IOException { - if (!ensureBoundSampleQueue()) { + if (!ensureBoundSampleQueue() && sampleStreamWrapper.isMappingFinished()) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); } 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 2e69e41d30..eba4596b7f 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 @@ -173,13 +173,17 @@ import java.util.Arrays; } /** - * Prepares a sample stream wrapper for which the master playlist provides enough information to - * prepare. + * Prepares the sample stream wrapper with master playlist information. + * + * @param trackGroups This {@link TrackGroupArray} to expose. + * @param primaryTrackGroupIndex The index of the adaptive track group. */ - public void prepareSingleTrack(Format format) { - track(0, C.TRACK_TYPE_UNKNOWN).format(format); - tracksEnded = true; - onTracksEnded(); + public void prepareWithMasterPlaylistInfo( + TrackGroupArray trackGroups, int primaryTrackGroupIndex) { + prepared = true; + this.trackGroups = trackGroups; + this.primaryTrackGroupIndex = primaryTrackGroupIndex; + callback.onPrepared(); } public void maybeThrowPrepareError() throws IOException { @@ -190,17 +194,30 @@ import java.util.Arrays; return trackGroups; } + public boolean isMappingFinished() { + return trackGroupToSampleQueueIndex != null; + } + public int bindSampleQueueToSampleStream(int trackGroupIndex) { + if (!isMappingFinished()) { + return C.INDEX_UNSET; + } int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { return C.INDEX_UNSET; } - setSampleQueueEnabledState(sampleQueueIndex, true); + if (sampleQueuesEnabledStates[sampleQueueIndex]) { + // This sample queue is already bound to a different sample stream. + return C.INDEX_UNSET; + } + sampleQueuesEnabledStates[sampleQueueIndex] = true; return sampleQueueIndex; } public void unbindSampleQueue(int trackGroupIndex) { - setSampleQueueEnabledState(trackGroupToSampleQueueIndex[trackGroupIndex], false); + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]); + sampleQueuesEnabledStates[sampleQueueIndex] = false; } /** @@ -693,7 +710,7 @@ import java.util.Arrays; } private void maybeFinishPrepare() { - if (released || prepared || !sampleQueuesBuilt) { + if (released || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) { return; } for (SampleQueue sampleQueue : sampleQueues) { @@ -701,9 +718,31 @@ import java.util.Arrays; return; } } - buildTracks(); - prepared = true; - callback.onPrepared(); + if (trackGroups != null) { + // The track groups were created with master playlist information. They only need to be mapped + // to a sample queue. + mapSampleQueuesToMatchTrackGroups(); + } else { + // Tracks are created using media segment information. + buildTracks(); + prepared = true; + callback.onPrepared(); + } + } + + private void mapSampleQueuesToMatchTrackGroups() { + int trackGroupCount = trackGroups.length; + trackGroupToSampleQueueIndex = new int[trackGroupCount]; + Arrays.fill(trackGroupToSampleQueueIndex, C.INDEX_UNSET); + for (int i = 0; i < trackGroupCount; i++) { + for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) { + SampleQueue sampleQueue = sampleQueues[queueIndex]; + if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) { + trackGroupToSampleQueueIndex[i] = queueIndex; + break; + } + } + } } /** @@ -794,17 +833,6 @@ import java.util.Arrays; this.trackGroups = new TrackGroupArray(trackGroups); } - /** - * Enables or disables a specified sample queue. - * - * @param sampleQueueIndex The index of the sample queue. - * @param enabledState True if the sample queue is being enabled, or false if it's being disabled. - */ - private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) { - Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState); - sampleQueuesEnabledStates[sampleQueueIndex] = enabledState; - } - private HlsMediaChunk getLastMediaChunk() { return mediaChunks.get(mediaChunks.size() - 1); } @@ -868,4 +896,19 @@ import java.util.Arrays; return chunk instanceof HlsMediaChunk; } + private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) { + String manifestFormatMimeType = manifestFormat.sampleMimeType; + String sampleFormatMimeType = sampleFormat.sampleMimeType; + int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType); + if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) { + return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType); + } else if (!Util.areEqual(manifestFormatMimeType, sampleFormatMimeType)) { + return false; + } + if (MimeTypes.APPLICATION_CEA608.equals(manifestFormatMimeType) + || MimeTypes.APPLICATION_CEA708.equals(manifestFormatMimeType)) { + return manifestFormat.accessibilityChannel == sampleFormat.accessibilityChannel; + } + return true; + } } From a7b11ecb17acb3dbd28b9158a13c9cab0f95efb1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Dec 2017 10:33:23 -0800 Subject: [PATCH 172/417] Add missing Nullable annotation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178117289 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 17cf118ea3..338c708864 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -729,7 +729,7 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { player.setPlaybackParameters(playbackParameters); } From 2f6a497d4418fa1c949104d309894648a53e66eb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 02:45:16 -0800 Subject: [PATCH 173/417] Use mappedTrackInfo local ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178216750 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 7d0975a750..0623f48a51 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 @@ -233,8 +233,8 @@ public class PlayerActivity extends Activity implements OnClickListener, } else if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); + trackSelectionHelper.showSelectionDialog( + this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag()); } } } From 8b4b01c7f67e7d28d7125c7848a7236f0463586b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 03:05:22 -0800 Subject: [PATCH 174/417] Skip ads before the initial player position Issue: #3527 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218391 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 57 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 920d80ee48..3e70286a5a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,9 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* IMA extension: + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). ### 2.6.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 0eba9db2ed..91e419fd48 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 @@ -65,7 +65,6 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { - static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } @@ -132,6 +131,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private AdsManager adsManager; private Timeline timeline; private long contentDurationMs; + private int podIndexOffset; private AdPlaybackState adPlaybackState; // Fields tracking IMA's state. @@ -274,6 +274,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.resume(); } } else { + pendingContentPositionMs = player.getCurrentPosition(); requestAds(); } } @@ -311,19 +312,45 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; + + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + } else if (adGroupIndexForPosition > 0) { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always + // start at 1, so take this into account when offsetting the pod index for the skipped ads. + podIndexOffset = adGroupIndexForPosition - 1; + } + adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); + updateAdPlaybackState(); } @@ -351,13 +378,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // 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; + adGroupIndex = + podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); int adCountInAdGroup = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " - + adGroupIndex); + Log.d( + TAG, + "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); } adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); updateAdPlaybackState(); @@ -741,4 +770,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } + /** + * Returns the index of the ad group that should be played before playing the content at {@code + * playbackPositionUs} when starting playback for the first time. This is the latest ad group at + * or before the specified playback position. If the first ad is after the playback position, + * returns {@link C#INDEX_UNSET}. + */ + private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) { + for (int i = 0; i < adGroupTimesUs.length; i++) { + long adGroupTimeUs = adGroupTimesUs[i]; + // A postroll ad is after any position in the content. + if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) { + return i == 0 ? C.INDEX_UNSET : (i - 1); + } + } + return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1); + } } From 439c3022d9453691432d7b4c05957e2b5c1521fc Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Dec 2017 03:06:54 -0800 Subject: [PATCH 175/417] Blacklist Moto Z from using secure DummySurface. Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218535 --- .../com/google/android/exoplayer2/video/DummySurface.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2d7a9dfd33..cc50443296 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -150,14 +150,17 @@ public final class DummySurface extends Surface { */ @TargetApi(24) private static boolean enableSecureDummySurfaceV24(Context context) { - if (Util.SDK_INT < 26 && "samsung".equals(Util.MANUFACTURER)) { + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. + // Pre API level 26 devices were not well tested unless they supported VR mode. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); From 44a8245a1a6a4ca575e2ac808a549fb1d9cbd926 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 06:38:39 -0800 Subject: [PATCH 176/417] Fix ad loading when there is no preroll ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178234009 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3e70286a5a..0caad888d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,7 @@ * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. ### 2.6.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 91e419fd48..19dfa1e83f 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 @@ -326,9 +326,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setMimeTypes(supportedMimeTypes); int adGroupIndexForPosition = getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == C.INDEX_UNSET) { + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { pendingContentPositionMs = C.TIME_UNSET; - } else if (adGroupIndexForPosition > 0) { + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { // Skip ad groups before the one at or immediately before the playback position. for (int i = 0; i < adGroupIndexForPosition; i++) { adPlaybackState.playedAdGroup(i); @@ -341,8 +345,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always - // start at 1, so take this into account when offsetting the pod index for the skipped ads. + // midroll/postroll. Midroll pod indices start at 1. podIndexOffset = adGroupIndexForPosition - 1; } From fede9c39c6c3ec4eaea64e13f149da4abac16e97 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 04:44:55 -0800 Subject: [PATCH 177/417] Treat captions that are wider than expected as middle aligned Issue: #3534 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178364353 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 0483f909b3..f018e055fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -740,8 +740,10 @@ public final class Cea608Decoder extends CeaDecoder { // The number of empty columns after the end of the text, in the same range. int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int startEndPaddingDelta = startPadding - endPadding; - if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { - // Treat approximately centered pop-on captions are middle aligned. + if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) { + // Treat approximately centered pop-on captions as middle aligned. We also treat captions + // that are wider than they should be in this way. See + // https://github.com/google/ExoPlayer/issues/3534. position = 0.5f; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { From 2b0b39ca3898ec71a8aa7b29386f789b3fff26fb Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 06:14:14 -0800 Subject: [PATCH 178/417] Public API for setting seek parameters Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178370038 --- .../google/android/exoplayer2/ExoPlayer.java | 7 ++ .../android/exoplayer2/ExoPlayerImpl.java | 8 ++ .../exoplayer2/ExoPlayerImplInternal.java | 76 ++++++++++--------- .../android/exoplayer2/SeekParameters.java | 72 ++++++++++++++++++ .../android/exoplayer2/SimpleExoPlayer.java | 5 ++ .../exoplayer2/testutil/StubExoPlayer.java | 6 ++ 6 files changed, 137 insertions(+), 37 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 915a083657..cc767752be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.os.Looper; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; @@ -251,4 +252,10 @@ public interface ExoPlayer extends Player { */ void blockingSendMessages(ExoPlayerMessage... messages); + /** + * Sets the parameters that control how seek operations are performed. + * + * @param seekParameters The seek parameters, or {@code null} to use the defaults. + */ + void setSeekParameters(@Nullable SeekParameters seekParameters); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ee96cb0c47..3fe6cc6eed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -258,6 +258,14 @@ import java.util.concurrent.CopyOnWriteArraySet; return playbackParameters; } + @Override + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + if (seekParameters == null) { + seekParameters = SeekParameters.DEFAULT; + } + internalPlayer.setSeekParameters(seekParameters); + } + @Override public void stop() { stop(/* reset= */ false); 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 dd3ce136d6..8f59451c48 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 @@ -66,15 +66,16 @@ import java.io.IOException; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; private static final int MSG_SET_PLAYBACK_PARAMETERS = 4; - private static final int MSG_STOP = 5; - private static final int MSG_RELEASE = 6; - private static final int MSG_REFRESH_SOURCE_INFO = 7; - private static final int MSG_PERIOD_PREPARED = 8; - private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; - private static final int MSG_CUSTOM = 11; - private static final int MSG_SET_REPEAT_MODE = 12; - private static final int MSG_SET_SHUFFLE_ENABLED = 13; + private static final int MSG_SET_SEEK_PARAMETERS = 5; + private static final int MSG_STOP = 6; + private static final int MSG_RELEASE = 7; + private static final int MSG_REFRESH_SOURCE_INFO = 8; + private static final int MSG_PERIOD_PREPARED = 9; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; + private static final int MSG_CUSTOM = 12; + private static final int MSG_SET_REPEAT_MODE = 13; + private static final int MSG_SET_SHUFFLE_ENABLED = 14; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -110,6 +111,9 @@ import java.io.IOException; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; + @SuppressWarnings("unused") + private SeekParameters seekParameters; + private PlaybackInfo playbackInfo; private MediaSource mediaSource; private Renderer[] enabledRenderers; @@ -148,6 +152,7 @@ import java.io.IOException; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); + seekParameters = SeekParameters.DEFAULT; playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -195,6 +200,10 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } + public void setSeekParameters(SeekParameters seekParameters) { + handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget(); + } + public void stop(boolean reset) { handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } @@ -294,62 +303,51 @@ import java.io.IOException; public boolean handleMessage(Message msg) { try { switch (msg.what) { - case MSG_PREPARE: { + case MSG_PREPARE: prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); return true; - } - case MSG_SET_PLAY_WHEN_READY: { + case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); return true; - } - case MSG_SET_REPEAT_MODE: { + case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); return true; - } - case MSG_SET_SHUFFLE_ENABLED: { + case MSG_SET_SHUFFLE_ENABLED: setShuffleModeEnabledInternal(msg.arg1 != 0); return true; - } - case MSG_DO_SOME_WORK: { + case MSG_DO_SOME_WORK: doSomeWork(); return true; - } - case MSG_SEEK_TO: { + case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); return true; - } - case MSG_SET_PLAYBACK_PARAMETERS: { + case MSG_SET_PLAYBACK_PARAMETERS: setPlaybackParametersInternal((PlaybackParameters) msg.obj); return true; - } - case MSG_STOP: { + case MSG_SET_SEEK_PARAMETERS: + setSeekParametersInternal((SeekParameters) msg.obj); + return true; + case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0); return true; - } - case MSG_RELEASE: { + case MSG_RELEASE: releaseInternal(); return true; - } - case MSG_PERIOD_PREPARED: { + case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); return true; - } - case MSG_REFRESH_SOURCE_INFO: { + case MSG_REFRESH_SOURCE_INFO: handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); return true; - } - case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: { + case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); return true; - } - case MSG_TRACK_SELECTION_INVALIDATED: { + case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); return true; - } - case MSG_CUSTOM: { + case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); return true; - } default: return false; } @@ -765,6 +763,10 @@ import java.io.IOException; mediaClock.setPlaybackParameters(playbackParameters); } + private void setSeekParametersInternal(SeekParameters seekParameters) { + this.seekParameters = seekParameters; + } + private void stopInternal(boolean reset) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java new file mode 100644 index 0000000000..8643b3999e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2017 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; + +import com.google.android.exoplayer2.util.Assertions; + +/** + * Parameters that apply to seeking. + * + *

      The predefined {@link #EXACT}, {@link #CLOSEST_SYNC}, {@link #PREVIOUS_SYNC} and {@link + * #NEXT_SYNC} parameters are suitable for most use cases. Seeking to sync points is typically + * faster but less accurate than exact seeking. + * + *

      In the general case, an instance specifies a maximum tolerance before ({@link + * #toleranceBeforeUs}) and after ({@link #toleranceAfterUs}) a requested seek position ({@code x}). + * If one or more sync points falls within the window {@code [x - toleranceBeforeUs, x + + * toleranceAfterUs]} then the seek will be performed to the sync point within the window that's + * closest to {@code x}. If no sync point falls within the window then the seek will be performed to + * {@code x - toleranceBeforeUs}. Internally the player may need to seek to an earlier sync point + * and discard media until this position is reached. + */ +public final class SeekParameters { + + /** Parameters for exact seeking. */ + public static final SeekParameters EXACT = new SeekParameters(0, 0); + /** Parameters for seeking to the closest sync point. */ + public static final SeekParameters CLOSEST_SYNC = + new SeekParameters(Long.MAX_VALUE, Long.MAX_VALUE); + /** Parameters for seeking to the sync point immediately before a requested seek position. */ + public static final SeekParameters PREVIOUS_SYNC = new SeekParameters(Long.MAX_VALUE, 0); + /** Parameters for seeking to the sync point immediately after a requested seek position. */ + public static final SeekParameters NEXT_SYNC = new SeekParameters(0, Long.MAX_VALUE); + /** Default parameters. */ + public static final SeekParameters DEFAULT = EXACT; + + /** + * The maximum time that the actual position seeked to may precede the requested seek position, in + * microseconds. + */ + public final long toleranceBeforeUs; + /** + * The maximum time that the actual position seeked to may exceed the requested seek position, in + * microseconds. + */ + public final long toleranceAfterUs; + + /** + * @param toleranceBeforeUs The maximum time that the actual position seeked to may precede the + * requested seek position, in microseconds. Must be non-negative. + * @param toleranceAfterUs The maximum time that the actual position seeked to may exceed the + * requested seek position, in microseconds. Must be non-negative. + */ + public SeekParameters(long toleranceBeforeUs, long toleranceAfterUs) { + Assertions.checkArgument(toleranceBeforeUs >= 0); + Assertions.checkArgument(toleranceAfterUs >= 0); + this.toleranceBeforeUs = toleranceBeforeUs; + this.toleranceAfterUs = toleranceAfterUs; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 338c708864..69369d4229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -738,6 +738,11 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getPlaybackParameters(); } + @Override + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + player.setSeekParameters(seekParameters); + } + @Override public void stop() { player.stop(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 0d94b8fa03..1ea83bf1ec 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -125,6 +126,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setSeekParameters(SeekParameters seekParameters) { + throw new UnsupportedOperationException(); + } + @Override public void stop() { throw new UnsupportedOperationException(); From aa01fb85dc816e035ab8ab2f75a30ca256953b8b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 06:51:51 -0800 Subject: [PATCH 179/417] Add an option to turn off hiding controls during ads Issue: #3532 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178372763 --- RELEASENOTES.md | 2 + .../exoplayer2/ui/SimpleExoPlayerView.java | 122 ++++++++++-------- library/ui/src/main/res/values/attrs.xml | 1 + 3 files changed, 70 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0caad888d9..a464b7e826 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,8 @@ * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). ### 2.6.0 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index dcc1c62569..1f67b83ba0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -56,146 +56,144 @@ import java.util.List; /** * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - *

      - * A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

      A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. * *

      Attributes

      + * * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: + * *

      + * *

        *
      • {@code use_artwork} - Whether artwork is used if available in audio streams. *
          - *
        • Corresponding method: {@link #setUseArtwork(boolean)}
        • - *
        • Default: {@code true}
        • + *
        • Corresponding method: {@link #setUseArtwork(boolean)} + *
        • Default: {@code true} *
        - *
      • *
      • {@code default_artwork} - Default artwork to use if no artwork available in audio * streams. *
          - *
        • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
        • - *
        • Default: {@code null}
        • + *
        • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
        • Default: {@code null} *
        - *
      • *
      • {@code use_controller} - Whether the playback controls can be shown. *
          - *
        • Corresponding method: {@link #setUseController(boolean)}
        • - *
        • Default: {@code true}
        • + *
        • Corresponding method: {@link #setUseController(boolean)} + *
        • Default: {@code true} *
        - *
      • *
      • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. *
          - *
        • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
        • - *
        • Default: {@code true}
        • + *
        • Corresponding method: {@link #setControllerHideOnTouch(boolean)} + *
        • Default: {@code true} *
        - *
      • *
      • {@code auto_show} - Whether the playback controls are automatically shown when * playback starts, pauses, ends, or fails. If set to false, the playback controls can be * manually operated with {@link #showController()} and {@link #hideController()}. *
          - *
        • Corresponding method: {@link #setControllerAutoShow(boolean)}
        • - *
        • Default: {@code true}
        • + *
        • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
        • Default: {@code true} + *
        + *
      • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
          + *
        • Corresponding method: {@link #setControllerHideDuringAds(boolean)} + *
        • Default: {@code true} *
        - *
      • *
      • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. *
          - *
        • Corresponding method: {@link #setResizeMode(int)}
        • - *
        • Default: {@code fit}
        • + *
        • Corresponding method: {@link #setResizeMode(int)} + *
        • Default: {@code fit} *
        - *
      • *
      • {@code surface_type} - The type of surface view used for video playbacks. Valid * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} * is recommended for audio only applications, since creating the surface can be expensive. * Using {@code surface_view} is recommended for video applications. *
          - *
        • Corresponding method: None
        • - *
        • Default: {@code surface_view}
        • + *
        • Corresponding method: None + *
        • Default: {@code surface_view} *
        - *
      • *
      • {@code shutter_background_color} - The background color of the {@code exo_shutter} * view. *
          - *
        • Corresponding method: {@link #setShutterBackgroundColor(int)}
        • - *
        • Default: {@code unset}
        • + *
        • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
        • Default: {@code unset} *
        - *
      • *
      • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
          - *
        • Corresponding method: None
        • - *
        • Default: {@code R.id.exo_simple_player_view}
        • + *
        • Corresponding method: None + *
        • Default: {@code R.id.exo_simple_player_view} *
        *
      • {@code controller_layout_id} - Specifies the id of the layout resource to be * inflated by the child {@link PlaybackControlView}. See below for more details. *
          - *
        • Corresponding method: None
        • - *
        • Default: {@code R.id.exo_playback_control_view}
        • + *
        • Corresponding method: None + *
        • Default: {@code R.id.exo_playback_control_view} *
        *
      • All attributes that can be set on a {@link PlaybackControlView} can also be set on a * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
      • *
      * *

      Overriding the layout file

      + * * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain * configurations, you can define {@code exo_simple_player_view.xml} layout files in your * application {@code res/layout*} directories. These layouts will override the one provided by the * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and * binds its children by looking for the following ids: + * *

      + * *

        *
      • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video * or album art of the media being played, and the configured {@code resize_mode}. The video * surface view is inflated into this frame as its first child. *
          - *
        • Type: {@link AspectRatioFrameLayout}
        • + *
        • Type: {@link AspectRatioFrameLayout} *
        - *
      • *
      • {@code exo_shutter} - A view that's made visible when video should be hidden. This * view is typically an opaque view that covers the video surface view, thereby obscuring it * when visible. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_subtitles} - Displays subtitles. *
          - *
        • Type: {@link SubtitleView}
        • + *
        • Type: {@link SubtitleView} *
        - *
      • *
      • {@code exo_artwork} - Displays album art. *
          - *
        • Type: {@link ImageView}
        • + *
        • Type: {@link ImageView} *
        - *
      • *
      • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as - * {@code rewind_increment} will not be automatically propagated through to this instance. If - * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. *
          - *
        • Type: {@link PlaybackControlView}
        • + *
        • Type: {@link PlaybackControlView} *
        - *
      • *
      • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
          - *
        • Type: {@link FrameLayout}
        • + *
        • Type: {@link FrameLayout} *
        - *
      • *
      - *

      - * All child views are optional and so can be omitted if not required, however where defined they + * + *

      All child views are optional and so can be omitted if not required, however where defined they * must be of the expected type. * *

      Specifying a custom layout file

      + * * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a * single instance in a layout file. This is achieved by setting the {@code player_layout_id} @@ -224,6 +222,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private Bitmap defaultArtwork; private int controllerShowTimeoutMs; private boolean controllerAutoShow; + private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { @@ -267,6 +266,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; boolean controllerHideOnTouch = true; boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -288,6 +288,8 @@ public final class SimpleExoPlayerView extends FrameLayout { controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); } finally { a.recycle(); } @@ -358,6 +360,7 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.controllerHideOnTouch = controllerHideOnTouch; this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; this.useController = useController && controller != null; hideController(); } @@ -649,6 +652,16 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerAutoShow = controllerAutoShow; } + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -784,8 +797,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { - if (isPlayingAd()) { - // Never show the controller if an ad is currently playing. + if (isPlayingAd() && controllerHideDuringAds) { return; } if (useController) { @@ -956,7 +968,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } else { maybeShowController(false); @@ -965,7 +977,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } } diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 525f95768c..1ab3854d21 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -47,6 +47,7 @@ + From e419154b86b0aad7ccd0cdcdda8db25d935fedb8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 07:57:06 -0800 Subject: [PATCH 180/417] Make DashMediaSource.Builder a factory for DashMediaSources This is in preparation for supporting non-extractor MediaSources for ads in AdsMediaSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178377627 --- .../exoplayer2/castdemo/PlayerManager.java | 7 +- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../exoplayer2/source/ads/AdsMediaSource.java | 65 +++--- .../source/dash/DashMediaSource.java | 219 ++++++++++-------- .../playbacktests/gts/DashTestRunner.java | 5 +- 5 files changed, 159 insertions(+), 146 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 0f4adfae99..7d63150201 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -393,10 +393,9 @@ import java.util.ArrayList; new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY)) .build(); case DemoUtil.MIME_TYPE_DASH: - return DashMediaSource.Builder - .forManifestUri(uri, DATA_SOURCE_FACTORY, - new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY)) - .build(); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); case DemoUtil.MIME_TYPE_HLS: return HlsMediaSource.Builder .forDataSource(uri, DATA_SOURCE_FACTORY) 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 0623f48a51..1be6df8437 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 @@ -371,11 +371,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_DASH: - return DashMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: return HlsMediaSource.Builder .forDataSource(uri, mediaDataSourceFactory) 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 54a8fd96ae..c701d6ca64 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,6 +44,31 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { + /** Factory for creating {@link MediaSource}s to play ad media. */ + public interface MediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the media or manifest to play. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. + * @return The new media source. + */ + MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link + * C#TYPE_OTHER}. + * + * @return The content types supported by media sources created by this factory. + */ + int[] getSupportedTypes(); + } + /** Listener for ads media source events. */ public interface EventListener extends MediaSourceEventListener { @@ -77,7 +102,7 @@ public final class AdsMediaSource implements MediaSource { @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final AdMediaSourceFactory adMediaSourceFactory; + private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -138,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -186,7 +211,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); + adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -371,44 +396,16 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Factory for {@link MediaSource}s for loading ad media. - */ - private interface AdMediaSourceFactory { - - /** - * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. - * - * @param uri The URI of the ad. - * @param handler A handler for listener events. May be null if delivery of events is not - * required. - * @param listener A listener for events. May be null if delivery of events is not required. - * @return The new media source. - */ - MediaSource createAdMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); - - /** - * Returns the content types supported by media sources created by this factory. Each element - * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or - * {@link C#TYPE_OTHER}. - * - * @return The content types supported by the factory. - */ - int[] getSupportedTypes(); - - } - - private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } @Override - public MediaSource createAdMediaSource( + public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { return new ExtractorMediaSource.Builder(uri, dataSourceFactory) .setEventListener(handler, listener) 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 af1a445b9f..9c0c58c87b 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 @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; @@ -64,62 +65,35 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * Builder for {@link DashMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link DashMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final DashManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser manifestParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link DashMediaSource}s. * - * @param manifest The manifest. {@link DashManifest#dynamic} must be false. * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideloadedManifest(DashManifest manifest, - DashChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.dynamic); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(DashManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + DashChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } /** @@ -127,95 +101,140 @@ public final class DashMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. - * @return This builder. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is - * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser( + public Factory setManifestParser( ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source - * loads data from multiple streams (video, audio etc...). The default is an instance of - * {@link DefaultCompositeSequenceableLoaderFactory}. + * loads data from multiple streams (video, audio etc...). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. * - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). - * @return This builder. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCompositeSequenceableLoaderFactory( + public Factory setCompositeSequenceableLoaderFactory( CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); return this; } /** - * Builds a new {@link DashMediaSource} using the current parameters. - *

      - * After this call, the builder should not be re-used. + * Returns a new {@link DashMediaSource} using the current parameters and the specified + * sideloaded manifest. * - * @return The newly built {@link DashMediaSource}. + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. */ - public DashMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { - manifestParser = new DashManifestParser(); - } - if (compositeSequenceableLoaderFactory == null) { - compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - } - return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - livePresentationDelayMs, eventHandler, eventListener); + public DashMediaSource createMediaSource( + DashManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.dynamic); + isCreateCalled = true; + return new DashMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); } + /** + * Returns a new {@link DashMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link DashMediaSource}. + */ + public DashMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link DashMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + */ + @Override + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH}; + } } /** @@ -283,7 +302,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -303,7 +322,7 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -327,7 +346,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -356,7 +375,7 @@ public final class DashMediaSource implements MediaSource { * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -387,7 +406,7 @@ public final class DashMediaSource implements MediaSource { * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 215d8a0518..8973853245 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,11 +316,10 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return DashMediaSource.Builder - .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + return new DashMediaSource.Factory(chunkSourceFactory, manifestDataSourceFactory) .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) .setLivePresentationDelayMs(0) - .build(); + .createMediaSource(manifestUri); } @Override From 8947950b5277fbeb01cf4cafa070bb7db1da094e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 08:32:46 -0800 Subject: [PATCH 181/417] Make SsMediaSource.Builder a factory for SsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178380856 --- .../exoplayer2/castdemo/PlayerManager.java | 7 +- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../source/smoothstreaming/SsMediaSource.java | 210 ++++++++++-------- 3 files changed, 122 insertions(+), 104 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 7d63150201..20ef72f9f2 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -388,10 +388,9 @@ import java.util.ArrayList; Uri uri = Uri.parse(sample.uri); switch (sample.mimeType) { case DemoUtil.MIME_TYPE_SS: - return SsMediaSource.Builder - .forManifestUri(uri, DATA_SOURCE_FACTORY, - new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY)) - .build(); + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); case DemoUtil.MIME_TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) 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 1be6df8437..38938bd367 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 @@ -365,11 +365,10 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return SsMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), 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 10772ba36c..9932db7869 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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; @@ -55,62 +56,35 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } - /** - * Builder for {@link SsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SsMediaSource}. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final SsManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser manifestParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link SsMediaSource}s. * - * @param manifest The manifest. {@link SsManifest#isLive} must be false. * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideLoadedManifest(SsManifest manifest, - SsChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.isLive); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(SsManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + SsChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } /** @@ -118,90 +92,136 @@ public final class SsMediaSource implements MediaSource, * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the * default start position should precede the end of the live window. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is an instance of - * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + public Factory setManifestParser(ParsingLoadable.Parser manifestParser) { + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source - * loads data from multiple streams (video, audio etc...). The default is an instance of - * {@link DefaultCompositeSequenceableLoaderFactory}. + * loads data from multiple streams (video, audio etc.). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. * - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). - * @return This builder. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc.). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCompositeSequenceableLoaderFactory( + public Factory setCompositeSequenceableLoaderFactory( CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); return this; } /** - * Builds a new {@link SsMediaSource} using the current parameters. - *

      - * After this call, the builder should not be re-used. + * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded + * manifest. * - * @return The newly built {@link SsMediaSource}. + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + * @throws IllegalArgumentException If {@link SsManifest#isLive} is true. */ - public SsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { + public SsMediaSource createMediaSource( + SsManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.isLive); + isCreateCalled = true; + return new SsMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link SsMediaSource}. + */ + public SsMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + */ + @Override + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { manifestParser = new SsManifestParser(); } - if (compositeSequenceableLoaderFactory == null) { - compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - } - return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - livePresentationDelayMs, eventHandler, eventListener); + return new SsMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_SS}; } } @@ -252,7 +272,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -272,7 +292,7 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -296,7 +316,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -323,7 +343,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -352,7 +372,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( From ba32d95dc476c452dd1e4e97229e04d0cd1c2632 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 09:07:55 -0800 Subject: [PATCH 182/417] Make HlsMediaSource.Builder a factory for HlsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178384204 --- .../exoplayer2/castdemo/PlayerManager.java | 4 +- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/source/hls/HlsMediaSource.java | 171 +++++++++--------- 3 files changed, 85 insertions(+), 96 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 20ef72f9f2..aec53c8e8a 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -396,9 +396,7 @@ import java.util.ArrayList; new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) .createMediaSource(uri); case DemoUtil.MIME_TYPE_HLS: - return HlsMediaSource.Builder - .forDataSource(uri, DATA_SOURCE_FACTORY) - .build(); + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_VIDEO_MP4: return new ExtractorMediaSource.Builder(uri, DATA_SOURCE_FACTORY).build(); default: { 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 38938bd367..215c4708e8 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 @@ -375,10 +375,8 @@ public class PlayerActivity extends Activity implements OnClickListener, buildDataSourceFactory(false)) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: - return HlsMediaSource.Builder - .forDataSource(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) .setEventListener(mainHandler, eventLogger) 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 1cddf6e94e..4c14d2029e 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; @@ -50,68 +52,54 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } - /** - * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link HlsMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri manifestUri; private final HlsDataSourceFactory hlsDataSourceFactory; private HlsExtractorFactory extractorFactory; - private ParsingLoadable.Parser playlistParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser playlistParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private boolean allowChunklessPreparation; - - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link DataSource.Factory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory A data source factory that will be wrapped by a - * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, - * segments and keys. - * @return A new builder. + * @param dataSourceFactory A data source factory that will be wrapped by a {@link + * DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and + * keys. */ - public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { - return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultHlsDataSourceFactory(dataSourceFactory)); } /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link HlsDataSourceFactory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for * manifests, segments and keys. - * @return A new builder. */ - public static Builder forHlsDataSource(Uri manifestUri, - HlsDataSourceFactory dataSourceFactory) { - return new Builder(manifestUri, dataSourceFactory); - } - - private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { - this.manifestUri = manifestUri; - this.hlsDataSourceFactory = hlsDataSourceFactory; + public Factory(HlsDataSourceFactory hlsDataSourceFactory) { + this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory); + extractorFactory = HlsExtractorFactory.DEFAULT; minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } /** - * Sets the factory for {@link Extractor}s for the segments. Default value is - * {@link HlsExtractorFactory#DEFAULT}. + * Sets the factory for {@link Extractor}s for the segments. The default value is {@link + * HlsExtractorFactory#DEFAULT}. * * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the - * segments. - * @return This builder. + * segments. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { - this.extractorFactory = extractorFactory; + public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorFactory = Assertions.checkNotNull(extractorFactory); return this; } @@ -119,54 +107,46 @@ public final class HlsMediaSource implements MediaSource, * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. - * @return This builder. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the parser to parse HLS playlists. The default is an instance of - * {@link HlsPlaylistParser}. + * Sets the parser to parse HLS playlists. The default is an instance of {@link + * HlsPlaylistParser}. * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { - this.playlistParser = playlistParser; + public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { + Assertions.checkState(!isCreateCalled); + this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source - * loads data from multiple streams (video, audio etc...). The default is an instance of - * {@link DefaultCompositeSequenceableLoaderFactory}. + * loads data from multiple streams (video, audio etc...). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. * - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). - * @return This builder. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCompositeSequenceableLoaderFactory( + public Factory setCompositeSequenceableLoaderFactory( CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); return this; } @@ -175,35 +155,44 @@ public final class HlsMediaSource implements MediaSource, * will be enabled for streams that provide sufficient information in their master playlist. * * @param allowChunklessPreparation Whether chunkless preparation is allowed. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setAllowChunklessPreparation(boolean allowChunklessPreparation) { + public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) { + Assertions.checkState(!isCreateCalled); this.allowChunklessPreparation = allowChunklessPreparation; return this; } /** - * Builds a new {@link HlsMediaSource} using the current parameters. - *

      - * After this call, the builder should not be re-used. + * Returns a new {@link HlsMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @return The newly built {@link HlsMediaSource}. + * @return The new {@link HlsMediaSource}. */ - public HlsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - if (extractorFactory == null) { - extractorFactory = HlsExtractorFactory.DEFAULT; - } + public MediaSource createMediaSource(Uri playlistUri) { + return createMediaSource(playlistUri, null, null); + } + + /** + * Returns a new {@link HlsMediaSource} using the current parameters. + * + * @param playlistUri The playlist {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link HlsMediaSource}. + */ + @Override + public MediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } - if (compositeSequenceableLoaderFactory == null) { - compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - } return new HlsMediaSource( - manifestUri, + playlistUri, hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, @@ -214,6 +203,10 @@ public final class HlsMediaSource implements MediaSource, allowChunklessPreparation); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_HLS}; + } } /** @@ -240,7 +233,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource( @@ -261,7 +254,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource( @@ -286,7 +279,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource( From f8834dacc64027efaeef373c26d68d2abe93e920 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:08:01 -0800 Subject: [PATCH 183/417] Use surfaceless context in DummySurface, if available Issue: #3558 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178604607 --- RELEASENOTES.md | 2 + .../exoplayer2/video/DummySurface.java | 108 ++++++++++-------- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a464b7e826..f6fb6f3611 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Use surfaceless context for secure DummySurface, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index cc50443296..2c172c086b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -24,6 +24,7 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; +import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -56,10 +57,13 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; +import android.support.annotation.IntDef; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import javax.microedition.khronos.egl.EGL10; /** @@ -70,16 +74,27 @@ public final class DummySurface extends Surface { private static final String TAG = "DummySurface"; + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - private static boolean secureSupported; - private static boolean secureSupportedInitialized; + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + private @interface SecureMode {} + + private static final int SECURE_MODE_NONE = 0; + private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + private static final int SECURE_MODE_PROTECTED_PBUFFER = 2; /** * Whether the surface is secure. */ public final boolean secure; + private static @SecureMode int secureMode; + private static boolean secureModeInitialized; + private final DummySurfaceThread thread; private boolean threadReleased; @@ -90,11 +105,11 @@ public final class DummySurface extends Surface { * @return Whether the device supports secure dummy surfaces. */ public static synchronized boolean isSecureSupported(Context context) { - if (!secureSupportedInitialized) { - secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); - secureSupportedInitialized = true; + if (!secureModeInitialized) { + secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); + secureModeInitialized = true; } - return secureSupported; + return secureMode != SECURE_MODE_NONE; } /** @@ -113,7 +128,7 @@ public final class DummySurface extends Surface { assertApiLevel17OrHigher(); Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); - return thread.init(secure); + return thread.init(secure ? secureMode : SECURE_MODE_NONE); } private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { @@ -143,33 +158,34 @@ public final class DummySurface extends Surface { } } - /** - * Returns whether use of secure dummy surfaces should be enabled. - * - * @param context Any {@link Context}. - */ @TargetApi(24) - private static boolean enableSecureDummySurfaceV24(Context context) { + private static @SecureMode int getSecureModeV24(Context context) { if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. // Moto Z XT1650 is also affected. See // https://github.com/google/ExoPlayer/issues/3215. - return false; + return SECURE_MODE_NONE; } if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. See - // https://github.com/google/ExoPlayer/issues/3215. - return false; + // Pre API level 26 devices were not well tested unless they supported VR mode. + return SECURE_MODE_NONE; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { - // EGL_EXT_protected_content is required to enable secure dummy surfaces. - return false; + if (eglExtensions == null) { + return SECURE_MODE_NONE; } - return true; + if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) { + return SECURE_MODE_NONE; + } + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may + // require support for EXT_protected_surface, but in practice it works on some devices that + // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558. + return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT) + ? SECURE_MODE_SURFACELESS_CONTEXT + : SECURE_MODE_PROTECTED_PBUFFER; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, @@ -195,12 +211,12 @@ public final class DummySurface extends Surface { textureIdHolder = new int[1]; } - public DummySurface init(boolean secure) { + public DummySurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), this); boolean wasInterrupted = false; synchronized (this) { - handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget(); + handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); while (surface == null && initException == null && initError == null) { try { wait(); @@ -236,7 +252,7 @@ public final class DummySurface extends Surface { switch (msg.what) { case MSG_INIT: try { - initInternal(msg.arg1 != 0); + initInternal(/* secureMode= */ msg.arg1); } catch (RuntimeException e) { Log.e(TAG, "Failed to initialize dummy surface", e); initException = e; @@ -266,7 +282,7 @@ public final class DummySurface extends Surface { } } - private void initInternal(boolean secure) { + private void initInternal(@SecureMode int secureMode) { display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); @@ -294,43 +310,45 @@ public final class DummySurface extends Surface { EGLConfig config = configs[0]; int[] glAttributes; - if (secure) { + if (secureMode == SECURE_MODE_NONE) { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE}; } else { - glAttributes = new int[] { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE}; + glAttributes = + new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; } context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); Assertions.checkState(context != null, "eglCreateContext failed"); - int[] pbufferAttributes; - if (secure) { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, - EGL_NONE}; + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL_NO_SURFACE; } else { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_NONE}; + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; + } else { + pbufferAttributes = new int[] {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + } + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + surface = pbuffer; } - pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); - boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); + boolean eglMadeCurrent = eglMakeCurrent(display, surface, surface, context); Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); glGenTextures(1, textureIdHolder, 0); surfaceTexture = new SurfaceTexture(textureIdHolder[0]); surfaceTexture.setOnFrameAvailableListener(this); - surface = new DummySurface(this, surfaceTexture, secure); + this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE); } private void releaseInternal() { From 8c7fe8a258e88fa513c4a1a88a4fe70c65eec2d4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:23:00 -0800 Subject: [PATCH 184/417] Make ExtractorMediaSource.Builder a factory for ExtractorMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178605481 --- .../exoplayer2/castdemo/PlayerManager.java | 2 +- .../exoplayer2/imademo/PlayerManager.java | 3 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 185 +++++++++--------- .../exoplayer2/source/ads/AdsMediaSource.java | 26 +-- 8 files changed, 118 insertions(+), 133 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index aec53c8e8a..548482f61f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -398,7 +398,7 @@ import java.util.ArrayList; case DemoUtil.MIME_TYPE_HLS: return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource.Builder(uri, DATA_SOURCE_FACTORY).build(); + return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + sample.mimeType); } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 6b840830c5..ec21f6d265 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -70,7 +70,8 @@ import com.google.android.exoplayer2.util.Util; // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); + new ExtractorMediaSource.Factory(dataSourceFactory) + .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, 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 215c4708e8..a60ae0c876 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 @@ -378,9 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: - return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); default: { throw new IllegalStateException("Unsupported type: " + type); } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index bd6e698dc6..fd18a3b1ae 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index aa61df74d9..d3ab421655 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 746f3d273f..3cc1a1d340 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -105,10 +106,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index b97d957ec4..a2d7941c3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -103,129 +104,113 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; - /** - * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link ExtractorMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private ExtractorsFactory extractorsFactory; + private @Nullable ExtractorsFactory extractorsFactory; + private @Nullable String customCacheKey; private int minLoadableRetryCount; - @Nullable private Handler eventHandler; - @Nullable private MediaSourceEventListener eventListener; - @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a new factory for {@link ExtractorMediaSource}s. + * * @param dataSourceFactory A factory for {@link DataSource}s to read the media. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory) { - this.uri = uri; + public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the factory for {@link Extractor}s to process the media stream. Default value is an - * instance of {@link DefaultExtractorsFactory}. - * - * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the - * possible formats are known, pass a factory that instantiates extractors for those - * formats. - * @return This builder. - */ - public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { - this.extractorsFactory = extractorsFactory; - return this; - } - - /** - * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. - * Default value is null. - * - * @param customCacheKey A custom key that uniquely identifies the original stream. Used for - * cache indexing. - * @return This builder. - */ - public Builder setCustomCacheKey(String customCacheKey) { - this.customCacheKey = customCacheKey; - return this; - } - - /** - * Sets the number of bytes that should be loaded between each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value - * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. * * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between - * each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @return This builder. + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; return this; } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. + * @param uri The {@link Uri}. + * @return The new {@link ExtractorMediaSource}. */ - @Deprecated - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); - return this; + public MediaSource createMediaSource(Uri uri) { + return createMediaSource(uri, null, null); } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. * + * @param uri The {@link Uri}. * @param eventHandler A handler for events. * @param eventListener A listener of events. - * @return This builder. + * @return The new {@link ExtractorMediaSource}. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Builds a new {@link ExtractorMediaSource} using the current parameters. - *

      - * After this call, the builder should not be re-used. - * - * @return The newly built {@link ExtractorMediaSource}. - */ - public ExtractorMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (extractorsFactory == null) { extractorsFactory = new DefaultExtractorsFactory(); } @@ -234,6 +219,10 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe continueLoadingCheckIntervalBytes); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } } /** @@ -244,11 +233,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -262,11 +255,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener, String customCacheKey) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); @@ -285,12 +282,18 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + String customCacheKey, + int continueLoadingCheckIntervalBytes) { this( uri, dataSourceFactory, 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 c701d6ca64..5611bedcca 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 @@ -163,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -396,28 +396,4 @@ public final class AdsMediaSource implements MediaSource { } - private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { - - private final DataSource.Factory dataSourceFactory; - - public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { - this.dataSourceFactory = dataSourceFactory; - } - - @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory) - .setEventListener(handler, listener) - .build(); - } - - @Override - public int[] getSupportedTypes() { - // Only ExtractorMediaSource is supported. - return new int[] {C.TYPE_OTHER}; - } - - } - } From 2e3667eeff663a154395b48d9cb1e46cf0d8b99a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 11 Dec 2017 05:31:54 -0800 Subject: [PATCH 185/417] Expose ability to get adjusted seek position from MediaPeriod Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178606133 --- .../exoplayer2/ExoPlayerImplInternal.java | 16 ++++++++++---- .../source/ClippingMediaPeriod.java | 21 +++++++++++++++++++ .../source/DeferredMediaPeriod.java | 6 ++++++ .../source/ExtractorMediaPeriod.java | 7 +++++++ .../exoplayer2/source/MediaPeriod.java | 14 +++++++++++++ .../exoplayer2/source/MergingMediaPeriod.java | 6 ++++++ .../source/SingleSampleMediaPeriod.java | 10 +++++++-- .../source/dash/DashMediaPeriod.java | 6 ++++++ .../exoplayer2/source/hls/HlsMediaPeriod.java | 6 ++++++ .../source/smoothstreaming/SsMediaPeriod.java | 6 ++++++ .../exoplayer2/testutil/FakeMediaPeriod.java | 6 ++++++ .../testutil/FakeSimpleExoPlayer.java | 8 +++++-- 12 files changed, 104 insertions(+), 8 deletions(-) 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 8f59451c48..20d75ec1bd 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 @@ -660,10 +660,18 @@ import java.io.IOException; periodPositionUs = 0; } try { - if (periodId.equals(playbackInfo.periodId) - && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { - // Seek position equals the current position. Do nothing. - return; + if (periodId.equals(playbackInfo.periodId)) { + long adjustedPeriodPositionUs = periodPositionUs; + if (playingPeriodHolder != null) { + adjustedPeriodPositionUs = + playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( + adjustedPeriodPositionUs, SeekParameters.DEFAULT); + } + if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + // Seek will be performed to the current position. Do nothing. + periodPositionUs = playbackInfo.positionUs; + return; + } } long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 539c4841e9..b1c12d6192 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; @@ -170,6 +171,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return seekUs - startUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs( + positionUs + startUs, adjustSeekParameters(positionUs + startUs, seekParameters)); + } + @Override public long getNextLoadPositionUs() { long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); @@ -202,6 +209,20 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; } + private SeekParameters adjustSeekParameters(long positionUs, SeekParameters seekParameters) { + long toleranceBeforeMs = Math.min(positionUs - startUs, seekParameters.toleranceBeforeUs); + long toleranceAfterMs = + endUs == C.TIME_END_OF_SOURCE + ? seekParameters.toleranceAfterUs + : Math.min(endUs - positionUs, seekParameters.toleranceAfterUs); + if (toleranceBeforeMs == seekParameters.toleranceBeforeUs + && toleranceAfterMs == seekParameters.toleranceAfterUs) { + return seekParameters; + } else { + return new SeekParameters(toleranceBeforeMs, toleranceAfterMs); + } + } + private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index 32a180b956..1895f10d53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -114,6 +115,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod.seekToUs(positionUs); } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + @Override public long getNextLoadPositionUs() { return mediaPeriod.getNextLoadPositionUs(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 6b9aeb39da..f8021c24df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -21,6 +21,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; @@ -369,6 +370,12 @@ import java.util.Arrays; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + // Treat all seeks into non-seekable media as being to t=0. + return seekMap.isSeekable() ? positionUs : 0; + } + // SampleStream methods. /* package */ boolean isReady(int track) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 54b34bc531..a5b2314d78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; @@ -149,6 +150,19 @@ public interface MediaPeriod extends SequenceableLoader { */ long seekToUs(long positionUs); + /** + * Returns the position to which a seek will be performed, given the specified seek position and + * {@link SeekParameters}. + * + *

      This method should only be called after the period has been prepared. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. Implementations may + * apply seek parameters on a best effort basis. + * @return The actual position to which a seek will be performed, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + // SequenceableLoader interface. Overridden to provide more specific documentation. /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 5ac9fc8d97..cc0c63ef41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -192,6 +193,11 @@ import java.util.IdentityHashMap; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return enabledPeriods[0].getAdjustedSeekPositionUs(positionUs, seekParameters); + } + // MediaPeriod.Callback implementation @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 7b8b54eedc..9fff3b4d85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -20,6 +20,7 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -153,11 +154,16 @@ import java.util.Arrays; @Override public long seekToUs(long positionUs) { for (int i = 0; i < sampleStreams.size(); i++) { - sampleStreams.get(i).seekToUs(positionUs); + sampleStreams.get(i).reset(); } return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // Loader.Callback implementation. @Override @@ -208,7 +214,7 @@ import java.util.Arrays; private int streamState; - public void seekToUs(long positionUs) { + public void reset() { if (streamState == STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_SEND_SAMPLE; } 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 f320ad2844..2b7b16228e 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 @@ -20,6 +20,7 @@ import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -306,6 +307,11 @@ import java.util.Map; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // SequenceableLoader.Callback implementation. @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 24acf0f84d..11602c722f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -244,6 +245,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // HlsSampleStreamWrapper.Callback implementation. @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 564993befe..5ee60bdeed 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.util.Base64; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -182,6 +183,11 @@ import java.util.ArrayList; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // SequenceableLoader.Callback implementation @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d34c1d1c0c..0a5dcd5741 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; @@ -176,6 +177,11 @@ public class FakeMediaPeriod implements MediaPeriod { return positionUs + seekOffsetUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + @Override public long getNextLoadPositionUs() { Assert.assertTrue(prepared); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 1e7e0cd933..d568770219 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -422,8 +422,12 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { SampleStream[] sampleStreams = new SampleStream[renderers.length]; boolean[] mayRetainStreamFlags = new boolean[renderers.length]; Arrays.fill(mayRetainStreamFlags, true); - mediaPeriod.selectTracks(selectorResult.selections.getAll(), mayRetainStreamFlags, - sampleStreams, new boolean[renderers.length], 0); + mediaPeriod.selectTracks( + selectorResult.selections.getAll(), + mayRetainStreamFlags, + sampleStreams, + new boolean[renderers.length], + /* positionUs = */ 0); eventListenerHandler.post(new Runnable() { @Override public void run() { From a4ae206ebefd22dcc58fd20e6766f42695b15f9f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 07:19:53 -0800 Subject: [PATCH 186/417] Support non-extractor ads in AdsMediaSource and demo apps Issue: #3302 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178615074 --- RELEASENOTES.md | 2 + demos/ima/build.gradle | 2 + .../exoplayer2/imademo/PlayerManager.java | 62 +++++++++++++++++-- .../exoplayer2/demo/PlayerActivity.java | 44 +++++++++---- .../exoplayer2/source/ads/AdsMediaSource.java | 58 ++++++++++++----- 5 files changed, 136 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f6fb6f3611..700bd025a9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,8 @@ * Use surfaceless context for secure DummySurface, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index c32228de28..536d8d4662 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -43,5 +43,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index ec21f6d265..51959451d1 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -17,13 +17,21 @@ package com.google.android.exoplayer2.imademo; import android.content.Context; import android.net.Uri; +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -35,12 +43,12 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; -/** - * Manages the {@link ExoPlayer}, the IMA plugin and all video playback. - */ -/* package */ final class PlayerManager { +/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ +/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { private final ImaAdsLoader adsLoader; + private final DataSource.Factory manifestDataSourceFactory; + private final DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; private long contentPosition; @@ -48,6 +56,14 @@ import com.google.android.exoplayer2.util.Util; public PlayerManager(Context context) { String adTag = context.getString(R.string.ad_tag_url); adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + manifestDataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, context.getString(R.string.application_name))); + mediaDataSourceFactory = + new DefaultDataSourceFactory( + context, + Util.getUserAgent(context, context.getString(R.string.application_name)), + new DefaultBandwidthMeter()); } public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { @@ -74,8 +90,14 @@ import com.google.android.exoplayer2.util.Util; .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. - MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, - adsLoader, simpleExoPlayerView.getOverlayFrameLayout()); + MediaSource mediaSourceWithAds = + new AdsMediaSource( + contentMediaSource, + /* adMediaSourceFactory= */ this, + adsLoader, + simpleExoPlayerView.getOverlayFrameLayout(), + /* eventHandler= */ null, + /* eventListener= */ null); // Prepare the player with the source. player.seekTo(contentPosition); @@ -99,4 +121,32 @@ import com.google.android.exoplayer2.util.Util; adsLoader.release(); } + // AdsMediaSource.MediaSourceFactory implementation. + + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + @ContentType int type = Util.inferContentType(uri); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } } 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 a60ae0c876..fa3c7d401a 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 @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -332,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener, } MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger); } MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -360,26 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener, updateButtonVisibilities(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + private MediaSource buildMediaSource( + Uri uri, + String overrideExtension, + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener) { @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); default: { throw new IllegalStateException("Unsupported type: " + type); } @@ -466,8 +472,22 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, - mainHandler, eventLogger); + AdsMediaSource.MediaSourceFactory adMediaSourceFactory = + new AdsMediaSource.MediaSourceFactory() { + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return PlayerActivity.this.buildMediaSource( + uri, /* overrideExtension= */ null, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource( + mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); } private void releaseAdsLoader() { 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 5611bedcca..0980e9d011 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 @@ -96,13 +96,13 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; + private final MediaSourceFactory adMediaSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; @Nullable private final Handler eventHandler; @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -119,28 +119,31 @@ public final class AdsMediaSource implements MediaSource { private MediaSource.Listener listener; /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

      - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. * @param adsLoader The loader for ads. * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup) { - this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null); + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup) { + this( + contentMediaSource, + dataSourceFactory, + adsLoader, + adUiViewGroup, + /* eventHandler= */ null, + /* eventListener= */ null); } /** * Constructs a new source that inserts ads linearly with the content specified by {@code - * contentMediaSource}. - * - *

      Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -156,14 +159,41 @@ public final class AdsMediaSource implements MediaSource { ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable EventListener eventListener) { + this( + contentMediaSource, + new ExtractorMediaSource.Factory(dataSourceFactory), + adsLoader, + adUiViewGroup, + eventHandler, + eventListener); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; + this.adMediaSourceFactory = adMediaSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; From e2bba1567e55eea81552a8810949d78f6d0b71db Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 Dec 2017 09:03:15 -0800 Subject: [PATCH 187/417] Add Builder for ImaAdsLoader and allow early requestAds Also fix propagation of ad errors that occur when no player is attached. Issue: #3548 Issue: #3556 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178767997 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 259 +++++++++++++----- 2 files changed, 195 insertions(+), 68 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 700bd025a9..b71faff349 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -51,6 +51,10 @@ * Fix ad loading when there is no preroll. * Add an option to turn off hiding controls during ad playback ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). ### 2.6.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 19dfa1e83f..acfe143952 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 android.content.Context; import android.net.Uri; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.ViewGroup; import android.webkit.WebView; @@ -65,10 +66,80 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { + static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } + /** Builder for {@link ImaAdsLoader}. */ + public static final class Builder { + + private final Context context; + + private @Nullable ImaSdkSettings imaSdkSettings; + private long vastLoadTimeoutMs; + + /** + * Creates a new builder for {@link ImaAdsLoader}. + * + * @param context The context; + */ + public Builder(Context context) { + this.context = Assertions.checkNotNull(context); + vastLoadTimeoutMs = C.TIME_UNSET; + } + + /** + * Sets the IMA SDK settings. The provided settings instance's player type and version fields + * may be overwritten. + * + *

      If this method is not called the default settings will be used. + * + * @param imaSdkSettings The {@link ImaSdkSettings}. + * @return This builder, for convenience. + */ + public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { + this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); + return this; + } + + /** + * Sets the VAST load timeout, in milliseconds. + * + * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. + * @return This builder, for convenience. + * @see AdsRequest#setVastLoadTimeout(float) + */ + public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) { + Assertions.checkArgument(vastLoadTimeoutMs >= 0); + this.vastLoadTimeoutMs = vastLoadTimeoutMs; + return this; + } + + /** + * Returns a new {@link ImaAdsLoader} for the specified ad tag. + * + * @param adTagUri The URI of a compatible ad tag to load. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * information on compatible ad tags. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdTag(Uri adTagUri) { + return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs); + } + + /** + * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. + * + * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of + * making a request via an ad tag URL. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdsResponse(String adsResponse) { + return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs); + } + } + private static final boolean DEBUG = false; private static final String TAG = "ImaAdsLoader"; @@ -94,9 +165,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; - /** - * The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}. - */ + /** The state of ad playback. */ @Retention(RetentionPolicy.SOURCE) @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) private @interface ImaAdState {} @@ -113,7 +182,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private static final int IMA_AD_STATE_PAUSED = 2; - private final Uri adTagUri; + private final @Nullable Uri adTagUri; + private final @Nullable String adsResponse; + private final long vastLoadTimeoutMs; private final Timeline.Period period; private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; @@ -129,6 +200,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private VideoProgressUpdate lastAdProgress; private AdsManager adsManager; + private AdErrorEvent pendingAdErrorEvent; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -144,9 +216,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; - /** - * The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}. - */ + /** The current ad playback state. */ private @ImaAdState int imaAdState; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been @@ -189,13 +259,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** * Creates a new IMA ads loader. * + *

      If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. + * * @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. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this(context, adTagUri, null); + this(context, adTagUri, null, null, C.TIME_UNSET); } /** @@ -207,9 +279,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * 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, ImaSdkSettings imaSdkSettings) { + this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET); + } + + private ImaAdsLoader( + Context context, + @Nullable Uri adTagUri, + @Nullable ImaSdkSettings imaSdkSettings, + @Nullable String adsResponse, + long vastLoadTimeoutMs) { + Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; + this.adsResponse = adsResponse; + this.vastLoadTimeoutMs = vastLoadTimeoutMs; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); imaSdkFactory = ImaSdkFactory.getInstance(); @@ -238,6 +324,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * Requests ads, if they have not already been requested. Must be called on the main thread. + * + *

      Ads will be requested automatically when the player is prepared if this method has not been + * called, so it is only necessary to call this method if you want to request ads before preparing + * the player + * + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public void requestAds(ViewGroup adUiViewGroup) { + if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { + // Ads have already been requested. + return; + } + adDisplayContainer.setAdContainer(adUiViewGroup); + pendingAdRequestContext = new Object(); + AdsRequest request = imaSdkFactory.createAdsRequest(); + if (adTagUri != null) { + request.setAdTagUrl(adTagUri.toString()); + } else /* adsResponse != null */ { + request.setAdsResponse(adsResponse); + } + if (vastLoadTimeoutMs != C.TIME_UNSET) { + request.setVastLoadTimeout(vastLoadTimeoutMs); + } + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); + adsLoader.requestAds(request); + } + // AdsLoader implementation. @Override @@ -268,14 +385,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); + maybeNotifyAdError(); if (adPlaybackState != null) { + // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState.copy()); if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } + } else if (adsManager != null) { + // Ads have loaded but the ads manager is not initialized. + startAdPlayback(); } else { - pendingContentPositionMs = player.getCurrentPosition(); - requestAds(); + // Ads haven't loaded yet, so request them. + requestAds(adUiViewGroup); } } @@ -312,49 +434,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; - - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); - this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - int adGroupIndexForPosition = - getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == 0) { - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - pendingContentPositionMs = C.TIME_UNSET; - // There is no preroll and midroll pod indices start at 1. - podIndexOffset = -1; - } else /* adGroupIndexForPosition > 0 */ { - // Skip ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState.playedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - - // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. Midroll pod indices start at 1. - podIndexOffset = adGroupIndexForPosition - 1; + if (player != null) { + // If a player is attached already, start playback immediately. + startAdPlayback(); } - - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - - updateAdPlaybackState(); } // AdEvent.AdEventListener implementation. @@ -384,14 +470,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); - int adCountInAdGroup = adPodInfo.getTotalAds(); + int adCount = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d( - TAG, - "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } - adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); + adPlaybackState.setAdCount(adGroupIndex, adCount); updateAdPlaybackState(); break; case CONTENT_PAUSE_REQUESTED: @@ -434,14 +518,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } - if (eventListener != null) { - IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); - eventListener.onLoadError(exception); + if (pendingAdErrorEvent == null) { + pendingAdErrorEvent = adErrorEvent; } + maybeNotifyAdError(); } // ContentProgressProvider implementation. @@ -654,18 +739,56 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. - private void requestAds() { - if (pendingAdRequestContext != null) { - // Ad request already in flight. - return; + private void startAdPlayback() { + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + + // Set up the ad playback state, skipping ads based on the start position as required. + pendingContentPositionMs = player.getCurrentPosition(); + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. Midroll pod indices start at 1. + podIndexOffset = adGroupIndexForPosition - 1; + } + + // Start ad playback. + adsManager.init(adsRenderingSettings); + updateAdPlaybackState(); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); + } + } + + private void maybeNotifyAdError() { + if (eventListener != null && pendingAdErrorEvent != null) { + IOException exception = + new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()); + eventListener.onLoadError(exception); + pendingAdErrorEvent = null; } - pendingAdRequestContext = new Object(); - AdsRequest request = imaSdkFactory.createAdsRequest(); - request.setAdTagUrl(adTagUri.toString()); - request.setAdDisplayContainer(adDisplayContainer); - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); } private void updateImaStateForPlayerState() { From f3dc075cabe720002580581463cebf165f1a9ff8 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Dec 2017 11:05:57 -0800 Subject: [PATCH 188/417] Propagate extras from queue item to metadata item. norelnotes=true ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178785377 --- .../mediasession/MediaSessionConnector.java | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) 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 d80487f2bd..2b4409e0fb 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -330,6 +331,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; + private final String metadataExtrasPrefix; private final Map commandMap; private Player player; @@ -356,15 +358,15 @@ public final class MediaSessionConnector { /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. - *

      - * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. + * + *

      Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController) { - this(mediaSession, playbackController, true); + public MediaSessionConnector( + MediaSessionCompat mediaSession, PlaybackController playbackController) { + this(mediaSession, playbackController, true, null); } /** @@ -372,17 +374,23 @@ public final class MediaSessionConnector { * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController A {@link PlaybackController} for handling playback actions, or - * {@code null} if the connector should handle playback actions directly. + * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code + * null} if the connector should handle playback actions directly. * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * {@code false}, you need to maintain the metadata of the media session yourself (provide at * least the duration to allow clients to show a progress bar). + * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active + * queue item to the session metadata. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController, boolean doMaintainMetadata) { + public MediaSessionConnector( + MediaSessionCompat mediaSession, + PlaybackController playbackController, + boolean doMaintainMetadata, + @Nullable String metadataExtrasPrefix) { this.mediaSession = mediaSession; this.playbackController = playbackController != null ? playbackController : new DefaultPlaybackController(); + this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : ""; this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -553,6 +561,25 @@ public final class MediaSessionConnector { MediaSessionCompat.QueueItem queueItem = queue.get(i); if (queueItem.getQueueId() == activeQueueItemId) { MediaDescriptionCompat description = queueItem.getDescription(); + Bundle extras = description.getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value instanceof String) { + builder.putString(metadataExtrasPrefix + key, (String) value); + } else if (value instanceof CharSequence) { + builder.putText(metadataExtrasPrefix + key, (CharSequence) value); + } else if (value instanceof Long) { + builder.putLong(metadataExtrasPrefix + key, (Long) value); + } else if (value instanceof Integer) { + builder.putLong(metadataExtrasPrefix + key, (Integer) value); + } else if (value instanceof Bitmap) { + builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value); + } else if (value instanceof RatingCompat) { + builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value); + } + } + } if (description.getTitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, String.valueOf(description.getTitle())); From 6c4bb2cdec71ecfae08af6bf2104e4d9e6d0c780 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Dec 2017 23:43:16 -0800 Subject: [PATCH 189/417] Update release notes to reflect builder -> factory change ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178866131 --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b71faff349..fce791015f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,9 +11,9 @@ * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` - factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, - `SsMediaSource.Builder`, and `MergingMediaSource`. -* Add Builder to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. +* Add Factory to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, `DashMediaSource`, `SingleSampleMediaSource`. * DASH: * Support in-MPD EventStream. From 318618d7a2412facf1274bd166608b6c2f8f0b7d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 01:59:28 -0800 Subject: [PATCH 190/417] Fix seek/prepare/stop acks when exception is thrown. 1. The player doesn't acknowledge phantom stops when an exception is thrown anymore. 2. It also makes sure it doesn't reset the pendingPrepareCount unless it's actually immediately acknowledging these prepares. 3. It ensures a seek is acknowledged even though an exception is thrown during seeking. Added tests (which previously failed) for all three cases. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178876362 --- .../android/exoplayer2/ExoPlayerTest.java | 100 +++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 121 ++++++++---------- .../testutil/ExoPlayerTestRunner.java | 6 +- 3 files changed, 158 insertions(+), 69 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 714dfff676..dad891718e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -344,6 +346,39 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); } + public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an illegal seek exception by seeking to an invalid position while the media + // source is still being prepared and the player doesn't immediately know it will fail. + // Because the media source prepares immediately, the exception will be thrown when the + // player processed the seek. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_IDLE) + .build(); + final boolean[] onSeekProcessedCalled = new boolean[1]; + EventListener listener = + new DefaultEventListener() { + @Override + public void onSeekProcessed() { + onSeekProcessedCalled[0] = true; + } + }; + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertTrue(onSeekProcessedCalled[0]); + } + public void testSeekDiscontinuity() throws Exception { FakeTimeline timeline = new FakeTimeline(1); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuity") @@ -808,4 +843,69 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } + + public void testReprepareAfterPlaybackError() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testReprepareAfterPlaybackError") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an internal exception by seeking to an invalid position while the media source + // is still being prepared and the player doesn't immediately know it will fail. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource( + new FakeMediaSource(timeline, /* manifest= */ null), + /* resetPosition= */ false, + /* resetState= */ false) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + } + + public void testPlaybackErrorDuringSourceInfoRefreshStillUpdatesTimeline() throws Exception { + final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testPlaybackErrorDuringSourceInfoRefreshStillUpdatesTimeline") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an internal exception by seeking to an invalid position while the media source + // is still being prepared. The error will be thrown while the player handles the new + // source info. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(timeline, /* manifest= */ null); + } + }) + .waitForPlaybackState(Player.STATE_IDLE) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + } } 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 20d75ec1bd..a1fe8c09c5 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 @@ -328,7 +328,7 @@ import java.io.IOException; setSeekParametersInternal((SeekParameters) msg.obj); return true; case MSG_STOP: - stopInternal(/* reset= */ msg.arg1 != 0); + stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); return true; case MSG_RELEASE: releaseInternal(); @@ -353,19 +353,19 @@ import java.io.IOException; } } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); + stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - stopInternal(/* reset= */ false); return true; } catch (IOException e) { Log.e(TAG, "Source error.", e); + stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - stopInternal(/* reset= */ false); return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); + stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - stopInternal(/* reset= */ false); return true; } } @@ -635,49 +635,50 @@ import java.io.IOException; return; } - Pair periodPosition = resolveSeekPosition(seekPosition); - if (periodPosition == null) { - // The seek position was valid for the timeline that it was performed into, but the - // timeline has changed and a suitable seek position could not be resolved in the new one. - setState(Player.STATE_ENDED); - // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal( - /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - eventHandler - .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, playbackInfo) - .sendToTarget(); - return; - } - boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; - int periodIndex = periodPosition.first; - long periodPositionUs = periodPosition.second; - long contentPositionUs = periodPositionUs; - MediaPeriodId periodId = - mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); - if (periodId.isAd()) { - seekPositionAdjusted = true; - periodPositionUs = 0; - } try { - if (periodId.equals(playbackInfo.periodId)) { - long adjustedPeriodPositionUs = periodPositionUs; - if (playingPeriodHolder != null) { - adjustedPeriodPositionUs = - playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( - adjustedPeriodPositionUs, SeekParameters.DEFAULT); - } - if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { - // Seek will be performed to the current position. Do nothing. - periodPositionUs = playbackInfo.positionUs; - return; - } + Pair periodPosition = resolveSeekPosition(seekPosition); + if (periodPosition == null) { + // The seek position was valid for the timeline that it was performed into, but the + // timeline has changed and a suitable seek position could not be resolved in the new one. + setState(Player.STATE_ENDED); + // Reset, but retain the source so that it can still be used should a seek occur. + resetInternal( + /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); + seekPositionAdjusted = true; + return; + } + + int periodIndex = periodPosition.first; + long periodPositionUs = periodPosition.second; + long contentPositionUs = periodPositionUs; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); + if (periodId.isAd()) { + seekPositionAdjusted = true; + periodPositionUs = 0; + } + try { + if (periodId.equals(playbackInfo.periodId)) { + long adjustedPeriodPositionUs = periodPositionUs; + if (playingPeriodHolder != null) { + adjustedPeriodPositionUs = + playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( + adjustedPeriodPositionUs, SeekParameters.DEFAULT); + } + if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + // Seek will be performed to the current position. Do nothing. + periodPositionUs = playbackInfo.positionUs; + return; + } + } + long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); + seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; + periodPositionUs = newPeriodPositionUs; + } finally { + playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); } - long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); - seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; - periodPositionUs = newPeriodPositionUs; } finally { - playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) .sendToTarget(); } @@ -775,12 +776,10 @@ import java.io.IOException; this.seekParameters = seekParameters; } - private void stopInternal(boolean reset) { + private void stopInternal(boolean reset, boolean acknowledgeStop) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - int prepareOrStopAcks = pendingPrepareCount + 1; - pendingPrepareCount = 0; - notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); + notifySourceInfoRefresh(acknowledgeStop); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1011,15 +1010,13 @@ import java.io.IOException; playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); if (oldTimeline == null) { - int processedPrepareAcks = pendingPrepareCount; - pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); + handleSourceInfoRefreshEndedPlayback(); } else { int periodIndex = periodPosition.first; long positionUs = periodPosition.second; @@ -1027,11 +1024,11 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(processedPrepareAcks); + notifySourceInfoRefresh(); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); + handleSourceInfoRefreshEndedPlayback(); } else { Pair defaultPosition = getPeriodPosition(timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); @@ -1041,10 +1038,10 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(processedPrepareAcks); + notifySourceInfoRefresh(); } } else { - notifySourceInfoRefresh(processedPrepareAcks); + notifySourceInfoRefresh(); } return; } @@ -1171,26 +1168,20 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback() { - handleSourceInfoRefreshEndedPlayback(0); - } - - private void handleSourceInfoRefreshEndedPlayback(int prepareAcks) { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - notifySourceInfoRefresh(prepareAcks, playbackInfo); + notifySourceInfoRefresh(); } private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(0); + notifySourceInfoRefresh(/* acknowledgeStop= */ false); } - private void notifySourceInfoRefresh(int prepareOrStopAcks) { - notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); - } - - private void notifySourceInfoRefresh(int prepareOrStopAcks, PlaybackInfo playbackInfo) { + private void notifySourceInfoRefresh(boolean acknowledgeStop) { + int prepareOrStopAcks = pendingPrepareCount + (acknowledgeStop ? 1 : 0); + pendingPrepareCount = 0; eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) .sendToTarget(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index fddeb60bf0..4905fc2233 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -478,9 +478,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } /** - * Blocks the current thread until the action schedule finished. Also returns when an - * {@link ExoPlaybackException} is thrown. This does not release the test runner and the test must - * still call {@link #blockUntilEnded(long)}. + * Blocks the current thread until the action schedule finished. This does not release the test + * runner and the test must still call {@link #blockUntilEnded(long)}. * * @param timeoutMs The maximum time to wait for the action schedule to finish. * @return This test runner. @@ -611,7 +610,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener while (endedCountDownLatch.getCount() > 0) { endedCountDownLatch.countDown(); } - actionScheduleFinishedCountDownLatch.countDown(); } // Player.EventListener From a092262d0b3f3852affb83253574a2abd8b4998c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 02:16:15 -0800 Subject: [PATCH 191/417] Update release notes for current 2.6.1 feature set ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178877884 --- RELEASENOTES.md | 83 +++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fce791015f..95eda228ea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,47 +2,35 @@ ### dev-v2 (not yet released) ### -* Add initial support for chunkless preparation in HLS. This allows an HLS media - source to finish preparation without donwloading any chunks, which might - considerably reduce the initial buffering time - ([#3149](https://github.com/google/ExoPlayer/issues/2980)). -* Add ability for `SequenceableLoader` to reevaluate its buffer and discard - buffered media so that it can be re-buffered in a different quality. -* Replace `DefaultTrackSelector.Parameters` copy methods with a builder. -* Allow more flexible loading strategy when playing media containing multiple - sub-streams, by allowing injection of custom `CompositeSequenceableLoader` - factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, - `SsMediaSource.Factory`, and `MergingMediaSource`. -* Add Factory to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, - `DashMediaSource`, `SingleSampleMediaSource`. -* DASH: - * Support in-MPD EventStream. - * Support time zone designators in ISO8601 UTCTiming elements - ([#3524](https://github.com/google/ExoPlayer/issues/3524)). -* Allow a back-buffer of media to be retained behind the current playback - position, for fast backward seeking. The back-buffer can be configured by - custom `LoadControl` implementations. +* Player interface: + * Add optional parameter to `stop` to reset the player when stopping. + * Add a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. +* Buffering: + * Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured by + custom `LoadControl` implementations. + * Add ability for `SequenceableLoader` to reevaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. + * Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. +* DASH: Support DASH manifest EventStream elements. +* HLS: Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). +* DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods + with a builder. * New Cast extension: Simplifies toggling between local and Cast playbacks. -* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to - use this with `FfmpegAudioRenderer`. -* Support extraction and decoding of Dolby Atmos - ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Add a reason to `EventListener.onTimelineChanged` to distinguish between - initial preparation, reset and dynamic updates. -* DefaultTrackSelector: Support undefined language text track selection when the - preferred language is not available - ([#2980](https://github.com/google/ExoPlayer/issues/2980)). -* Add optional parameter to `Player.stop` to reset the player when stopping. -* Fix handling of playback parameters changes while paused when followed by a - seek. -* Fix playback of live FLV streams that do not contain an audio track - ([#3188](https://github.com/google/ExoPlayer/issues/3188)). + +### 2.6.1 ### + +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. -* CEA-608: Fix handling of row count changes in roll-up mode - ([#3513](https://github.com/google/ExoPlayer/issues/3513)). -* Use surfaceless context for secure DummySurface, if available - ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Support non-ExtractorMediaSource ads ([#3302](https://github.com/google/ExoPlayer/issues/3302)). @@ -55,6 +43,25 @@ ([#3548](https://github.com/google/ExoPlayer/issues/3548)). * Support overriding the ad load timeout ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option + to use this with `FfmpegAudioRenderer`. + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by a + seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### From 106e69b3fca7a1e53ad367897d64e9d3b016c7c0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:09:21 -0800 Subject: [PATCH 192/417] Check if native libraries are available in tests. If the library is not available, no tracks can be selected and the tests silently run through by immediately switching to ended state without error. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178904347 --- .../android/exoplayer2/ext/flac/FlacExtractorTest.java | 8 ++++++++ .../android/exoplayer2/ext/flac/FlacPlaybackTest.java | 9 ++++++++- .../android/exoplayer2/ext/opus/OpusPlaybackTest.java | 8 ++++++++ .../android/exoplayer2/ext/vp9/VpxPlaybackTest.java | 9 ++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 7b193997c3..57ce487ac7 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; */ public class FlacExtractorTest extends InstrumentationTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testSample() throws Exception { ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index fd18a3b1ae..b236b706b8 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -37,6 +37,14 @@ public class FlacPlaybackTest extends InstrumentationTestCase { private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_FLAC_URI); } @@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index d3ab421655..c547cff434 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -37,6 +37,14 @@ public class OpusPlaybackTest extends InstrumentationTestCase { private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!OpusLibrary.isAvailable()) { + fail("Opus library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_OPUS_URI); } diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 3cc1a1d340..0a902e2efe 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -43,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase { private static final String TAG = "VpxPlaybackTest"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!VpxLibrary.isAvailable()) { + fail("Vpx library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_URI); } @@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } From a5cd0b87bc504593771b14926f02e84c71d9b490 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 08:32:28 -0800 Subject: [PATCH 193/417] Update SingleSampleMediaSource with factory/listener changes - Convert the Builder into a Factory - Have it use MediaSourceEventListener - Also made some misc related fixes to other sources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178906521 --- .../source/ExtractorMediaPeriod.java | 17 +- .../source/ExtractorMediaSource.java | 4 +- .../source/SingleSampleMediaPeriod.java | 99 +++--- .../source/SingleSampleMediaSource.java | 282 +++++++++++++----- .../exoplayer2/source/hls/HlsMediaSource.java | 4 +- 5 files changed, 280 insertions(+), 126 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f8021c24df..4773ac53a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -443,9 +443,6 @@ import java.util.Arrays; @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - if (released) { - return; - } eventDispatcher.loadCanceled( loadable.dataSpec, C.DATA_TYPE_MEDIA, @@ -458,12 +455,14 @@ import java.util.Arrays; elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded); - copyLengthFromLoader(loadable); - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(); - } - if (enabledTrackCount > 0) { - callback.onContinueLoadingRequested(this); + if (!released) { + copyLengthFromLoader(loadable); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + if (enabledTrackCount > 0) { + callback.onContinueLoadingRequested(this); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index a2d7941c3f..14453653af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -195,7 +195,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param uri The {@link Uri}. * @return The new {@link ExtractorMediaSource}. */ - public MediaSource createMediaSource(Uri uri) { + public ExtractorMediaSource createMediaSource(Uri uri) { return createMediaSource(uri, null, null); } @@ -208,7 +208,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @return The new {@link ExtractorMediaSource}. */ @Override - public MediaSource createMediaSource( + public ExtractorMediaSource createMediaSource( Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { isCreateCalled = true; if (extractorsFactory == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 9fff3b4d85..e76de60b86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -15,14 +15,12 @@ */ package com.google.android.exoplayer2.source; -import android.net.Uri; -import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -44,14 +42,14 @@ import java.util.Arrays; */ private static final int INITIAL_SAMPLE_SIZE = 1024; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; + private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + private final long durationUs; + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; @@ -63,16 +61,20 @@ import java.util.Arrays; /* package */ int sampleSize; private int errorCount; - public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, - int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaPeriod( + DataSpec dataSpec, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + boolean treatLoadErrorsAsEndOfStream) { + this.dataSpec = dataSpec; this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; + this.eventDispatcher = eventDispatcher; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); @@ -131,7 +133,9 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } - loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, + loader.startLoading( + new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), + this, minLoadableRetryCount); return true; } @@ -169,6 +173,18 @@ import java.util.Arrays; @Override public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); sampleSize = loadable.sampleSize; sampleData = loadable.sampleData; loadingFinished = true; @@ -178,34 +194,46 @@ import java.util.Arrays; @Override public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - // Do nothing. + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); } @Override public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - notifyLoadError(error); errorCount++; - if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) { + boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize, + error, + /* wasCanceled= */ cancel); + if (cancel) { loadingFinished = true; return Loader.DONT_RETRY; } return Loader.RETRY; } - // Internal methods. - - private void notifyLoadError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(eventSourceId, e); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private static final int STREAM_STATE_SEND_FORMAT = 0; @@ -270,14 +298,15 @@ import java.util.Arrays; /* package */ static final class SourceLoadable implements Loadable { - private final Uri uri; + public final DataSpec dataSpec; + private final DataSource dataSource; private int sampleSize; private byte[] sampleData; - public SourceLoadable(Uri uri, DataSource dataSource) { - this.uri = uri; + public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { + this.dataSpec = dataSpec; this.dataSource = dataSource; } @@ -297,7 +326,7 @@ import java.util.Arrays; sampleSize = 0; try { // Create and open the input. - dataSource.open(new DataSpec(uri)); + dataSource.open(dataSpec); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 51afb8eee9..b92085d15e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -17,11 +17,14 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource { /** * Listener of {@link SingleSampleMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -45,35 +51,23 @@ public final class SingleSampleMediaSource implements MediaSource { } - /** - * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SingleSampleMediaSource}. */ + public static final class Factory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private final Format format; - private final long durationUs; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private int eventSourceId; private boolean treatLoadErrorsAsEndOfStream; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a factory for {@link SingleSampleMediaSource}s. + * * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will * be obtained. - * @param format The {@link Format} associated with the output track. - * @param durationUs The duration of the media stream in microseconds. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - this.format = format; - this.durationUs = durationUs; + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } @@ -82,37 +76,15 @@ public final class SingleSampleMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } - /** - * Sets the listener to respond to events and the handler to deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. - * - * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. - * @return This builder. - */ - public Builder setEventSourceId(int eventSourceId) { - this.eventSourceId = eventSourceId; - return this; - } - /** * Sets whether load errors will be treated as end-of-stream signal (load errors will not be * propagated). The default value is false. @@ -120,27 +92,53 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated * normally by {@link SampleStream#maybeThrowError()}. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + Assertions.checkState(!isCreateCalled); this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; return this; } /** - * Builds a new {@link SingleSampleMediaSource} using the current parameters. - *

      - * After this call, the builder should not be re-used. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @return The new {@link ExtractorMediaSource}. + */ + public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { + return createMediaSource(uri, format, durationUs, null, null); + } + + /** + * Returns a new {@link SingleSampleMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @param eventHandler A handler for events. + * @param eventListener A listener of events., Format format, long durationUs * @return The newly built {@link SingleSampleMediaSource}. */ - public SingleSampleMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - - return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, - minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + public SingleSampleMediaSource createMediaSource( + Uri uri, + Format format, + long durationUs, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + return new SingleSampleMediaSource( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener, treatLoadErrorsAsEndOfStream); } @@ -151,13 +149,12 @@ public final class SingleSampleMediaSource implements MediaSource { */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final Format format; + private final long durationUs; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; @@ -167,11 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs) { + public SingleSampleMediaSource( + Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } @@ -182,12 +179,16 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount) { + this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false); } /** @@ -203,20 +204,46 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + int eventSourceId, + boolean treatLoadErrorsAsEndOfStream) { + this( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId), + treatLoadErrorsAsEndOfStream); + } + + private SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, + boolean treatLoadErrorsAsEndOfStream) { this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + dataSpec = new DataSpec(uri); timeline = new SinglePeriodTimeline(durationUs, true, false); } @@ -235,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); + return new SingleSampleMediaPeriod( + dataSpec, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventDispatcher, + treatLoadErrorsAsEndOfStream); } @Override @@ -249,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource { // Do nothing. } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + + private final EventListener eventListener; + private final int eventSourceId; + + public EventListenerWrapper(EventListener eventListener, int eventSourceId) { + this.eventListener = Assertions.checkNotNull(eventListener); + this.eventSourceId = eventSourceId; + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(eventSourceId, error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } 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 4c14d2029e..b628807109 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 @@ -170,7 +170,7 @@ public final class HlsMediaSource implements MediaSource, * * @return The new {@link HlsMediaSource}. */ - public MediaSource createMediaSource(Uri playlistUri) { + public HlsMediaSource createMediaSource(Uri playlistUri) { return createMediaSource(playlistUri, null, null); } @@ -183,7 +183,7 @@ public final class HlsMediaSource implements MediaSource, * @return The new {@link HlsMediaSource}. */ @Override - public MediaSource createMediaSource( + public HlsMediaSource createMediaSource( Uri playlistUri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { From 2cbf0ef0ab978979988fbe35251e644df2faf6a6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:37:27 -0800 Subject: [PATCH 194/417] Move playback state, isLoading, and track selector result to PlaybackInfo. This is a no-op change replacing the local variables in ExoPlayerImplInternal with the new ones in PlaybackInfo. *** Use playbackState, isLoading and trackSelectorResult from playbackInfo in ExoPlayerImpl. *** Move duplicated listener notification in ExoPlayerImpl to new method. Also split reset method in one parts which creates the new playback info and one part which notifies the listeners. The increment of the pending operation counter needs to happen in between. *** Use only one pending operation counter in ExoPlayerImpl. This also allows to move onSeekProcessed into the notification chain. *** Replace playback info changing messages to ExoPlayerImpl by single message type. As they are all handled in the same way, they can be summarized to one message. *** Only send playback info change notifications once per playback thread message. This ensures that all concurrent changes actually reach ExoPlayerImpl concurrently. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178907165 --- .../android/exoplayer2/ExoPlayerTest.java | 88 +++--- .../android/exoplayer2/ExoPlayerImpl.java | 290 ++++++++++-------- .../exoplayer2/ExoPlayerImplInternal.java | 247 ++++++++------- .../android/exoplayer2/PlaybackInfo.java | 119 ++++++- .../trackselection/TrackSelectorResult.java | 2 +- 5 files changed, 460 insertions(+), 286 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index dad891718e..a227aa3575 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -56,18 +56,15 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 0); + Timeline timeline = Timeline.EMPTY; FakeRenderer renderer = new FakeRenderer(); - // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline - // update happens after the transition to STATE_ENDED and the test runner may already have been - // stopped. Remove action schedule as soon as state changes are part of the masking and the - // correct order of events is restored. - ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlayEmptyTimeline") - .waitForTimelineChanged(timeline) - .build(); - ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) - .build().start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setRenderers(renderer) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); @@ -307,21 +304,28 @@ public final class ExoPlayerTest extends TestCase { public void testSeekProcessedCallback() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); - ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") - // Initial seek before timeline preparation started. Expect immediate seek processed while - // the player is still in STATE_IDLE. - .pause().seek(5) - // Wait until the media source starts preparing and issue more initial seeks. Expect only - // one seek processed after the source has been prepared. - .waitForPlaybackState(Player.STATE_BUFFERING).seek(2).seek(10) - // Wait until media source prepared and re-seek to same position. Expect a seek processed - // while still being in STATE_READY. - .waitForPlaybackState(Player.STATE_READY).seek(10) - // Start playback and wait until playback reaches second window. - .play().waitForPositionDiscontinuity() - // Seek twice in concession, expecting the first seek to be replaced (and thus except only - // on seek processed callback). - .seek(5).seek(60).build(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekProcessedCallback") + // Initial seek. Expect immediate seek processed. + .pause() + .seek(5) + .waitForSeekProcessed() + // Multiple overlapping seeks while the player is still preparing. Expect only one seek + // processed. + .seek(2) + .seek(10) + // Wait until media source prepared and re-seek to same position. Expect a seek + // processed while still being in STATE_READY. + .waitForPlaybackState(Player.STATE_READY) + .seek(10) + // Start playback and wait until playback reaches second window. + .play() + .waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced (and thus except + // only on seek processed callback). + .seek(5) + .seek(60) + .build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); Player.EventListener eventListener = new Player.DefaultEventListener() { private int currentPlaybackState = Player.STATE_IDLE; @@ -340,7 +344,7 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); assertEquals(4, playbackStatesWhenSeekProcessed.size()); - assertEquals(Player.STATE_IDLE, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(1)); assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(2)); assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); @@ -804,19 +808,24 @@ public final class ExoPlayerTest extends TestCase { public void testStopDuringPreparationOverwritesPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopOverwritesPrepare") - .waitForPlaybackState(Player.STATE_BUFFERING) - .stop(true) - .build(); - ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testStopOverwritesPrepare") + .waitForPlaybackState(Player.STATE_BUFFERING) + .seek(0) + .stop(true) + .waitForSeekProcessed() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); testRunner.assertTimelinesEqual(Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertNoPositionDiscontinuities(); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { @@ -855,8 +864,9 @@ public final class ExoPlayerTest extends TestCase { .waitForPlaybackState(Player.STATE_IDLE) .prepareSource( new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ false, + /* resetPosition= */ true, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 3fe6cc6eed..2869a7668e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -42,24 +42,19 @@ import java.util.concurrent.CopyOnWriteArraySet; private final Renderer[] renderers; private final TrackSelector trackSelector; - private final TrackSelectionArray emptyTrackSelections; + private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; - private boolean tracksSelected; private boolean playWhenReady; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int playbackState; - private int pendingSeekAcks; - private int pendingPrepareOrStopAcks; - private boolean waitingForInitialTimeline; - private boolean isLoading; - private TrackGroupArray trackGroups; - private TrackSelectionArray trackSelections; + private int pendingOperationAcks; + private boolean hasPendingPrepare; + private boolean hasPendingSeek; private PlaybackParameters playbackParameters; // Playback information when there is no pending seek/set source operation. @@ -87,13 +82,16 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playWhenReady = false; this.repeatMode = Player.REPEAT_MODE_OFF; this.shuffleModeEnabled = false; - this.playbackState = Player.STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); - emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); + emptyTrackSelectorResult = + new TrackSelectorResult( + TrackGroupArray.EMPTY, + new boolean[renderers.length], + new TrackSelectionArray(new TrackSelection[renderers.length]), + null, + new RendererConfiguration[renderers.length]); window = new Timeline.Window(); period = new Timeline.Period(); - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; playbackParameters = PlaybackParameters.DEFAULT; Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); eventHandler = new Handler(eventLooper) { @@ -102,9 +100,19 @@ import java.util.concurrent.CopyOnWriteArraySet; ExoPlayerImpl.this.handleEvent(msg); } }; - playbackInfo = new PlaybackInfo(Timeline.EMPTY, null, 0, 0); - internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - repeatMode, shuffleModeEnabled, eventHandler, this); + playbackInfo = + new PlaybackInfo(Timeline.EMPTY, /* startPositionUs= */ 0, emptyTrackSelectorResult); + internalPlayer = + new ExoPlayerImplInternal( + renderers, + trackSelector, + emptyTrackSelectorResult, + loadControl, + playWhenReady, + repeatMode, + shuffleModeEnabled, + eventHandler, + this); } @Override @@ -124,7 +132,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getPlaybackState() { - return playbackState; + return playbackInfo.playbackState; } @Override @@ -134,10 +142,22 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - waitingForInitialTimeline = true; - pendingPrepareOrStopAcks++; - reset(resetPosition, resetState); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; internalPlayer.prepare(mediaSource, resetPosition); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override @@ -146,7 +166,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); } } } @@ -190,7 +210,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isLoading() { - return isLoading; + return playbackInfo.isLoading; } @Override @@ -214,19 +234,22 @@ import java.util.concurrent.CopyOnWriteArraySet; if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } + hasPendingSeek = true; + pendingOperationAcks++; if (isPlayingAd()) { // TODO: Investigate adding support for seeking during ads. This is complicated to do in // general because the midroll ad preceding the seek destination must be played before the // content position can be played, if a different ad is playing at the moment. Log.w(TAG, "seekTo ignored because an ad is playing"); - if (pendingSeekAcks == 0) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); - } - } + eventHandler + .obtainMessage( + ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED, + /* operationAcks */ 1, + /* positionDiscontinuityReason */ C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); return; } - pendingSeekAcks++; maskingWindowIndex = windowIndex; if (timeline.isEmpty()) { maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; @@ -273,9 +296,23 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop(boolean reset) { - pendingPrepareOrStopAcks++; - reset(/* resetPosition= */ reset, /* resetState= */ reset); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + /* resetPosition= */ reset, + /* resetState= */ reset, + /* playbackState= */ Player.STATE_IDLE); + // Trigger internal stop first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this stop. The internal player can't change the playback info immediately + // because it uses a callback. + pendingOperationAcks++; internalPlayer.stop(reset); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override @@ -421,12 +458,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public TrackGroupArray getCurrentTrackGroups() { - return trackGroups; + return playbackInfo.trackSelectorResult.groups; } @Override public TrackSelectionArray getCurrentTrackSelections() { - return trackSelections; + return playbackInfo.trackSelectorResult.selections; } @Override @@ -442,51 +479,14 @@ import java.util.concurrent.CopyOnWriteArraySet; // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { switch (msg.what) { - case ExoPlayerImplInternal.MSG_STATE_CHANGED: { - playbackState = msg.arg1; - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } + case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: + handlePlaybackInfo( + (PlaybackInfo) msg.obj, + /* operationAcks= */ msg.arg1, + /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, + /* positionDiscontinuityReason= */ msg.arg2); break; - } - case ExoPlayerImplInternal.MSG_LOADING_CHANGED: { - isLoading = msg.arg1 != 0; - for (Player.EventListener listener : listeners) { - listener.onLoadingChanged(isLoading); - } - break; - } - case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - int prepareOrStopAcks = msg.arg1; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, 0, false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL); - break; - } - case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareOrStopAcks == 0) { - TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; - tracksSelected = true; - trackGroups = trackSelectorResult.groups; - trackSelections = trackSelectorResult.selections; - trackSelector.onSelectionActivated(trackSelectorResult.info); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - break; - } - case ExoPlayerImplInternal.MSG_SEEK_ACK: { - boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, - DISCONTINUITY_REASON_SEEK_ADJUSTMENT); - break; - } - case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - @DiscontinuityReason int discontinuityReason = msg.arg1; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); - break; - } - case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { + case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; if (!this.playbackParameters.equals(playbackParameters)) { this.playbackParameters = playbackParameters; @@ -495,24 +495,24 @@ import java.util.concurrent.CopyOnWriteArraySet; } } break; - } - case ExoPlayerImplInternal.MSG_ERROR: { + case ExoPlayerImplInternal.MSG_ERROR: ExoPlaybackException exception = (ExoPlaybackException) msg.obj; for (Player.EventListener listener : listeners) { listener.onPlayerError(exception); } break; - } default: throw new IllegalStateException(); } } - private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, - boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { - pendingPrepareOrStopAcks -= prepareOrStopAcks; - pendingSeekAcks -= seekAcks; - if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { + private void handlePlaybackInfo( + PlaybackInfo playbackInfo, + int operationAcks, + boolean positionDiscontinuity, + @DiscontinuityReason int positionDiscontinuityReason) { + pendingOperationAcks -= operationAcks; + if (pendingOperationAcks == 0) { if (playbackInfo.timeline == null) { // Replace internal null timeline with externally visible empty timeline. playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest); @@ -523,37 +523,32 @@ import java.util.concurrent.CopyOnWriteArraySet; playbackInfo.fromNewPosition( playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); } - boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline - || this.playbackInfo.manifest != playbackInfo.manifest; - this.playbackInfo = playbackInfo; - if (timelineOrManifestChanged || waitingForInitialTimeline) { - if (playbackInfo.timeline.isEmpty()) { - // Update the masking variables, which are used when the timeline becomes empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; - } - @Player.TimelineChangeReason int reason = waitingForInitialTimeline - ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; - waitingForInitialTimeline = false; - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, reason); - } - } - if (positionDiscontinuity) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(positionDiscontinuityReason); - } - } - } - if (pendingSeekAcks == 0 && seekAcks > 0) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); + if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare) + && playbackInfo.timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline becomes empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; } + @Player.TimelineChangeReason + int timelineChangeReason = + hasPendingPrepare + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + boolean seekProcessed = hasPendingSeek; + hasPendingPrepare = false; + hasPendingSeek = false; + updatePlaybackInfo( + playbackInfo, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed); } } - private void reset(boolean resetPosition, boolean resetState) { + private PlaybackInfo getResetPlaybackInfo( + boolean resetPosition, boolean resetState, int playbackState) { if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; @@ -563,22 +558,62 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingPeriodIndex = getCurrentPeriodIndex(); maskingWindowPositionMs = getCurrentPosition(); } - if (resetState) { - if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { - playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, - Player.TIMELINE_CHANGE_REASON_RESET); - } + return new PlaybackInfo( + resetState ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? null : playbackInfo.manifest, + playbackInfo.periodId, + playbackInfo.startPositionUs, + playbackInfo.contentPositionUs, + playbackState, + /* isLoading= */ false, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); + } + + private void updatePlaybackInfo( + PlaybackInfo newPlaybackInfo, + boolean positionDiscontinuity, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, + boolean seekProcessed) { + boolean timelineOrManifestChanged = + playbackInfo.timeline != newPlaybackInfo.timeline + || playbackInfo.manifest != newPlaybackInfo.manifest; + boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; + boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; + boolean trackSelectorResultChanged = + this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; + playbackInfo = newPlaybackInfo; + if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } + } + if (positionDiscontinuity) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(positionDiscontinuityReason); + } + } + if (trackSelectorResultChanged) { + trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged( + playbackInfo.trackSelectorResult.groups, playbackInfo.trackSelectorResult.selections); + } + } + if (isLoadingChanged) { + for (Player.EventListener listener : listeners) { + listener.onLoadingChanged(playbackInfo.isLoading); + } + } + if (playbackStateChanged) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); + } + } + if (seekProcessed) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); } } } @@ -593,7 +628,6 @@ import java.util.concurrent.CopyOnWriteArraySet; } private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0; + return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; } - } 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 a1fe8c09c5..b52696533d 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 @@ -27,6 +27,7 @@ import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -51,14 +52,9 @@ import java.io.IOException; private static final String TAG = "ExoPlayerImplInternal"; // External messages - public static final int MSG_STATE_CHANGED = 0; - public static final int MSG_LOADING_CHANGED = 1; - public static final int MSG_TRACKS_CHANGED = 2; - public static final int MSG_SEEK_ACK = 3; - public static final int MSG_POSITION_DISCONTINUITY = 4; - public static final int MSG_SOURCE_INFO_REFRESHED = 5; - public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 6; - public static final int MSG_ERROR = 7; + public static final int MSG_PLAYBACK_INFO_CHANGED = 0; + public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; + public static final int MSG_ERROR = 2; // Internal messages private static final int MSG_PREPARE = 0; @@ -99,6 +95,7 @@ import java.io.IOException; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; + private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; private final Handler handler; private final HandlerThread internalPlaybackThread; @@ -110,6 +107,7 @@ import java.io.IOException; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; + private final PlaybackInfoUpdate playbackInfoUpdate; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,8 +118,6 @@ import java.io.IOException; private boolean released; private boolean playWhenReady; private boolean rebuffering; - private boolean isLoading; - private int state; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; private int customMessagesSent; @@ -136,24 +132,34 @@ import java.io.IOException; private MediaPeriodHolder readingPeriodHolder; private MediaPeriodHolder playingPeriodHolder; - public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled, Handler eventHandler, ExoPlayer player) { + public ExoPlayerImplInternal( + Renderer[] renderers, + TrackSelector trackSelector, + TrackSelectorResult emptyTrackSelectorResult, + LoadControl loadControl, + boolean playWhenReady, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Handler eventHandler, + ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; + this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; - this.state = Player.STATE_IDLE; this.player = player; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); seekParameters = SeekParameters.DEFAULT; - playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); + playbackInfo = + new PlaybackInfo( + /* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); + playbackInfoUpdate = new PlaybackInfoUpdate(); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); @@ -305,84 +311,99 @@ import java.io.IOException; switch (msg.what) { case MSG_PREPARE: prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); - return true; + break; case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); - return true; + break; case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); - return true; + break; case MSG_SET_SHUFFLE_ENABLED: setShuffleModeEnabledInternal(msg.arg1 != 0); - return true; + break; case MSG_DO_SOME_WORK: doSomeWork(); - return true; + break; case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); - return true; + break; case MSG_SET_PLAYBACK_PARAMETERS: setPlaybackParametersInternal((PlaybackParameters) msg.obj); - return true; + break; case MSG_SET_SEEK_PARAMETERS: setSeekParametersInternal((SeekParameters) msg.obj); - return true; + break; case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); - return true; + break; case MSG_RELEASE: releaseInternal(); - return true; + break; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); - return true; + break; case MSG_REFRESH_SOURCE_INFO: handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); - return true; + break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); - return true; + break; case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); - return true; + break; case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); - return true; + break; default: return false; } + maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { Log.e(TAG, "Source error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } + return true; } // Private methods. private void setState(int state) { - if (this.state != state) { - this.state = state; - eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget(); + if (playbackInfo.playbackState != state) { + playbackInfo = playbackInfo.copyWithPlaybackState(state); } } private void setIsLoading(boolean isLoading) { - if (this.isLoading != isLoading) { - this.isLoading = isLoading; - eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget(); + if (playbackInfo.isLoading != isLoading) { + playbackInfo = playbackInfo.copyWithIsLoading(isLoading); + } + } + + private void maybeNotifyPlaybackInfoChanged() { + if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { + eventHandler + .obtainMessage( + MSG_PLAYBACK_INFO_CHANGED, + playbackInfoUpdate.operationAcks, + playbackInfoUpdate.positionDiscontinuity + ? playbackInfoUpdate.discontinuityReason + : C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); + playbackInfoUpdate.reset(playbackInfo); } } @@ -403,10 +424,10 @@ import java.io.IOException; stopRenderers(); updatePlaybackPositions(); } else { - if (state == Player.STATE_READY) { + if (playbackInfo.playbackState == Player.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else if (state == Player.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } @@ -474,10 +495,9 @@ import java.io.IOException; MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, - playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfo = + playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } } @@ -511,8 +531,7 @@ import java.io.IOException; if (periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); @@ -575,7 +594,7 @@ import java.io.IOException; && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); - } else if (state == Player.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { float playbackSpeed = mediaClock.getPlaybackParameters().speed; boolean isNewlyReady = enabledRenderers.length > 0 ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( @@ -587,7 +606,7 @@ import java.io.IOException; startRenderers(); } } - } else if (state == Player.STATE_READY) { + } else if (playbackInfo.playbackState == Player.STATE_READY) { boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded : isTimelineReady(playingPeriodDurationUs); if (!isStillReady) { @@ -597,15 +616,16 @@ import java.io.IOException; } } - if (state == Player.STATE_BUFFERING) { + if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } - if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) { + if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + || playbackInfo.playbackState == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) { + } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -626,12 +646,10 @@ import java.io.IOException; } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); Timeline timeline = playbackInfo.timeline; if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; - eventHandler - .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 0, 0, playbackInfo) - .sendToTarget(); return; } @@ -679,8 +697,9 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); } } finally { - eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) - .sendToTarget(); + if (seekPositionAdjusted) { + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } } } @@ -779,7 +798,9 @@ import java.io.IOException; private void stopInternal(boolean reset, boolean acknowledgeStop) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - notifySourceInfoRefresh(acknowledgeStop); + playbackInfoUpdate.incrementPendingOperationAcks( + pendingPrepareCount + (acknowledgeStop ? 1 : 0)); + pendingPrepareCount = 0; loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -817,25 +838,29 @@ import java.io.IOException; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); + Timeline timeline = playbackInfo.timeline; + int firstPeriodIndex = + timeline == null || timeline.isEmpty() + ? 0 + : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + .firstPeriodIndex; if (resetPosition) { - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - Timeline timeline = playbackInfo.timeline; - int firstPeriodIndex = timeline == null || timeline.isEmpty() - ? 0 - : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) - .firstPeriodIndex; pendingInitialSeekPosition = null; - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); - } else { - // The new start position is the current playback position. - playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, - playbackInfo.contentPositionUs); } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); - playbackInfo = playbackInfo.copyWithTimeline(null, null); } + playbackInfo = + new PlaybackInfo( + resetState ? null : playbackInfo.timeline, + resetState ? null : playbackInfo.manifest, + resetPosition ? new MediaPeriodId(firstPeriodIndex) : playbackInfo.periodId, + // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. + resetPosition ? C.TIME_UNSET : playbackInfo.startPositionUs, + resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs, + playbackInfo.playbackState, + /* isLoading= */ false, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); @@ -849,7 +874,8 @@ import java.io.IOException; for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } - if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { + if (playbackInfo.playbackState == Player.STATE_READY + || playbackInfo.playbackState == Player.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -909,11 +935,11 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { + if (playbackInfo.playbackState != Player.STATE_ENDED + && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); resetRendererPosition(periodPositionUs); } @@ -936,8 +962,7 @@ import java.io.IOException; } } } - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult) - .sendToTarget(); + playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. @@ -954,7 +979,7 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - if (state != Player.STATE_ENDED) { + if (playbackInfo.playbackState != Player.STATE_ENDED) { maybeContinueLoading(); updatePlaybackPositions(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); @@ -1010,6 +1035,8 @@ import java.io.IOException; playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); if (oldTimeline == null) { + playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); + pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; @@ -1024,7 +1051,6 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { @@ -1038,10 +1064,7 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(); } - } else { - notifySourceInfoRefresh(); } return; } @@ -1050,7 +1073,6 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { - notifySourceInfoRefresh(); return; } Object playingPeriodUid = periodHolder == null @@ -1090,7 +1112,6 @@ import java.io.IOException; MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); newPositionUs = seekToPeriodPosition(periodId, newPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, C.TIME_UNSET); - notifySourceInfoRefresh(); return; } @@ -1107,14 +1128,12 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, contentPositionUs); - notifySourceInfoRefresh(); return; } } if (periodHolder == null) { // We don't have any period holders, so we're done. - notifySourceInfoRefresh(); return; } @@ -1152,8 +1171,6 @@ import java.io.IOException; break; } } - - notifySourceInfoRefresh(); } private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { @@ -1172,18 +1189,6 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - notifySourceInfoRefresh(); - } - - private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(/* acknowledgeStop= */ false); - } - - private void notifySourceInfoRefresh(boolean acknowledgeStop) { - int prepareOrStopAcks = pendingPrepareCount + (acknowledgeStop ? 1 : 0); - pendingPrepareCount = 0; - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) - .sendToTarget(); } /** @@ -1287,7 +1292,7 @@ import java.io.IOException; if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); - } else if (loadingPeriodHolder != null && !isLoading) { + } else if (loadingPeriodHolder != null && !playbackInfo.isLoading) { maybeContinueLoading(); } @@ -1305,9 +1310,8 @@ import java.io.IOException; setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { @@ -1488,7 +1492,7 @@ import java.io.IOException; } playingPeriodHolder = periodHolder; - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget(); + playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1514,7 +1518,7 @@ import java.io.IOException; rendererIndex); Format[] formats = getFormats(newSelection); // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == Player.STATE_READY; + boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !wasRendererEnabled && playing; // Enable the renderer. @@ -1805,7 +1809,40 @@ import java.io.IOException; this.timeline = timeline; this.manifest = manifest; } + } + private static final class PlaybackInfoUpdate { + + private PlaybackInfo lastPlaybackInfo; + private int operationAcks; + private boolean positionDiscontinuity; + private @DiscontinuityReason int discontinuityReason; + + public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { + return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; + } + + public void reset(PlaybackInfo playbackInfo) { + lastPlaybackInfo = playbackInfo; + operationAcks = 0; + positionDiscontinuity = false; + } + + public void incrementPendingOperationAcks(int operationAcks) { + this.operationAcks += operationAcks; + } + + public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { + if (positionDiscontinuity + && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { + // We always prefer non-internal discontinuity reasons. We also assume that we won't report + // more than one non-internal discontinuity per message iteration. + Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); + return; + } + positionDiscontinuity = true; + this.discontinuityReason = discontinuityReason; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index a2ffa43c4b..65392ba269 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -15,35 +15,59 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * Information about an ongoing playback. */ /* package */ final class PlaybackInfo { - public final Timeline timeline; - public final Object manifest; + public final @Nullable Timeline timeline; + public final @Nullable Object manifest; public final MediaPeriodId periodId; public final long startPositionUs; public final long contentPositionUs; + public final int playbackState; + public final boolean isLoading; + public final TrackSelectorResult trackSelectorResult; public volatile long positionUs; public volatile long bufferedPositionUs; - public PlaybackInfo(Timeline timeline, Object manifest, int periodIndex, long startPositionUs) { - this(timeline, manifest, new MediaPeriodId(periodIndex), startPositionUs, C.TIME_UNSET); + public PlaybackInfo( + @Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) { + this( + timeline, + /* manifest= */ null, + new MediaPeriodId(0), + startPositionUs, + /* contentPositionUs =*/ C.TIME_UNSET, + Player.STATE_IDLE, + /* isLoading= */ false, + trackSelectorResult); } - public PlaybackInfo(Timeline timeline, Object manifest, MediaPeriodId periodId, - long startPositionUs, long contentPositionUs) { + public PlaybackInfo( + @Nullable Timeline timeline, + @Nullable Object manifest, + MediaPeriodId periodId, + long startPositionUs, + long contentPositionUs, + int playbackState, + boolean isLoading, + TrackSelectorResult trackSelectorResult) { this.timeline = timeline; this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; - positionUs = startPositionUs; - bufferedPositionUs = startPositionUs; + this.positionUs = startPositionUs; + this.bufferedPositionUs = startPositionUs; + this.playbackState = playbackState; + this.isLoading = isLoading; + this.trackSelectorResult = trackSelectorResult; } public PlaybackInfo fromNewPosition(int periodIndex, long startPositionUs, @@ -53,19 +77,88 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { - return new PlaybackInfo(timeline, manifest, periodId, startPositionUs, contentPositionUs); + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); } public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, - periodId.copyWithPeriodIndex(periodIndex), startPositionUs, contentPositionUs); + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId.copyWithPeriodIndex(periodIndex), + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { - PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, periodId, startPositionUs, - contentPositionUs); + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithPlaybackState(int playbackState) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithIsLoading(boolean isLoading) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 801f5b9584..68adc32395 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -74,7 +74,7 @@ public final class TrackSelectorResult { * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other) { - if (other == null) { + if (other == null || other.selections.length != selections.length) { return false; } for (int i = 0; i < selections.length; i++) { From 073c92ed3618030cb7a5cb3901b6026b6f1a1e0c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 09:54:25 -0800 Subject: [PATCH 195/417] Don't send playback info updates when released. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178916145 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 b52696533d..c34e947046 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 @@ -336,9 +336,6 @@ import java.io.IOException; case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); break; - case MSG_RELEASE: - releaseInternal(); - break; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); break; @@ -354,6 +351,10 @@ import java.io.IOException; case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); break; + case MSG_RELEASE: + releaseInternal(); + // Return immediately to not send playback info updates after release. + return true; default: return false; } From f5d7b67eeab2a5fe3d5edcbebcaa6ba9fdd9b9bc Mon Sep 17 00:00:00 2001 From: jschlag Date: Wed, 13 Dec 2017 15:46:30 -0800 Subject: [PATCH 196/417] try turning off vp9 loop filter on android ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178970007 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 9 ++++++--- .../google/android/exoplayer2/ext/vp9/VpxDecoder.java | 7 ++++--- extensions/vp9/src/main/jni/vpx_jni.cc | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index dd303af0d8..ac944a7b01 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -92,6 +92,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private final boolean scaleToFit; + private final boolean disableLoopFilter; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; private final boolean playClearSamplesWithoutKeys; @@ -154,7 +155,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify, - null, false); + null, false, false); } /** @@ -173,13 +174,15 @@ public final class LibvpxVideoRenderer extends BaseRenderer { * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. */ public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { + boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) { super(C.TRACK_TYPE_VIDEO); this.scaleToFit = scaleToFit; + this.disableLoopFilter = disableLoopFilter; this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.drmSessionManager = drmSessionManager; @@ -625,7 +628,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createVpxDecoder"); decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - mediaCrypto); + mediaCrypto, disableLoopFilter); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long codecInitializedTimestamp = SystemClock.elapsedRealtime(); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index ef999d5d2b..6a15023c0b 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -49,10 +49,11 @@ import java.nio.ByteBuffer; * @param initialInputBufferSize The initial size of each input buffer. * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. + * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException { + ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException { super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); @@ -61,7 +62,7 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(); + vpxDecContext = vpxInit(disableLoopFilter); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } @@ -139,7 +140,7 @@ import java.nio.ByteBuffer; vpxClose(vpxDecContext); } - private native long vpxInit(); + private native long vpxInit(boolean disableLoopFilter); private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 5c480d1525..9666875b04 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -283,7 +283,7 @@ static void convert_16_to_8_standard(const vpx_image_t* const img, } } -DECODER_FUNC(jlong, vpxInit) { +DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); @@ -295,6 +295,9 @@ DECODER_FUNC(jlong, vpxInit) { errorCode = err; return 0; } + if (disableLoopFilter) { + vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true); + } // Populate JNI References. const jclass outputBufferClass = env->FindClass( From a17375b7d3a5b95751ad15d4cae4a80081149c8c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 04:25:51 -0800 Subject: [PATCH 197/417] Resend playback info update when skipping very short periods. Skipping short periods in a while loop is conceptually a new operation and thus we need to send out the updated playback info in between for the listeners to receive multiple period transition discontinuities. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179027334 --- .../android/exoplayer2/ExoPlayerTest.java | 24 +++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 6 +++++ 2 files changed, 30 insertions(+) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index a227aa3575..40b4b2d383 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -111,6 +112,29 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + /** Tests playback of periods with very short duration. */ + public void testPlayShortDurationPeriods() throws Exception { + // TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US / 100 = 1000 us per period. + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 100, /* id= */ 0)); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setRenderers(renderer) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + int[] expectedReasons = new int[99]; + Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + assertEquals(100, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); + } + /** * Tests that the player does not unnecessarily reset renderers when playing a multi-period * source. 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 c34e947046..7b52f79be5 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 @@ -1303,16 +1303,22 @@ import java.io.IOException; } // Advance the playing period if necessary. + boolean advancedPlayingPeriod = false; while (playWhenReady && playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { // All enabled renderers' streams have been read to the end, and the playback position reached // the end of the playing period, so advance playback to the next period. + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); updatePlaybackPositions(); + advancedPlayingPeriod = true; } if (readingPeriodHolder.info.isFinal) { From 37a275f67e85f6077548bb5d8894fb958e81923c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 05:18:07 -0800 Subject: [PATCH 198/417] Enable SeekParameters functionality for ExtractorMediaSource Also fix ClippingMediaSource to consider the start position an artificial key-frame, and to properly offset the value returned by getAdjustedSeekPositionUs. Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179032243 --- RELEASENOTES.md | 6 ++++ .../exoplayer2/ExoPlayerImplInternal.java | 12 +++---- .../source/ClippingMediaPeriod.java | 23 +++++++----- .../source/ExtractorMediaPeriod.java | 32 +++++++++++++++-- .../google/android/exoplayer2/util/Util.java | 34 ++++++++++++++++++ .../android/exoplayer2/util/UtilTest.java | 36 +++++++++++++++++++ 6 files changed, 126 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 95eda228ea..9875333dad 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,12 @@ sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, `SsMediaSource.Factory`, and `MergingMediaSource`. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. + * Note: `SeekParameters` are only currently effective when playing + `ExtractorMediaSource`s (i.e. progressive streams). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can 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 7b52f79be5..09b3231467 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 @@ -678,20 +678,20 @@ import java.io.IOException; periodPositionUs = 0; } try { + long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { - long adjustedPeriodPositionUs = periodPositionUs; - if (playingPeriodHolder != null) { - adjustedPeriodPositionUs = + if (playingPeriodHolder != null && newPeriodPositionUs != 0) { + newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( - adjustedPeriodPositionUs, SeekParameters.DEFAULT); + newPeriodPositionUs, seekParameters); } - if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + if ((newPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { // Seek will be performed to the current position. Do nothing. periodPositionUs = playbackInfo.positionUs; return; } } - long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); + newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } finally { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index b1c12d6192..5685b8b70b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -165,16 +165,23 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb sampleStream.clearSentEos(); } } - long seekUs = mediaPeriod.seekToUs(positionUs + startUs); - Assertions.checkState(seekUs == positionUs + startUs - || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); + long offsetPositionUs = positionUs + startUs; + long seekUs = mediaPeriod.seekToUs(offsetPositionUs); + Assertions.checkState( + seekUs == offsetPositionUs + || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); return seekUs - startUs; } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs( - positionUs + startUs, adjustSeekParameters(positionUs + startUs, seekParameters)); + if (positionUs == startUs) { + // Never adjust seeks to the start of the clipped view. + return 0; + } + long offsetPositionUs = positionUs + startUs; + SeekParameters clippedSeekParameters = clipSeekParameters(offsetPositionUs, seekParameters); + return mediaPeriod.getAdjustedSeekPositionUs(offsetPositionUs, clippedSeekParameters) - startUs; } @Override @@ -209,12 +216,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; } - private SeekParameters adjustSeekParameters(long positionUs, SeekParameters seekParameters) { - long toleranceBeforeMs = Math.min(positionUs - startUs, seekParameters.toleranceBeforeUs); + private SeekParameters clipSeekParameters(long offsetPositionUs, SeekParameters seekParameters) { + long toleranceBeforeMs = Math.min(offsetPositionUs - startUs, seekParameters.toleranceBeforeUs); long toleranceAfterMs = endUs == C.TIME_END_OF_SOURCE ? seekParameters.toleranceAfterUs - : Math.min(endUs - positionUs, seekParameters.toleranceAfterUs); + : Math.min(endUs - offsetPositionUs, seekParameters.toleranceAfterUs); if (toleranceBeforeMs == seekParameters.toleranceBeforeUs && toleranceAfterMs == seekParameters.toleranceAfterUs) { return seekParameters; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 4773ac53a1..e5d1fae7bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -372,8 +373,33 @@ import java.util.Arrays; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - // Treat all seeks into non-seekable media as being to t=0. - return seekMap.isSeekable() ? positionUs : 0; + if (!seekMap.isSeekable()) { + // Treat all seeks into non-seekable media as being to t=0. + return 0; + } + SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); + long minPositionUs = + Util.subtractWithOverflowDefault( + positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = + Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); + long firstPointUs = seekPoints.first.timeUs; + boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs; + long secondPointUs = seekPoints.second.timeUs; + boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs; + if (firstPointValid && secondPointValid) { + if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) { + return firstPointUs; + } else { + return secondPointUs; + } + } else if (firstPointValid) { + return firstPointUs; + } else if (secondPointValid) { + return secondPointUs; + } else { + return minPositionUs; + } } // SampleStream methods. @@ -657,7 +683,7 @@ import java.util.Arrays; return pendingResetPositionUs != C.TIME_UNSET; } - private boolean isLoadableExceptionFatal(IOException e) { + private static boolean isLoadableExceptionFatal(IOException e) { return e instanceof UnrecognizedInputFormatException; } 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 0594f52288..d796e6936f 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 @@ -362,6 +362,40 @@ public final class Util { return Math.max(min, Math.min(value, max)); } + /** + * Returns the sum of two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x + y} overflows. + * @return {@code x + y}, or {@code overflowResult} if the result overflows. + */ + public static long addWithOverflowDefault(long x, long y, long overflowResult) { + long result = x + y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ result) & (y ^ result)) < 0) { + return overflowResult; + } + return result; + } + + /** + * Returns the difference between two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x - y} overflows. + * @return {@code x - y}, or {@code overflowResult} if the result overflows. + */ + public static long subtractWithOverflowDefault(long x, long y, long overflowResult) { + long result = x - y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ y) & (x ^ result)) < 0) { + return overflowResult; + } + return result; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 68ed686c62..ca7a3b199d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -41,6 +41,42 @@ import org.robolectric.annotation.Config; @Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class UtilTest { + @Test + public void testAddWithOverflowDefault() { + long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(15); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + public void testSubtrackWithOverflowDefault() { + long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(-5); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + @Test public void testInferContentType() { assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); From 0ccf816a5caabb99484cfedc23df9a703edfc68e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 14 Dec 2017 06:54:12 -0800 Subject: [PATCH 199/417] Add a bitmask for text tracks' selection flags in DefaultTrackSelector ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179039563 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 128 +++++++++++++----- .../DefaultTrackSelectorTest.java | 128 ++++++++++++++++++ 3 files changed, 226 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9875333dad..686a6d10ba 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ ([#3149](https://github.com/google/ExoPlayer/issues/3149)). * DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods with a builder. +* DefaultTrackSelector: Support disabling of individual text track selection + flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. ### 2.6.1 ### 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 2f0dc8f04e..09bd81416c 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 @@ -82,6 +82,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private String preferredAudioLanguage; private String preferredTextLanguage; private boolean selectUndeterminedTextLanguage; + private int disabledTextTrackSelectionFlags; private boolean forceLowestBitrate; private boolean allowMixedMimeAdaptiveness; private boolean allowNonSeamlessAdaptiveness; @@ -109,6 +110,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { preferredAudioLanguage = initialValues.preferredAudioLanguage; preferredTextLanguage = initialValues.preferredTextLanguage; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; + disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; forceLowestBitrate = initialValues.forceLowestBitrate; allowMixedMimeAdaptiveness = initialValues.allowMixedMimeAdaptiveness; allowNonSeamlessAdaptiveness = initialValues.allowNonSeamlessAdaptiveness; @@ -153,6 +155,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * See {@link Parameters#disabledTextTrackSelectionFlags}. + * + * @return This builder. + */ + public ParametersBuilder setDisabledTextTrackSelectionFlags( + int disabledTextTrackSelectionFlags) { + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + return this; + } + /** * See {@link Parameters#forceLowestBitrate}. * @@ -287,11 +300,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Builds a {@link Parameters} instance with the selected values. */ public Parameters build() { - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); + return new Parameters( + preferredAudioLanguage, + preferredTextLanguage, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags, + forceLowestBitrate, + allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, + maxVideoWidth, + maxVideoHeight, + maxVideoBitrate, + exceedVideoConstraintsIfNecessary, + exceedRendererCapabilitiesIfNecessary, + viewportWidth, + viewportHeight, + viewportOrientationMayChange); } } @@ -303,19 +327,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * An instance with default values: + * *

        - *
      • No preferred audio language.
      • - *
      • No preferred text language.
      • - *
      • Text tracks with undetermined language are not selected if no track with - * {@link #preferredTextLanguage} is available.
      • - *
      • Lowest bitrate track selections are not forced.
      • - *
      • Adaptation between different mime types is not allowed.
      • - *
      • Non seamless adaptation is allowed.
      • - *
      • No max limit for video width/height.
      • - *
      • No max video bitrate.
      • - *
      • Video constraints are exceeded if no supported selection can be made otherwise.
      • - *
      • Renderer capabilities are exceeded if no supported selection can be made.
      • - *
      • No viewport constraints.
      • + *
      • No preferred audio language. + *
      • No preferred text language. + *
      • Text tracks with undetermined language are not selected if no track with {@link + * #preferredTextLanguage} is available. + *
      • All selection flags are considered for text track selections. + *
      • Lowest bitrate track selections are not forced. + *
      • Adaptation between different mime types is not allowed. + *
      • Non seamless adaptation is allowed. + *
      • No max limit for video width/height. + *
      • No max video bitrate. + *
      • Video constraints are exceeded if no supported selection can be made otherwise. + *
      • Renderer capabilities are exceeded if no supported selection can be made. + *
      • No viewport constraints. *
      */ public static final Parameters DEFAULT = new Parameters(); @@ -338,6 +364,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. */ public final boolean selectUndeterminedTextLanguage; + /** + * Bitmask of selection flags that are disabled for text track selections. See {@link + * C.SelectionFlags}. + */ + public final int disabledTextTrackSelectionFlags; // Video /** @@ -392,19 +423,44 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean exceedRendererCapabilitiesIfNecessary; private Parameters() { - this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, - Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this( + null, + null, + false, + 0, + false, + false, + true, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + true, + true, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + true); } - private Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, - boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, - int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, + private Parameters( + String preferredAudioLanguage, + String preferredTextLanguage, + boolean selectUndeterminedTextLanguage, + int disabledTextTrackSelectionFlags, + boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, + boolean allowNonSeamlessAdaptiveness, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoBitrate, + boolean exceedVideoConstraintsIfNecessary, + boolean exceedRendererCapabilitiesIfNecessary, + int viewportWidth, + int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -434,14 +490,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { return false; } Parameters other = (Parameters) obj; - return forceLowestBitrate == other.forceLowestBitrate + return selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage + && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags + && forceLowestBitrate == other.forceLowestBitrate && allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness - && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight + && maxVideoWidth == other.maxVideoWidth + && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && viewportOrientationMayChange == other.viewportOrientationMayChange - && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight + && viewportWidth == other.viewportWidth + && viewportHeight == other.viewportHeight && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); @@ -449,19 +509,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public int hashCode() { - int result = preferredAudioLanguage.hashCode(); - result = 31 * result + preferredTextLanguage.hashCode(); + int result = selectUndeterminedTextLanguage ? 1 : 0; + result = 31 * result + disabledTextTrackSelectionFlags; result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0); result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; - result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (viewportOrientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; + result = 31 * result + maxVideoBitrate; + result = 31 * result + preferredAudioLanguage.hashCode(); + result = 31 * result + preferredTextLanguage.hashCode(); return result; } @@ -923,8 +985,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; + int maskedSelectionFlags = + format.selectionFlags & ~params.disabledTextTrackSelectionFlags; + boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); if (preferredLanguageFound diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 1eff48b730..24362d1570 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -529,6 +529,134 @@ public final class DefaultTrackSelectorTest { .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** Tests text track selection flags. */ + @Test + public void testsTextTrackSelectionFlags() throws ExoPlaybackException { + Format forcedOnly = + Format.createTextContainerFormat( + "forcedOnly", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_FORCED, + "eng"); + Format forcedDefault = + Format.createTextContainerFormat( + "forcedDefault", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT, + "eng"); + Format defaultOnly = + Format.createTextContainerFormat( + "defaultOnly", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_DEFAULT, + "eng"); + Format forcedOnlySpanish = + Format.createTextContainerFormat( + "forcedOnlySpanish", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_FORCED, + "spa"); + Format noFlag = + Format.createTextContainerFormat( + "noFlag", null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "eng"); + + RendererCapabilities[] textRendererCapabilities = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + // There is no text language preference, the first track flagged as default should be selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedDefault); + + // Ditto. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, noFlag, defaultOnly)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(defaultOnly); + + // With no language preference and no text track flagged as default, the first forced should be + // selected. + result = trackSelector.selectTracks(textRendererCapabilities, wrapFormats(forcedOnly, noFlag)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedOnly); + + trackSelector.setParameters( + Parameters.DEFAULT + .buildUpon() + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build()); + + // Default flags are disabled, so the first track flagged as forced should be selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedOnly); + + trackSelector.setParameters( + trackSelector.getParameters().buildUpon().setPreferredAudioLanguage("spa").build()); + + // Default flags are disabled, but there is a text track flagged as forced whose language + // matches the preferred audio language. + result = + trackSelector.selectTracks( + textRendererCapabilities, + wrapFormats(forcedDefault, forcedOnly, defaultOnly, noFlag, forcedOnlySpanish)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedOnlySpanish); + + trackSelector.setParameters( + trackSelector + .getParameters() + .buildUpon() + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED) + .build()); + + // All selection flags are disabled and there is no language preference, so nothing should be + // selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); + + // There is a preferred language, so the first language-matching track flagged as default should + // be selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedDefault); + + trackSelector.setParameters( + trackSelector + .getParameters() + .buildUpon() + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build()); + + // Same as above, but the default flag is disabled. If multiple tracks match the preferred + // language, those not flagged as forced are preferred, as they likely include the contents of + // forced subtitles. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(noFlag, forcedOnly, forcedDefault, defaultOnly)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(noFlag); + } + /** * Tests that the default track selector will select a text track with undetermined language if no * text track with the preferred language is available but From 435686f96923c56212cab540c0940f39ce180a18 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 08:27:32 -0800 Subject: [PATCH 200/417] Add missing attrs to SimpleExoplayerView They worked without being present in the declare-styleable, but they need to be present for Android Studio auto-complete to suggest them. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179047478 --- library/ui/src/main/res/values/attrs.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 1ab3854d21..b6ed4b17af 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,10 +51,13 @@ + + - + + From edbb9795517363e486b203e0dd5e0dd3a5e41e53 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 08:38:02 -0800 Subject: [PATCH 201/417] Allow to configure maximum buffer size in DefaultLoadControl. This adds a parameter to configure a maximum buffer size in bytes. If left at its default of C.LENGTH_UNSET, the target buffer is determined using a overridable method based on the track selection. Also adding a parameter to decide whether to prioritize time or size constraints. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179048554 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultLoadControl.java | 115 +++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 686a6d10ba..c7f7ed7bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -64,6 +64,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. * Use surfaceless context for secure `DummySurface`, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * FLV: Fix playback of live streams that do not contain an audio track diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index d329f6584b..3708500d9f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,12 +51,23 @@ public class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + /** + * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control + * automatically determines its target buffer size. + */ + public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET; + + /** The default prioritization of buffer time constraints over size constraints. */ + public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + private final DefaultAllocator allocator; private final long minBufferUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; + private final int targetBufferBytesOverwrite; + private final boolean prioritizeTimeOverSizeThresholds; private final PriorityTaskManager priorityTaskManager; private int targetBufferSize; @@ -75,8 +86,14 @@ public class DefaultLoadControl implements LoadControl { * @param allocator The {@link DefaultAllocator} used by the loader. */ public DefaultLoadControl(DefaultAllocator allocator) { - this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + this( + allocator, + DEFAULT_MIN_BUFFER_MS, + DEFAULT_MAX_BUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_TARGET_BUFFER_BYTES, + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } /** @@ -92,10 +109,27 @@ public class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { - this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds) { + this( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, null); } @@ -112,18 +146,30 @@ public class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. - * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - * periods. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @param priorityTaskManager If not null, registers itself as a task with priority {@link + * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds, PriorityTaskManager priorityTaskManager) { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; + targetBufferBytesOverwrite = targetBufferBytes; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.priorityTaskManager = priorityTaskManager; } @@ -135,12 +181,10 @@ public class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - targetBufferSize = 0; - for (int i = 0; i < renderers.length; i++) { - if (trackSelections.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); - } - } + targetBufferSize = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? calculateTargetBufferSize(renderers, trackSelections) + : targetBufferBytesOverwrite; allocator.setTargetBufferSize(targetBufferSize); } @@ -178,16 +222,28 @@ public class DefaultLoadControl implements LoadControl { } bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; + return minBufferDurationUs <= 0 + || bufferedDurationUs >= minBufferDurationUs + || (!prioritizeTimeOverSizeThresholds + && allocator.getTotalBytesAllocated() >= targetBufferSize); } @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs // between watermarks - && isBuffering && !targetBufferSizeReached); + if (prioritizeTimeOverSizeThresholds) { + isBuffering = + bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering + && !targetBufferSizeReached); + } else { + isBuffering = + !targetBufferSizeReached + && (bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks + } if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -198,6 +254,25 @@ public class DefaultLoadControl implements LoadControl { return isBuffering; } + /** + * Calculate target buffer size in bytes based on the selected tracks. The player will try not to + * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}. + * + * @param renderers The renderers for which the track were selected. + * @param trackSelectionArray The selected tracks. + * @return The target buffer size in bytes. + */ + protected int calculateTargetBufferSize( + Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + int targetBufferSize = 0; + for (int i = 0; i < renderers.length; i++) { + if (trackSelectionArray.get(i) != null) { + targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + } + } + return targetBufferSize; + } + private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 65360760c2fba971de5e8433f94b0448eb22f41f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:17:30 -0800 Subject: [PATCH 202/417] Pass -1 not C.TIME_UNSET when duration is unknown ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179165479 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 7 ++++++- 1 file changed, 6 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 acfe143952..79074d3956 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 @@ -151,6 +151,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ + private static final long IMA_DURATION_UNSET = -1L; + /** * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. @@ -533,6 +536,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public VideoProgressUpdate getContentProgress() { + boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; + long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; if (player == null) { return lastContentProgress; } else if (pendingContentPositionMs != C.TIME_UNSET) { @@ -542,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || contentDurationMs == C.TIME_UNSET) { + } else if (playingAd || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); From e913ede77e65cfbea63426ef35005c740048e783 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 203/417] Fix condition for detecting that an ad has ended onEnded was being called also for content finishing, as in this case the playing ad index changed (from INDEX_UNSET to 0). Fix this test so we only detect ads finishing. Also add logging for onEnded callbacks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167737 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 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 79074d3956..0f0f64c068 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 @@ -703,6 +703,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); + } } } @@ -798,16 +801,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; + int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); + playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; if (!sentContentComplete) { - boolean adFinished = (wasPlayingAd && !playingAd) - || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); + boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; 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(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); + } } if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); @@ -819,7 +826,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } } - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } private void resumeContentInternal() { From 3cc08d0ea3d28c88b2aef7c650bbe5f85a67c712 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 204/417] Fix typo Issue #3594 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167738 --- .../com/google/android/exoplayer2/text/cea/Cea708Decoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 030f0cdbb0..6bdbebc73b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder { private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) - private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) @@ -464,7 +464,7 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF1: case COMMAND_DF2: case COMMAND_DF3: - case COMMAND_DS4: + case COMMAND_DF4: case COMMAND_DF5: case COMMAND_DF6: case COMMAND_DF7: From 8e35bffcc3f6a1bbadf94b748c7ea7e215874e8b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 03:06:10 -0800 Subject: [PATCH 205/417] Use playAd/stopAd to control position updates switching Previously the ad/content progress updates were toggled based on whether the player was playing ads or content. After this change, we switch based on whether playAd/stopAd has been called instead. This seems to resolve an issue where occasionally the player would get stuck at the start of an ad, but as I don't have a root cause for that issue and it's only sporadically reproducible I'm not certain this is a reliable fix. Issue: #3525 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179169296 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 0f0f64c068..284d716582 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 @@ -547,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || !hasContentDuration) { + } else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); @@ -560,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A public VideoProgressUpdate getAdProgress() { if (player == null) { return lastAdProgress; - } else if (!playingAd) { + } else if (imaAdState == IMA_AD_STATE_NONE) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { long adDuration = player.getDuration(); From bb8c60879517280a1fc10601ed871f377c1f5138 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 15 Dec 2017 03:42:17 -0800 Subject: [PATCH 206/417] Make updating showTimeoutMs takes effect immediately. Update PlaybackControlView and SimpleExoPlayerView so when showTimeoutMs is set while the controller is shown, the new timeout takes effect immediately. GitHub: #3554 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179171727 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 4 ++++ .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 751a6c81a9..7659dff9c6 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -519,6 +519,10 @@ public class PlaybackControlView extends FrameLayout { */ public void setShowTimeoutMs(int showTimeoutMs) { this.showTimeoutMs = showTimeoutMs; + // showTimeoutMs is changed, so call hideAfterTimeout to reset the timeout. + if (isVisible()) { + hideAfterTimeout(); + } } /** diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 1f67b83ba0..c5a4bc8086 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -613,6 +613,11 @@ public final class SimpleExoPlayerView extends FrameLayout { public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { Assertions.checkState(controller != null); this.controllerShowTimeoutMs = controllerShowTimeoutMs; + // If controller is already visible, call showController to update the controller's timeout + // if necessary. + if (controller.isVisible()) { + showController(); + } } /** From 0287f13c0a10de2a97867125b185702e3eed6978 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 11:59:36 -0800 Subject: [PATCH 207/417] Fix analyze/lint errors - Lint doesn't like a static import of something not available on the minimum API level. - The method linked to in the Javadoc was incorrect (wrong signature). I couldn't really work out why it was there, so I got rid of it rather than updating. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179222587 --- .../android/exoplayer2/audio/ChannelMappingAudioProcessor.java | 2 -- .../java/com/google/android/exoplayer2/video/DummySurface.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index c3f3e32526..17b90680dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -51,8 +51,6 @@ import java.util.Arrays; /** * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. - * - * @see AudioSink#configure(String, int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2c172c086b..9fcf89d628 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -24,7 +24,6 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; -import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -326,7 +325,7 @@ public final class DummySurface extends Surface { EGLSurface surface; if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { - surface = EGL_NO_SURFACE; + surface = EGL14.EGL_NO_SURFACE; } else { int[] pbufferAttributes; if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { From d9bee4d29c54eb12ca64b64e6f455add96d242ba Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 12:38:05 -0800 Subject: [PATCH 208/417] Bump version to 2.6.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179227114 --- constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 ++-- demos/ima/src/main/AndroidManifest.xml | 4 ++-- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/constants.gradle b/constants.gradle index bad69389a5..c18fb28d4d 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = '2.6.0' + releaseVersion = '2.6.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 8aaef5f8ce..e12e27fa4c 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index f14feeda74..0efeaf6f7f 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index ec8016e8a3..00326157a2 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2601" + android:versionName="2.6.1"> 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 f13a7de0ca..b2200b6671 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 @@ -31,13 +31,13 @@ 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.6.0"; + public static final String VERSION = "2.6.1"; /** * 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.6.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,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 = 2006000; + public static final int VERSION_INT = 2006001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 539d291fce5928adbb638dea67dd415425ff2d10 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Dec 2017 04:44:58 -0800 Subject: [PATCH 209/417] Fix some lint/analyze errors ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179406910 --- .../exoplayer2/drm/OfflineLicenseHelperTest.java | 2 ++ .../exoplayer2/extractor/ts/AdtsReaderTest.java | 1 + .../upstream/cache/CachedContentIndexTest.java | 2 ++ .../upstream/cache/CachedRegionTrackerTest.java | 3 ++- .../exoplayer2/upstream/cache/SimpleCacheSpanTest.java | 2 ++ .../exoplayer2/audio/ChannelMappingAudioProcessor.java | 2 ++ .../exoplayer2/audio/ResamplingAudioProcessor.java | 1 + .../android/exoplayer2/extractor/ts/LatmReader.java | 3 +-- .../trackselection/DefaultTrackSelector.java | 7 ++++--- .../com/google/android/exoplayer2/video/ColorInfo.java | 10 ++++------ .../exoplayer2/testutil/FakeSimpleExoPlayer.java | 8 +++----- 11 files changed, 24 insertions(+), 17 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 02b29a31b5..76730abc6e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -38,6 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); MockitoUtil.setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, @@ -48,6 +49,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { protected void tearDown() throws Exception { offlineLicenseHelper.release(); offlineLicenseHelper = null; + super.tearDown(); } public void testDownloadRenewReleaseKey() throws Exception { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index 6a31250e15..1a10d24c94 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -69,6 +69,7 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { + super.setUp(); FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index 7f6e203c20..17b8313500 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -36,6 +36,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { + super.setUp(); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @@ -43,6 +44,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { @Override protected void tearDown() throws Exception { Util.recursiveDelete(cacheDir); + super.tearDown(); } public void testAddGetRemove() throws Exception { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index f40ae0bc7e..fc4a9cfed6 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -46,8 +46,8 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); MockitoUtil.setUpMockito(this); - tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); @@ -56,6 +56,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void tearDown() throws Exception { Util.recursiveDelete(cacheDir); + super.tearDown(); } public void testGetRegion_noSpansInCache() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java index 8c684b1cb3..eb1cfd82a8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java @@ -48,6 +48,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @@ -55,6 +56,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { @Override protected void tearDown() throws Exception { Util.recursiveDelete(cacheDir); + super.tearDown(); } public void testCacheFile() throws Exception { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 17b90680dd..50b484b938 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -51,6 +51,8 @@ import java.util.Arrays; /** * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. + * + * @see AudioSink#configure(int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index a78adbcee3..d5a18c5ebf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -102,6 +102,7 @@ import java.nio.ByteOrder; resampledSize = size / 2; break; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index d06c6f0cb4..313e556764 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -61,7 +61,6 @@ public final class LatmReader implements ElementaryStreamReader { // Container data. private boolean streamMuxRead; - private int audioMuxVersion; private int audioMuxVersionA; private int numSubframes; private int frameLengthType; @@ -176,7 +175,7 @@ public final class LatmReader implements ElementaryStreamReader { * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { - audioMuxVersion = data.readBits(1); + int audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; if (audioMuxVersionA == 0) { if (audioMuxVersion == 1) { 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 09bd81416c..509e86345e 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import android.content.Context; import android.graphics.Point; +import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -1216,11 +1217,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Compares the score of the current track format with another {@link AudioTrackScore}. * * @param other The other score to compare to. - * @return A positive integer if this score is better than the other. Zero if they are - * equal. A negative integer if this score is worse than the other. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. */ @Override - public int compareTo(AudioTrackScore other) { + public int compareTo(@NonNull AudioTrackScore other) { if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) { return compareInts(this.withinRendererCapabilitiesScore, other.withinRendererCapabilitiesScore); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index 7bdc43f85c..14e40f8605 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -91,12 +91,10 @@ public final class ColorInfo implements Parcelable { return false; } ColorInfo other = (ColorInfo) obj; - if (colorSpace != other.colorSpace || colorRange != other.colorRange - || colorTransfer != other.colorTransfer - || !Arrays.equals(hdrStaticInfo, other.hdrStaticInfo)) { - return false; - } - return true; + return colorSpace == other.colorSpace + && colorRange == other.colorRange + && colorTransfer == other.colorTransfer + && Arrays.equals(hdrStaticInfo, other.hdrStaticInfo); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index d568770219..a8ba3b3420 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -481,11 +481,9 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, long bufferedPositionUs) { - if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { - return true; - } - return - loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, 1f, rebuffering); + return bufferedPositionUs == C.TIME_END_OF_SOURCE + || loadControl.shouldStartPlayback( + bufferedPositionUs - rendererPositionUs, 1f, rebuffering); } private void handlePlayerError(final ExoPlaybackException e) { From 6c2d1e1966625cd9132c0629649f8210764b205a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 19 Dec 2017 09:22:59 -0800 Subject: [PATCH 210/417] Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also attempts to update these when the source info is refreshed. A sorted list of pending posts is kept and the player triggers these posts when the playback position moves over the specified position. Issue:#2189 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179563355 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 403 ++++++++++++++++++ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 ++++--- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 +++++++++--- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 +++++++++++++ .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 ++- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 + .../android/exoplayer2/testutil/Action.java | 60 +++ .../exoplayer2/testutil/ActionSchedule.java | 63 +++ .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 18 files changed, 1280 insertions(+), 232 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c7f7ed7bbd..3c45c3449a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler. + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 0a902e2efe..0f8df65959 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -119,9 +119,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, - LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - new VpxVideoSurfaceView(context))); + player + .createMessage(videoRenderer) + .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) + .setMessage(new VpxVideoSurfaceView(context)) + .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 40b4b2d383..9d8e2dcd9d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,11 +17,13 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -942,4 +944,405 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } + + public void testSendMessagesDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testMultipleSendMessages() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target80, /* positionMs= */ 80) + .sendMessage(target50, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target50.positionMs >= 50); + assertTrue(target80.positionMs >= 80); + assertTrue(target80.positionMs >= target50.positionMs); + } + + public void testMultipleSendMessagesAtSameTime() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* positionMs= */ 50) + .sendMessage(target2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target1.positionMs >= 50); + assertTrue(target2.positionMs >= 50); + } + + public void testSendMessagesMultiPeriodResolution() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); + long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) + .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) + .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline, null), + /* resetPosition= */ false, + /* resetState= */ true) + .waitForPlaybackState(Player.STATE_READY) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, targetStartFirstPeriod.windowIndex); + assertTrue(targetStartFirstPeriod.positionMs >= 0); + assertEquals(0, targetEndMiddlePeriod.windowIndex); + assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); + assertEquals(1, targetStartMiddlePeriod.windowIndex); + assertTrue(targetStartMiddlePeriod.positionMs >= 0); + assertEquals(1, targetEndLastPeriod.windowIndex); + assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); + } + + public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesRepeatDoesNotRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(1, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage( + target, + /* windowIndex= */ 0, + /* positionMs= */ 50, + /* deleteAfterDelivery= */ false) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveCurrentWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(1, target.windowIndex); + } + + public void testSendMessagesMultiWindowDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMultiWindowAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .waitForTimelineChanged(secondTimeline) + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(0, target.windowIndex); + } + + public void testSendMessagesNonLinearPeriodOrder() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) + .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) + .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForPositionDiscontinuity() + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, target1.windowIndex); + assertEquals(1, target2.windowIndex); + assertEquals(2, target3.windowIndex); + } + + private static final class PositionGrabbingMessageTarget extends PlayerTarget { + + public int windowIndex; + public long positionMs; + public int messageCount; + + public PositionGrabbingMessageTarget() { + windowIndex = C.INDEX_UNSET; + positionMs = C.POSITION_UNSET; + } + + @Override + public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + messageCount++; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a4103787d1..8ee9a13c55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index cc767752be..4bd28150bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,40 +34,43 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from - * {@link ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link + * ExoPlayerFactory}. * *

      Player components

      + * *

      ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: + * *

        *
      • A {@link MediaSource} that defines the media to be played, loads the media, and from - * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)} - * at the start of playback. The library modules provide default implementations for regular media - * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) - * and HLS (HlsMediaSource), an implementation for loading single media samples - * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and - * implementations for building more complex MediaSources from simpler ones - * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, - * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and - * {@link ClippingMediaSource}).
      • + * which the loaded media can be read. A MediaSource is injected via {@link + * #prepare(MediaSource)} at the start of playback. The library modules provide default + * implementations for regular media files ({@link ExtractorMediaSource}), DASH + * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an + * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's + * most often used for side-loaded subtitle files, and implementations for building more + * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link + * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link + * LoopingMediaSource} and {@link ClippingMediaSource}). *
      • {@link Renderer}s that render individual components of the media. The library - * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, - * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer - * consumes media from the MediaSource being played. Renderers are injected when the player is - * created.
      • + * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, + * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A + * Renderer consumes media from the MediaSource being played. Renderers are injected when the + * player is created. *
      • A {@link TrackSelector} that selects tracks provided by the MediaSource to be - * consumed by each of the available Renderers. The library provides a default implementation - * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when - * the player is created.
      • + * consumed by each of the available Renderers. The library provides a default implementation + * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected + * when the player is created. *
      • A {@link LoadControl} that controls when the MediaSource buffers more media, and how - * much media is buffered. The library provides a default implementation - * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the - * player is created.
      • + * much media is buffered. The library provides a default implementation ({@link + * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player + * is created. *
      + * *

      An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -81,30 +84,32 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * it's possible to load data from a non-standard source, or through a different network stack. * *

      Threading model

      - *

      The figure below shows ExoPlayer's threading model.

      - *

      - * ExoPlayer's threading model - *

      + * + *

      The figure below shows ExoPlayer's threading model. + * + *

      ExoPlayer's threading
+ * model * *

        - *
      • It is recommended that ExoPlayer instances are created and accessed from a single application - * thread. The application's main thread is ideal. Accessing an instance from multiple threads is - * discouraged, however if an application does wish to do this then it may do so provided that it - * ensures accesses are synchronized.
      • - *
      • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, - * registered listeners will be called on the application's main thread.
      • - *
      • An internal playback thread is responsible for playback. Injected player components such as - * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this - * thread.
      • - *
      • When the application performs an operation on the player, for example a seek, a message is - * delivered to the internal playback thread via a message queue. The internal playback thread - * consumes messages from the queue and performs the corresponding operations. Similarly, when a - * playback event occurs on the internal playback thread, a message is delivered to the application - * thread via a second message queue. The application thread consumes messages from the queue, - * updating the application visible state and calling corresponding listener methods.
      • - *
      • Injected player components may use additional background threads. For example a MediaSource - * may use background threads to load data. These are implementation specific.
      • + *
      • It is recommended that ExoPlayer instances are created and accessed from a single + * application thread. The application's main thread is ideal. Accessing an instance from + * multiple threads is discouraged, however if an application does wish to do this then it may + * do so provided that it ensures accesses are synchronized. + *
      • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that + * case, registered listeners will be called on the application's main thread. + *
      • An internal playback thread is responsible for playback. Injected player components such as + * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this + * thread. + *
      • When the application performs an operation on the player, for example a seek, a message is + * delivered to the internal playback thread via a message queue. The internal playback thread + * consumes messages from the queue and performs the corresponding operations. Similarly, when + * a playback event occurs on the internal playback thread, a message is delivered to the + * application thread via a second message queue. The application thread consumes messages + * from the queue, updating the application visible state and calling corresponding listener + * methods. + *
      • Injected player components may use additional background threads. For example a MediaSource + * may use background threads to load data. These are implementation specific. *
      */ public interface ExoPlayer extends Player { @@ -115,54 +120,28 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** - * A component of an {@link ExoPlayer} that can receive messages on the playback thread. - *

      - * Messages can be delivered to a component via {@link #sendMessages} and - * {@link #blockingSendMessages}. - */ - interface ExoPlayerComponent { + /** @deprecated Use {@link PlayerMessage.Target} instead. */ + @Deprecated + interface ExoPlayerComponent extends PlayerMessage.Target {} - /** - * Handles a message delivered to the component. Called on the playback thread. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - - } - - /** - * Defines a message and a target {@link ExoPlayerComponent} to receive it. - */ + /** @deprecated Use {@link PlayerMessage} instead. */ + @Deprecated final class ExoPlayerMessage { - /** - * The target to receive the message. - */ - public final ExoPlayerComponent target; - /** - * The type of the message. - */ + /** The target to receive the message. */ + public final PlayerMessage.Target target; + /** The type of the message. */ public final int messageType; - /** - * The message. - */ + /** The message. */ public final Object message; - /** - * @param target The target of the message. - * @param messageType The message type. - * @param message The message. - */ - public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { + /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ + @Deprecated + public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } - } /** @@ -236,20 +215,25 @@ public interface ExoPlayer extends Player { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sends messages to their target components. The messages are delivered on the playback thread. - * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player - * as an error. - * - * @param messages The messages to be sent. + * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message + * will be delivered immediately without blocking on the playback thread. The default {@link + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be + * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. + * Alternatively, the message can be sent at a specific window using {@link + * PlayerMessage#setPosition(int, long)}. */ + PlayerMessage createMessage(PlayerMessage.Target target); + + /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ + @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have - * been delivered. - * - * @param messages The messages to be sent. + * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link + * PlayerMessage#blockUntilDelivered()}. */ + @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2869a7668e..afb6428fa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -31,6 +32,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -45,6 +48,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; + private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -113,6 +117,7 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -326,12 +331,47 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { - internalPlayer.sendMessages(messages); + for (ExoPlayerMessage message : messages) { + createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + } + } + + @Override + public PlayerMessage createMessage(Target target) { + return new PlayerMessage( + internalPlayer, + target, + playbackInfo.timeline, + getCurrentWindowIndex(), + internalPlayerHandler); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - internalPlayer.blockingSendMessages(messages); + List playerMessages = new ArrayList<>(); + for (ExoPlayerMessage message : messages) { + playerMessages.add( + createMessage(message.target) + .setType(message.messageType) + .setMessage(message.message) + .send()); + } + boolean wasInterrupted = false; + for (PlayerMessage message : playerMessages) { + boolean blockMessage = true; + while (blockMessage) { + try { + message.blockUntilDelivered(); + blockMessage = false; + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } @Override 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 09b3231467..f3d0e1794b 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 @@ -22,10 +22,10 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,14 +40,19 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; -/** - * Implements the internal behavior of {@link ExoPlayerImpl}. - */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, - PlaybackParameterListener { +/** Implements the internal behavior of {@link ExoPlayerImpl}. */ +/* package */ final class ExoPlayerImplInternal + implements Handler.Callback, + MediaPeriod.Callback, + TrackSelector.InvalidationListener, + MediaSource.Listener, + PlaybackParameterListener, + PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; @@ -108,6 +113,7 @@ import java.io.IOException; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; + private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,13 +126,12 @@ import java.io.IOException; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int customMessagesSent; - private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; + private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -166,6 +171,7 @@ import java.io.IOException; rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); + customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -214,34 +220,15 @@ import java.io.IOException; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void sendMessages(ExoPlayerMessage... messages) { + @Override + public synchronized void sendMessage( + PlayerMessage message, PlayerMessage.Sender.Listener listener) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); + listener.onMessageDeleted(); return; } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - } - - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - return; - } - int messageNumber = customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - boolean wasInterrupted = false; - while (customMessagesProcessed <= messageNumber) { - try { - wait(); - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); } public synchronized void release() { @@ -349,7 +336,7 @@ import java.io.IOException; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + sendMessageInternal((CustomMessageInfo) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -537,8 +524,9 @@ import java.io.IOException; } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); + playbackInfo.positionUs = periodPositionUs; } - playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -656,7 +644,8 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = resolveSeekPosition(seekPosition); + Pair periodPosition = + resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -850,6 +839,11 @@ import java.io.IOException; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); + for (CustomMessageInfo customMessageInfo : customMessageInfos) { + customMessageInfo.listener.onMessageDeleted(); + } + customMessageInfos.clear(); + nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -870,21 +864,153 @@ import java.io.IOException; } } - private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { - try { - for (ExoPlayerMessage message : messages) { - message.target.handleMessage(message.messageType, message.message); + private void sendMessageInternal(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + // If no delivery time is specified, trigger immediate message delivery. + sendCustomMessagesToTarget(customMessageInfo); + } else if (playbackInfo.timeline == null) { + // Still waiting for initial timeline to resolve position. + customMessageInfos.add(customMessageInfo); + } else { + if (resolveCustomMessagePosition(customMessageInfo)) { + customMessageInfos.add(customMessageInfo); + // Ensure new message is inserted according to playback order. + Collections.sort(customMessageInfos); + } else { + customMessageInfo.listener.onMessageDeleted(); } - if (playbackInfo.playbackState == Player.STATE_READY - || playbackInfo.playbackState == Player.STATE_BUFFERING) { - // The message may have caused something to change that now requires us to do work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + + private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { + final Runnable handleMessageRunnable = + new Runnable() { + @Override + public void run() { + try { + customMessageInfo + .message + .getTarget() + .handleMessage( + customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + customMessageInfo.listener.onMessageDelivered(); + if (customMessageInfo.message.getDeleteAfterDelivery()) { + customMessageInfo.listener.onMessageDeleted(); + } + // The message may have caused something to change that now requires us to do + // work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + }; + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } + + private void resolveCustomMessagePositions() { + for (int i = customMessageInfos.size() - 1; i >= 0; i--) { + if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { + // Remove messages if new position can't be resolved. + customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.remove(i); } - } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + } + // Re-sort messages by playback order. + Collections.sort(customMessageInfos); + } + + private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in current timeline. + Pair periodPosition = + resolveSeekPosition( + new SeekPosition( + customMessageInfo.message.getTimeline(), + customMessageInfo.message.getWindowIndex(), + C.msToUs(customMessageInfo.message.getPositionMs())), + /* trySubsequentPeriods= */ false); + if (periodPosition == null) { + return false; } + customMessageInfo.setResolvedPosition( + periodPosition.first, + periodPosition.second, + playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); + } else { + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + customMessageInfo.resolvedPeriodIndex = index; + } + return true; + } + + private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { + if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { + return; + } + // If this is the first call from the start position, include oldPeriodPositionUs in potential + // trigger positions. + if (playbackInfo.startPositionUs == oldPeriodPositionUs) { + oldPeriodPositionUs--; + } + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) + int currentPeriodIndex = playbackInfo.periodId.periodIndex; + CustomMessageInfo prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + while (prevInfo != null + && (prevInfo.resolvedPeriodIndex > currentPeriodIndex + || (prevInfo.resolvedPeriodIndex == currentPeriodIndex + && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { + nextCustomMessageInfoIndex--; + prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + } + CustomMessageInfo nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && (nextInfo.resolvedPeriodIndex < currentPeriodIndex + || (nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { + nextCustomMessageInfoIndex++; + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + } + // Check if any message falls within the covered time span. + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs + && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { + sendCustomMessagesToTarget(nextInfo); + if (nextInfo.message.getDeleteAfterDelivery()) { + customMessageInfos.remove(nextCustomMessageInfoIndex); + } else { + nextCustomMessageInfoIndex++; + } + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; } } @@ -1034,12 +1160,14 @@ import java.io.IOException; Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + Pair periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1224,11 +1352,14 @@ import java.io.IOException; * internal timeline. * * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition(SeekPosition seekPosition) { + private Pair resolveSeekPosition( + SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1257,12 +1388,14 @@ import java.io.IOException; // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, - C.TIME_UNSET); + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition( + timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + } } // We didn't find one. Give up. return null; @@ -1802,7 +1935,45 @@ import java.io.IOException; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } + } + private static final class CustomMessageInfo implements Comparable { + + public final PlayerMessage message; + public final PlayerMessage.Sender.Listener listener; + + public int resolvedPeriodIndex; + public long resolvedPeriodTimeUs; + public @Nullable Object resolvedPeriodUid; + + public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + this.message = message; + this.listener = listener; + } + + public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { + resolvedPeriodIndex = periodIndex; + resolvedPeriodTimeUs = periodTimeUs; + resolvedPeriodUid = periodUid; + } + + @Override + public int compareTo(@NonNull CustomMessageInfo other) { + if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { + // CustomMessageInfos with a resolved period position are always smaller. + return resolvedPeriodUid != null ? -1 : 1; + } + if (resolvedPeriodUid == null) { + // Don't sort message with unresolved positions. + return 0; + } + // Sort resolved media times by period index and then by period position. + int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; + if (comparePeriodIndex != 0) { + return comparePeriodIndex; + } + return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); + } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 978f4f7a97..593d3d1fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java new file mode 100644 index 0000000000..44a4b0c7c2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2017 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; + +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; + +/** + * Defines a player message which can be sent with a {@link Sender} and received by a {@link + * Target}. + */ +public final class PlayerMessage { + + /** A target for messages. */ + public interface Target { + + /** + * Handles a message delivered to the target. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + } + + /** A sender for messages. */ + public interface Sender { + + /** A listener for message events triggered by the sender. */ + interface Listener { + + /** Called when the message has been delivered. */ + void onMessageDelivered(); + + /** Called when the message has been deleted. */ + void onMessageDeleted(); + } + + /** + * Sends a message. + * + * @param message The message to be sent. + * @param listener The listener to listen to message events. + */ + void sendMessage(PlayerMessage message, Listener listener); + } + + private final Target target; + private final Sender sender; + private final Timeline timeline; + + private int type; + private Object message; + private Handler handler; + private int windowIndex; + private long positionMs; + private boolean deleteAfterDelivery; + private boolean isSent; + private boolean isDelivered; + private boolean isDeleted; + + /** + * Creates a new message. + * + * @param sender The {@link Sender} used to send the message. + * @param target The {@link Target} the message is sent to. + * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If + * set to {@link Timeline#EMPTY}, any position can be specified. + * @param defaultWindowIndex The default window index in the {@code timeline} when no other window + * index is specified. + * @param defaultHandler The default handler to send the message on when no other handler is + * specified. + */ + public PlayerMessage( + Sender sender, + Target target, + Timeline timeline, + int defaultWindowIndex, + Handler defaultHandler) { + this.sender = sender; + this.target = target; + this.timeline = timeline; + this.handler = defaultHandler; + this.windowIndex = defaultWindowIndex; + this.positionMs = C.TIME_UNSET; + this.deleteAfterDelivery = true; + } + + /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ + public Timeline getTimeline() { + return timeline; + } + + /** Returns the target the message is sent to. */ + public Target getTarget() { + return target; + } + + /** + * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param messageType The custom message type. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setType(int messageType) { + Assertions.checkState(!isSent); + this.type = messageType; + return this; + } + + /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + public int getType() { + return type; + } + + /** + * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param message The custom message. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setMessage(@Nullable Object message) { + Assertions.checkState(!isSent); + this.message = message; + return this; + } + + /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ + public Object getMessage() { + return message; + } + + /** + * Sets the handler the message is delivered on. + * + * @param handler A {@link Handler}. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setHandler(Handler handler) { + Assertions.checkState(!isSent); + this.handler = handler; + return this; + } + + /** Returns the handler the message is delivered on. */ + public Handler getHandler() { + return handler; + } + + /** + * Sets a position in the current window at which the message will be delivered. + * + * @param positionMs The position in the current window at which the message will be sent, in + * milliseconds. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(long positionMs) { + Assertions.checkState(!isSent); + this.positionMs = positionMs; + return this; + } + + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + + /** + * Sets a position in a window at which the message will be delivered. + * + * @param windowIndex The index of the window at which the message will be sent. + * @param positionMs The position in the window with index {@code windowIndex} at which the + * message will be sent, in milliseconds. + * @return This message. + * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not + * empty and the provided window index is not within the bounds of the timeline. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(int windowIndex, long positionMs) { + Assertions.checkState(!isSent); + Assertions.checkArgument(positionMs != C.TIME_UNSET); + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); + } + this.windowIndex = windowIndex; + this.positionMs = positionMs; + return this; + } + + /** Returns window index at which the message will be delivered. */ + public int getWindowIndex() { + return windowIndex; + } + + /** + * Sets whether the message will be deleted after delivery. If false, the message will be resent + * if playback reaches the specified position again. Only allowed to be false if a position is set + * with {@link #setPosition(long)}. + * + * @param deleteAfterDelivery Whether the message is deleted after delivery. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { + Assertions.checkState(!isSent); + this.deleteAfterDelivery = deleteAfterDelivery; + return this; + } + + /** Returns whether the message will be deleted after delivery. */ + public boolean getDeleteAfterDelivery() { + return deleteAfterDelivery; + } + + /** + * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated + * out of the player as an error using {@link + * Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage send() { + Assertions.checkState(!isSent); + if (positionMs == C.TIME_UNSET) { + Assertions.checkArgument(deleteAfterDelivery); + } + isSent = true; + sender.sendMessage( + this, + new Sender.Listener() { + @Override + public void onMessageDelivered() { + synchronized (PlayerMessage.this) { + isDelivered = true; + PlayerMessage.this.notifyAll(); + } + } + + @Override + public void onMessageDeleted() { + synchronized (PlayerMessage.this) { + isDeleted = true; + PlayerMessage.this.notifyAll(); + } + } + }); + return this; + } + + /** + * Blocks until after the message has been delivered or the player is no longer able to deliver + * the message. + * + *

      Note that this method can't be called if the current thread is the same thread used by the + * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + * + * @return Whether the message was delivered successfully. + * @throws IllegalStateException If this method is called before {@link #send()}. + * @throws IllegalStateException If this method is called on the same thread used by the message + * handler set with {@link #setHandler(Handler)}. + * @throws InterruptedException If the current thread is interrupted while waiting for the message + * to be delivered. + */ + public synchronized boolean blockUntilDelivered() throws InterruptedException { + Assertions.checkState(!isSent); + Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + while (!isDelivered && !isDeleted) { + wait(); + } + return isDelivered; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 6def1591da..d0a07930e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,22 +15,20 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - *

      - * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + * + *

      Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - *

      - * Renderer state transitions - *

      + * + *

      Renderer state transitions */ -public interface Renderer extends ExoPlayerComponent { +public interface Renderer extends PlayerMessage.Target { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 69369d4229..e2d0ed1422 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,8 +93,6 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; - private final int videoRendererCount; - private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -124,25 +122,6 @@ public class SimpleExoPlayer implements ExoPlayer { renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); - // Obtain counts of video and audio renderers. - int videoRendererCount = 0; - int audioRendererCount = 0; - for (Renderer renderer : renderers) { - switch (renderer.getTrackType()) { - case C.TRACK_TYPE_VIDEO: - videoRendererCount++; - break; - case C.TRACK_TYPE_AUDIO: - audioRendererCount++; - break; - default: - // Don't count other track types. - break; - } - } - this.videoRendererCount = videoRendererCount; - this.audioRendererCount = audioRendererCount; - // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -163,15 +142,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, - videoScalingMode); + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setMessage(videoScalingMode) + .send(); } } - player.sendMessages(messages); } /** @@ -352,15 +331,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, - audioAttributes); + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setMessage(audioAttributes) + .send(); } } - player.sendMessages(messages); } /** @@ -377,14 +356,11 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); } } - player.sendMessages(messages); } /** @@ -770,6 +746,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.sendMessages(messages); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + return player.createMessage(target); + } + @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -908,22 +889,25 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; + boolean surfaceReplaced = this.surface != null && this.surface != surface; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); + PlayerMessage message = + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); + if (surfaceReplaced) { + try { + message.blockUntilDelivered(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } } - if (this.surface != null && this.surface != surface) { - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); + if (surfaceReplaced) { // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - } else { - player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c410456e7b..54537ba548 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,8 +23,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -42,7 +41,7 @@ import java.util.Map; * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { +public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -147,8 +146,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, - new MessageData<>(index, mediaSource, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD) + .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -220,8 +222,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - new MessageData<>(index, mediaSources, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -256,8 +261,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, - new MessageData<>(index, null, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_REMOVE) + .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -293,8 +301,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_MOVE) + .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -427,8 +438,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.sendMessages( - new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); } } } 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 d796e6936f..a5f5222820 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 @@ -561,6 +561,18 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Compares two long values and returns the same value as {@code Long.compare(long, long)}. + * + * @param left The left operand. + * @param right The right operand. + * @return 0, if left == right, a negative value if left < right, or a positive value if left + * > right. + */ + public static int compareLong(long left, long right) { + return left < right ? -1 : left == right ? 0 : 1; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * 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 ff0b8a6bc0..5ec45af29f 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 @@ -18,13 +18,17 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -345,7 +349,63 @@ public abstract class Action { Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } + } + /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ + public static final class SendMessages extends Action { + + private final Target target; + private final int windowIndex; + private final long positionMs; + private final boolean deleteAfterDelivery; + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param positionMs The position at which the message should be sent, in milliseconds. + */ + public SendMessages(String tag, Target target, long positionMs) { + this( + tag, + target, + /* windowIndex= */ C.INDEX_UNSET, + positionMs, + /* deleteAfterDelivery= */ true); + } + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param windowIndex The window index at which the message should be sent, or {@link + * C#INDEX_UNSET} for the current window. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + */ + public SendMessages( + String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + super(tag, "SendMessages"); + this.target = target; + this.windowIndex = windowIndex; + this.positionMs = positionMs; + this.deleteAfterDelivery = deleteAfterDelivery; + } + + @Override + protected void doActionImpl( + final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (target instanceof PlayerTarget) { + ((PlayerTarget) target).setPlayer(player); + } + PlayerMessage message = player.createMessage(target); + if (windowIndex != C.INDEX_UNSET) { + message.setPosition(windowIndex, positionMs); + } else { + message.setPosition(positionMs); + } + message.setHandler(new Handler()); + message.setDeleteAfterDelivery(deleteAfterDelivery); + message.send(); + } } /** 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 477071f91f..2ac487c98e 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 @@ -20,8 +20,11 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -29,6 +32,7 @@ import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -315,6 +319,44 @@ public final class ActionSchedule { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param positionMs The position in the current window at which the message should be sent, in + * milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, long positionMs) { + return apply(new SendMessages(tag, target, positionMs)); + } + + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, int windowIndex, long positionMs) { + return apply( + new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); + } + + /** + * Schedules to send a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + * @return The builder, for convenience. + */ + public Builder sendMessage( + Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -365,7 +407,28 @@ public final class ActionSchedule { currentDelayMs = 0; return this; } + } + /** + * Provides a wrapper for a {@link Target} which has access to the player when handling messages. + * Can be used with {@link Builder#sendMessage(Target, long)}. + */ + public abstract static class PlayerTarget implements Target { + + private SimpleExoPlayer player; + + /** Handles the message send to the component and additionally provides access to the player. */ + public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); + + /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + handleMessage(player, messageType, message); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 4a9d79f906..797c09d6b6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -170,7 +171,7 @@ public final class FakeTimeline extends Timeline { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? periodIndex : null; + Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -198,11 +199,13 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; + Period period = new Period(); + for (int i = 0; i < getPeriodCount(); i++) { + if (getPeriod(i, period, true).uid.equals(uid)) { + return i; + } } - int index = (Integer) uid; - return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; + return C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 4f31a8b027..93c14afc8f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -281,7 +283,8 @@ public class MediaSourceTestRunner { } - private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static class EventHandlingExoPlayer extends StubExoPlayer + implements Handler.Callback, PlayerMessage.Sender { private final Handler handler; @@ -290,23 +293,33 @@ public class MediaSourceTestRunner { } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); + public PlayerMessage createMessage(PlayerMessage.Target target) { + return new PlayerMessage( + /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); } @Override + public void sendMessage(PlayerMessage message, Listener listener) { + handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + } + + @Override + @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + Pair messageAndListener = (Pair) msg.obj; + try { + messageAndListener + .first + .getTarget() + .handleMessage( + messageAndListener.first.getType(), messageAndListener.first.getMessage()); + messageAndListener.second.onMessageDelivered(); + messageAndListener.second.onMessageDeleted(); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); } return true; } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 1ea83bf1ec..7164fa13ab 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -146,6 +147,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + throw new UnsupportedOperationException(); + } + @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); From 3b633f81b2e84f91d02dd68c8f1e0656ff757a6a Mon Sep 17 00:00:00 2001 From: tiffanywong Date: Wed, 20 Dec 2017 03:12:51 -0800 Subject: [PATCH 211/417] Automated g4 rollback of changelist 179563355. *** Original change description *** Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also at... *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179666357 --- RELEASENOTES.md | 4 - .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 403 ------------------ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 +++---- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 +++--------- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 ------------- .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 +-- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 - .../android/exoplayer2/testutil/Action.java | 60 --- .../exoplayer2/testutil/ActionSchedule.java | 63 --- .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 - 18 files changed, 232 insertions(+), 1280 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c45c3449a..c7f7ed7bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,10 +6,6 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. - * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow - more customization of the message. Now supports setting a message delivery - playback position and/or a delivery handler. - ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 0f8df65959..0a902e2efe 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -119,11 +119,9 @@ public class VpxPlaybackTest extends InstrumentationTestCase { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player - .createMessage(videoRenderer) - .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) - .setMessage(new VpxVideoSurfaceView(context)) - .send(); + player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, + LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, + new VpxVideoSurfaceView(context))); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 9d8e2dcd9d..40b4b2d383 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,13 +17,11 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; -import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -944,405 +942,4 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } - - public void testSendMessagesDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testMultipleSendMessages() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target80, /* positionMs= */ 80) - .sendMessage(target50, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target50.positionMs >= 50); - assertTrue(target80.positionMs >= 80); - assertTrue(target80.positionMs >= target50.positionMs); - } - - public void testMultipleSendMessagesAtSameTime() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target1, /* positionMs= */ 50) - .sendMessage(target2, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target1.positionMs >= 50); - assertTrue(target2.positionMs >= 50); - } - - public void testSendMessagesMultiPeriodResolution() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 2); - PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); - long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); - long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) - .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) - .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) - .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) - // Add additional prepare at end and wait until it's processed to ensure that - // messages sent at end of playback are received before test ends. - .waitForPlaybackState(Player.STATE_ENDED) - .prepareSource( - new FakeMediaSource(timeline, null), - /* resetPosition= */ false, - /* resetState= */ true) - .waitForPlaybackState(Player.STATE_READY) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - assertEquals(0, targetStartFirstPeriod.windowIndex); - assertTrue(targetStartFirstPeriod.positionMs >= 0); - assertEquals(0, targetEndMiddlePeriod.windowIndex); - assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); - assertEquals(1, targetStartMiddlePeriod.windowIndex); - assertTrue(targetStartMiddlePeriod.positionMs >= 0); - assertEquals(1, targetEndLastPeriod.windowIndex); - assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); - } - - public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .seek(/* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) - .seek(/* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .seek(/* positionMs= */ 51) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(C.POSITION_UNSET, target.positionMs); - } - - public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) - .seek(/* positionMs= */ 51) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(C.POSITION_UNSET, target.positionMs); - } - - public void testSendMessagesRepeatDoesNotRepost() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .setRepeatMode(Player.REPEAT_MODE_ALL) - .waitForPositionDiscontinuity() - .setRepeatMode(Player.REPEAT_MODE_OFF) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(1, target.messageCount); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage( - target, - /* windowIndex= */ 0, - /* positionMs= */ 50, - /* deleteAfterDelivery= */ false) - .setRepeatMode(Player.REPEAT_MODE_ALL) - .waitForPositionDiscontinuity() - .setRepeatMode(Player.REPEAT_MODE_OFF) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(2, target.messageCount); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesMoveCurrentWindowIndex() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* positionMs= */ 50) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(secondTimeline, null); - } - }) - .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - assertEquals(1, target.windowIndex); - } - - public void testSendMessagesMultiWindowDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(2, target.windowIndex); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesMultiWindowAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(2, target.windowIndex); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesMoveWindowIndex() throws Exception { - Timeline timeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); - final Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(secondTimeline, null); - } - }) - .waitForTimelineChanged(secondTimeline) - .seek(/* windowIndex= */ 0, /* positionMs= */ 0) - .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - assertEquals(0, target.windowIndex); - } - - public void testSendMessagesNonLinearPeriodOrder() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) - .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) - .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) - .seek(/* windowIndex= */ 1, /* positionMs= */ 0) - .waitForPositionDiscontinuity() - .seek(/* windowIndex= */ 0, /* positionMs= */ 0) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(0, target1.windowIndex); - assertEquals(1, target2.windowIndex); - assertEquals(2, target3.windowIndex); - } - - private static final class PositionGrabbingMessageTarget extends PlayerTarget { - - public int windowIndex; - public long positionMs; - public int messageCount; - - public PositionGrabbingMessageTarget() { - windowIndex = C.INDEX_UNSET; - positionMs = C.POSITION_UNSET; - } - - @Override - public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { - windowIndex = player.getCurrentWindowIndex(); - positionMs = player.getCurrentPosition(); - messageCount++; - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 8ee9a13c55..a4103787d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } - // PlayerMessage.Target implementation. + // ExoPlayerComponent implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 4bd28150bc..cc767752be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,43 +34,40 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link - * ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from + * {@link ExoPlayerFactory}. * *

      Player components

      - * *

      ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: - * *

        *
      • A {@link MediaSource} that defines the media to be played, loads the media, and from - * which the loaded media can be read. A MediaSource is injected via {@link - * #prepare(MediaSource)} at the start of playback. The library modules provide default - * implementations for regular media files ({@link ExtractorMediaSource}), DASH - * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an - * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's - * most often used for side-loaded subtitle files, and implementations for building more - * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link - * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link - * LoopingMediaSource} and {@link ClippingMediaSource}). + * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)} + * at the start of playback. The library modules provide default implementations for regular media + * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) + * and HLS (HlsMediaSource), an implementation for loading single media samples + * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and + * implementations for building more complex MediaSources from simpler ones + * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, + * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and + * {@link ClippingMediaSource}).
      • *
      • {@link Renderer}s that render individual components of the media. The library - * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, - * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A - * Renderer consumes media from the MediaSource being played. Renderers are injected when the - * player is created. + * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, + * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer + * consumes media from the MediaSource being played. Renderers are injected when the player is + * created.
      • *
      • A {@link TrackSelector} that selects tracks provided by the MediaSource to be - * consumed by each of the available Renderers. The library provides a default implementation - * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected - * when the player is created. + * consumed by each of the available Renderers. The library provides a default implementation + * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when + * the player is created.
      • *
      • A {@link LoadControl} that controls when the MediaSource buffers more media, and how - * much media is buffered. The library provides a default implementation ({@link - * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player - * is created. + * much media is buffered. The library provides a default implementation + * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the + * player is created.
      • *
      - * *

      An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -84,32 +81,30 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * it's possible to load data from a non-standard source, or through a different network stack. * *

      Threading model

      - * - *

      The figure below shows ExoPlayer's threading model. - * - *

      ExoPlayer's threading
- * model + *

      The figure below shows ExoPlayer's threading model.

      + *

      + * ExoPlayer's threading model + *

      * *
        - *
      • It is recommended that ExoPlayer instances are created and accessed from a single - * application thread. The application's main thread is ideal. Accessing an instance from - * multiple threads is discouraged, however if an application does wish to do this then it may - * do so provided that it ensures accesses are synchronized. - *
      • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that - * case, registered listeners will be called on the application's main thread. - *
      • An internal playback thread is responsible for playback. Injected player components such as - * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this - * thread. - *
      • When the application performs an operation on the player, for example a seek, a message is - * delivered to the internal playback thread via a message queue. The internal playback thread - * consumes messages from the queue and performs the corresponding operations. Similarly, when - * a playback event occurs on the internal playback thread, a message is delivered to the - * application thread via a second message queue. The application thread consumes messages - * from the queue, updating the application visible state and calling corresponding listener - * methods. - *
      • Injected player components may use additional background threads. For example a MediaSource - * may use background threads to load data. These are implementation specific. + *
      • It is recommended that ExoPlayer instances are created and accessed from a single application + * thread. The application's main thread is ideal. Accessing an instance from multiple threads is + * discouraged, however if an application does wish to do this then it may do so provided that it + * ensures accesses are synchronized.
      • + *
      • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, + * registered listeners will be called on the application's main thread.
      • + *
      • An internal playback thread is responsible for playback. Injected player components such as + * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this + * thread.
      • + *
      • When the application performs an operation on the player, for example a seek, a message is + * delivered to the internal playback thread via a message queue. The internal playback thread + * consumes messages from the queue and performs the corresponding operations. Similarly, when a + * playback event occurs on the internal playback thread, a message is delivered to the application + * thread via a second message queue. The application thread consumes messages from the queue, + * updating the application visible state and calling corresponding listener methods.
      • + *
      • Injected player components may use additional background threads. For example a MediaSource + * may use background threads to load data. These are implementation specific.
      • *
      */ public interface ExoPlayer extends Player { @@ -120,28 +115,54 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** @deprecated Use {@link PlayerMessage.Target} instead. */ - @Deprecated - interface ExoPlayerComponent extends PlayerMessage.Target {} + /** + * A component of an {@link ExoPlayer} that can receive messages on the playback thread. + *

      + * Messages can be delivered to a component via {@link #sendMessages} and + * {@link #blockingSendMessages}. + */ + interface ExoPlayerComponent { - /** @deprecated Use {@link PlayerMessage} instead. */ - @Deprecated + /** + * Handles a message delivered to the component. Called on the playback thread. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + + } + + /** + * Defines a message and a target {@link ExoPlayerComponent} to receive it. + */ final class ExoPlayerMessage { - /** The target to receive the message. */ - public final PlayerMessage.Target target; - /** The type of the message. */ + /** + * The target to receive the message. + */ + public final ExoPlayerComponent target; + /** + * The type of the message. + */ public final int messageType; - /** The message. */ + /** + * The message. + */ public final Object message; - /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ - @Deprecated - public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { + /** + * @param target The target of the message. + * @param messageType The message type. + * @param message The message. + */ + public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } + } /** @@ -215,25 +236,20 @@ public interface ExoPlayer extends Player { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message - * will be delivered immediately without blocking on the playback thread. The default {@link - * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a - * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be - * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. - * Alternatively, the message can be sent at a specific window using {@link - * PlayerMessage#setPosition(int, long)}. + * Sends messages to their target components. The messages are delivered on the playback thread. + * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player + * as an error. + * + * @param messages The messages to be sent. */ - PlayerMessage createMessage(PlayerMessage.Target target); - - /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ - @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link - * PlayerMessage#blockUntilDelivered()}. + * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have + * been delivered. + * + * @param messages The messages to be sent. */ - @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index afb6428fa5..2869a7668e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,7 +22,6 @@ import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; -import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -32,8 +31,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -48,7 +45,6 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; - private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -117,7 +113,6 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); - internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -331,47 +326,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { - for (ExoPlayerMessage message : messages) { - createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); - } - } - - @Override - public PlayerMessage createMessage(Target target) { - return new PlayerMessage( - internalPlayer, - target, - playbackInfo.timeline, - getCurrentWindowIndex(), - internalPlayerHandler); + internalPlayer.sendMessages(messages); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - List playerMessages = new ArrayList<>(); - for (ExoPlayerMessage message : messages) { - playerMessages.add( - createMessage(message.target) - .setType(message.messageType) - .setMessage(message.message) - .send()); - } - boolean wasInterrupted = false; - for (PlayerMessage message : playerMessages) { - boolean blockMessage = true; - while (blockMessage) { - try { - message.blockUntilDelivered(); - blockMessage = false; - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + internalPlayer.blockingSendMessages(messages); } @Override 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 f3d0e1794b..09b3231467 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 @@ -22,10 +22,10 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,19 +40,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -/** Implements the internal behavior of {@link ExoPlayerImpl}. */ -/* package */ final class ExoPlayerImplInternal - implements Handler.Callback, - MediaPeriod.Callback, - TrackSelector.InvalidationListener, - MediaSource.Listener, - PlaybackParameterListener, - PlayerMessage.Sender { +/** + * Implements the internal behavior of {@link ExoPlayerImpl}. + */ +/* package */ final class ExoPlayerImplInternal implements Handler.Callback, + MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, + PlaybackParameterListener { private static final String TAG = "ExoPlayerImplInternal"; @@ -113,7 +108,6 @@ import java.util.Collections; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; - private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -126,12 +120,13 @@ import java.util.Collections; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; + private int customMessagesSent; + private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; - private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -171,7 +166,6 @@ import java.util.Collections; rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); - customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -220,15 +214,34 @@ import java.util.Collections; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - @Override - public synchronized void sendMessage( - PlayerMessage message, PlayerMessage.Sender.Listener listener) { + public void sendMessages(ExoPlayerMessage... messages) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); - listener.onMessageDeleted(); return; } - handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); + customMessagesSent++; + handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + } + + public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { + if (released) { + Log.w(TAG, "Ignoring messages sent after release."); + return; + } + int messageNumber = customMessagesSent++; + handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + boolean wasInterrupted = false; + while (customMessagesProcessed <= messageNumber) { + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } public synchronized void release() { @@ -336,7 +349,7 @@ import java.util.Collections; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessageInternal((CustomMessageInfo) msg.obj); + sendMessagesInternal((ExoPlayerMessage[]) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -524,9 +537,8 @@ import java.util.Collections; } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); - maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); - playbackInfo.positionUs = periodPositionUs; } + playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -644,8 +656,7 @@ import java.util.Collections; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = - resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); + Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -839,11 +850,6 @@ import java.util.Collections; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); - for (CustomMessageInfo customMessageInfo : customMessageInfos) { - customMessageInfo.listener.onMessageDeleted(); - } - customMessageInfos.clear(); - nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -864,153 +870,21 @@ import java.util.Collections; } } - private void sendMessageInternal(CustomMessageInfo customMessageInfo) { - if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { - // If no delivery time is specified, trigger immediate message delivery. - sendCustomMessagesToTarget(customMessageInfo); - } else if (playbackInfo.timeline == null) { - // Still waiting for initial timeline to resolve position. - customMessageInfos.add(customMessageInfo); - } else { - if (resolveCustomMessagePosition(customMessageInfo)) { - customMessageInfos.add(customMessageInfo); - // Ensure new message is inserted according to playback order. - Collections.sort(customMessageInfos); - } else { - customMessageInfo.listener.onMessageDeleted(); + private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { + try { + for (ExoPlayerMessage message : messages) { + message.target.handleMessage(message.messageType, message.message); } - } - } - - private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { - final Runnable handleMessageRunnable = - new Runnable() { - @Override - public void run() { - try { - customMessageInfo - .message - .getTarget() - .handleMessage( - customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); - } catch (ExoPlaybackException e) { - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - } finally { - customMessageInfo.listener.onMessageDelivered(); - if (customMessageInfo.message.getDeleteAfterDelivery()) { - customMessageInfo.listener.onMessageDeleted(); - } - // The message may have caused something to change that now requires us to do - // work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } - } - }; - handler.post( - new Runnable() { - @Override - public void run() { - customMessageInfo.message.getHandler().post(handleMessageRunnable); - } - }); - } - - private void resolveCustomMessagePositions() { - for (int i = customMessageInfos.size() - 1; i >= 0; i--) { - if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { - // Remove messages if new position can't be resolved. - customMessageInfos.get(i).listener.onMessageDeleted(); - customMessageInfos.remove(i); + if (playbackInfo.playbackState == Player.STATE_READY + || playbackInfo.playbackState == Player.STATE_BUFFERING) { + // The message may have caused something to change that now requires us to do work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - } - // Re-sort messages by playback order. - Collections.sort(customMessageInfos); - } - - private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { - if (customMessageInfo.resolvedPeriodUid == null) { - // Position is still unresolved. Try to find window in current timeline. - Pair periodPosition = - resolveSeekPosition( - new SeekPosition( - customMessageInfo.message.getTimeline(), - customMessageInfo.message.getWindowIndex(), - C.msToUs(customMessageInfo.message.getPositionMs())), - /* trySubsequentPeriods= */ false); - if (periodPosition == null) { - return false; + } finally { + synchronized (this) { + customMessagesProcessed++; + notifyAll(); } - customMessageInfo.setResolvedPosition( - periodPosition.first, - periodPosition.second, - playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); - } else { - // Position has been resolved for a previous timeline. Try to find the updated period index. - int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); - if (index == C.INDEX_UNSET) { - return false; - } - customMessageInfo.resolvedPeriodIndex = index; - } - return true; - } - - private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { - if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { - return; - } - // If this is the first call from the start position, include oldPeriodPositionUs in potential - // trigger positions. - if (playbackInfo.startPositionUs == oldPeriodPositionUs) { - oldPeriodPositionUs--; - } - // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) - int currentPeriodIndex = playbackInfo.periodId.periodIndex; - CustomMessageInfo prevInfo = - nextCustomMessageInfoIndex > 0 - ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) - : null; - while (prevInfo != null - && (prevInfo.resolvedPeriodIndex > currentPeriodIndex - || (prevInfo.resolvedPeriodIndex == currentPeriodIndex - && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { - nextCustomMessageInfoIndex--; - prevInfo = - nextCustomMessageInfoIndex > 0 - ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) - : null; - } - CustomMessageInfo nextInfo = - nextCustomMessageInfoIndex < customMessageInfos.size() - ? customMessageInfos.get(nextCustomMessageInfoIndex) - : null; - while (nextInfo != null - && nextInfo.resolvedPeriodUid != null - && (nextInfo.resolvedPeriodIndex < currentPeriodIndex - || (nextInfo.resolvedPeriodIndex == currentPeriodIndex - && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { - nextCustomMessageInfoIndex++; - nextInfo = - nextCustomMessageInfoIndex < customMessageInfos.size() - ? customMessageInfos.get(nextCustomMessageInfoIndex) - : null; - } - // Check if any message falls within the covered time span. - while (nextInfo != null - && nextInfo.resolvedPeriodUid != null - && nextInfo.resolvedPeriodIndex == currentPeriodIndex - && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs - && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendCustomMessagesToTarget(nextInfo); - if (nextInfo.message.getDeleteAfterDelivery()) { - customMessageInfos.remove(nextCustomMessageInfoIndex); - } else { - nextCustomMessageInfoIndex++; - } - nextInfo = - nextCustomMessageInfoIndex < customMessageInfos.size() - ? customMessageInfos.get(nextCustomMessageInfoIndex) - : null; } } @@ -1160,14 +1034,12 @@ import java.util.Collections; Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); - resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = - resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); + Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1352,14 +1224,11 @@ import java.util.Collections; * internal timeline. * * @param seekPosition The position to resolve. - * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching - * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition( - SeekPosition seekPosition, boolean trySubsequentPeriods) { + private Pair resolveSeekPosition(SeekPosition seekPosition) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1388,14 +1257,12 @@ import java.util.Collections; // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - if (trySubsequentPeriods) { - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition( - timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); - } + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, + C.TIME_UNSET); } // We didn't find one. Give up. return null; @@ -1935,45 +1802,7 @@ import java.util.Collections; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } - } - private static final class CustomMessageInfo implements Comparable { - - public final PlayerMessage message; - public final PlayerMessage.Sender.Listener listener; - - public int resolvedPeriodIndex; - public long resolvedPeriodTimeUs; - public @Nullable Object resolvedPeriodUid; - - public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { - this.message = message; - this.listener = listener; - } - - public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { - resolvedPeriodIndex = periodIndex; - resolvedPeriodTimeUs = periodTimeUs; - resolvedPeriodUid = periodUid; - } - - @Override - public int compareTo(@NonNull CustomMessageInfo other) { - if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { - // CustomMessageInfos with a resolved period position are always smaller. - return resolvedPeriodUid != null ? -1 : 1; - } - if (resolvedPeriodUid == null) { - // Don't sort message with unresolved positions. - return 0; - } - // Sort resolved media times by period index and then by period position. - int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; - if (comparePeriodIndex != 0) { - return comparePeriodIndex; - } - return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); - } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 593d3d1fce..978f4f7a97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return ADAPTIVE_NOT_SUPPORTED; } - // PlayerMessage.Target implementation. + // ExoPlayerComponent implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java deleted file mode 100644 index 44a4b0c7c2..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright (C) 2017 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; - -import android.os.Handler; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; - -/** - * Defines a player message which can be sent with a {@link Sender} and received by a {@link - * Target}. - */ -public final class PlayerMessage { - - /** A target for messages. */ - public interface Target { - - /** - * Handles a message delivered to the target. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - } - - /** A sender for messages. */ - public interface Sender { - - /** A listener for message events triggered by the sender. */ - interface Listener { - - /** Called when the message has been delivered. */ - void onMessageDelivered(); - - /** Called when the message has been deleted. */ - void onMessageDeleted(); - } - - /** - * Sends a message. - * - * @param message The message to be sent. - * @param listener The listener to listen to message events. - */ - void sendMessage(PlayerMessage message, Listener listener); - } - - private final Target target; - private final Sender sender; - private final Timeline timeline; - - private int type; - private Object message; - private Handler handler; - private int windowIndex; - private long positionMs; - private boolean deleteAfterDelivery; - private boolean isSent; - private boolean isDelivered; - private boolean isDeleted; - - /** - * Creates a new message. - * - * @param sender The {@link Sender} used to send the message. - * @param target The {@link Target} the message is sent to. - * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If - * set to {@link Timeline#EMPTY}, any position can be specified. - * @param defaultWindowIndex The default window index in the {@code timeline} when no other window - * index is specified. - * @param defaultHandler The default handler to send the message on when no other handler is - * specified. - */ - public PlayerMessage( - Sender sender, - Target target, - Timeline timeline, - int defaultWindowIndex, - Handler defaultHandler) { - this.sender = sender; - this.target = target; - this.timeline = timeline; - this.handler = defaultHandler; - this.windowIndex = defaultWindowIndex; - this.positionMs = C.TIME_UNSET; - this.deleteAfterDelivery = true; - } - - /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ - public Timeline getTimeline() { - return timeline; - } - - /** Returns the target the message is sent to. */ - public Target getTarget() { - return target; - } - - /** - * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. - * - * @param messageType The custom message type. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setType(int messageType) { - Assertions.checkState(!isSent); - this.type = messageType; - return this; - } - - /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ - public int getType() { - return type; - } - - /** - * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. - * - * @param message The custom message. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setMessage(@Nullable Object message) { - Assertions.checkState(!isSent); - this.message = message; - return this; - } - - /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ - public Object getMessage() { - return message; - } - - /** - * Sets the handler the message is delivered on. - * - * @param handler A {@link Handler}. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setHandler(Handler handler) { - Assertions.checkState(!isSent); - this.handler = handler; - return this; - } - - /** Returns the handler the message is delivered on. */ - public Handler getHandler() { - return handler; - } - - /** - * Sets a position in the current window at which the message will be delivered. - * - * @param positionMs The position in the current window at which the message will be sent, in - * milliseconds. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setPosition(long positionMs) { - Assertions.checkState(!isSent); - this.positionMs = positionMs; - return this; - } - - /** - * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, - * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. - */ - public long getPositionMs() { - return positionMs; - } - - /** - * Sets a position in a window at which the message will be delivered. - * - * @param windowIndex The index of the window at which the message will be sent. - * @param positionMs The position in the window with index {@code windowIndex} at which the - * message will be sent, in milliseconds. - * @return This message. - * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not - * empty and the provided window index is not within the bounds of the timeline. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setPosition(int windowIndex, long positionMs) { - Assertions.checkState(!isSent); - Assertions.checkArgument(positionMs != C.TIME_UNSET); - if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { - throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); - } - this.windowIndex = windowIndex; - this.positionMs = positionMs; - return this; - } - - /** Returns window index at which the message will be delivered. */ - public int getWindowIndex() { - return windowIndex; - } - - /** - * Sets whether the message will be deleted after delivery. If false, the message will be resent - * if playback reaches the specified position again. Only allowed to be false if a position is set - * with {@link #setPosition(long)}. - * - * @param deleteAfterDelivery Whether the message is deleted after delivery. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { - Assertions.checkState(!isSent); - this.deleteAfterDelivery = deleteAfterDelivery; - return this; - } - - /** Returns whether the message will be deleted after delivery. */ - public boolean getDeleteAfterDelivery() { - return deleteAfterDelivery; - } - - /** - * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated - * out of the player as an error using {@link - * Player.EventListener#onPlayerError(ExoPlaybackException)}. - * - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage send() { - Assertions.checkState(!isSent); - if (positionMs == C.TIME_UNSET) { - Assertions.checkArgument(deleteAfterDelivery); - } - isSent = true; - sender.sendMessage( - this, - new Sender.Listener() { - @Override - public void onMessageDelivered() { - synchronized (PlayerMessage.this) { - isDelivered = true; - PlayerMessage.this.notifyAll(); - } - } - - @Override - public void onMessageDeleted() { - synchronized (PlayerMessage.this) { - isDeleted = true; - PlayerMessage.this.notifyAll(); - } - } - }); - return this; - } - - /** - * Blocks until after the message has been delivered or the player is no longer able to deliver - * the message. - * - *

      Note that this method can't be called if the current thread is the same thread used by the - * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. - * - * @return Whether the message was delivered successfully. - * @throws IllegalStateException If this method is called before {@link #send()}. - * @throws IllegalStateException If this method is called on the same thread used by the message - * handler set with {@link #setHandler(Handler)}. - * @throws InterruptedException If the current thread is interrupted while waiting for the message - * to be delivered. - */ - public synchronized boolean blockUntilDelivered() throws InterruptedException { - Assertions.checkState(!isSent); - Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); - while (!isDelivered && !isDeleted) { - wait(); - } - return isDelivered; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index d0a07930e0..6def1591da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,20 +15,22 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - * - *

      Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + *

      + * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - * - *

      Renderer state transitions + *

      + * Renderer state transitions + *

      */ -public interface Renderer extends PlayerMessage.Target { +public interface Renderer extends ExoPlayerComponent { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e2d0ed1422..69369d4229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,6 +93,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; + private final int videoRendererCount; + private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -122,6 +124,25 @@ public class SimpleExoPlayer implements ExoPlayer { renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); + // Obtain counts of video and audio renderers. + int videoRendererCount = 0; + int audioRendererCount = 0; + for (Renderer renderer : renderers) { + switch (renderer.getTrackType()) { + case C.TRACK_TYPE_VIDEO: + videoRendererCount++; + break; + case C.TRACK_TYPE_AUDIO: + audioRendererCount++; + break; + default: + // Don't count other track types. + break; + } + } + this.videoRendererCount = videoRendererCount; + this.audioRendererCount = audioRendererCount; + // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -142,15 +163,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; + ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_SCALING_MODE) - .setMessage(videoScalingMode) - .send(); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, + videoScalingMode); } } + player.sendMessages(messages); } /** @@ -331,15 +352,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; + ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_AUDIO_ATTRIBUTES) - .setMessage(audioAttributes) - .send(); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, + audioAttributes); } } + player.sendMessages(messages); } /** @@ -356,11 +377,14 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; + ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); } } + player.sendMessages(messages); } /** @@ -746,11 +770,6 @@ public class SimpleExoPlayer implements ExoPlayer { player.sendMessages(messages); } - @Override - public PlayerMessage createMessage(PlayerMessage.Target target) { - return player.createMessage(target); - } - @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -889,25 +908,22 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - boolean surfaceReplaced = this.surface != null && this.surface != surface; + ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - PlayerMessage message = - player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); - if (surfaceReplaced) { - try { - message.blockUntilDelivered(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); } } - if (surfaceReplaced) { + if (this.surface != null && this.surface != surface) { + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + player.blockingSendMessages(messages); // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } + } else { + player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 54537ba548..c410456e7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,7 +23,8 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -41,7 +42,7 @@ import java.util.Map; * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { +public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -146,11 +147,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player - .createMessage(this) - .setType(MSG_ADD) - .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, + new MessageData<>(index, mediaSource, actionOnCompletion))); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -222,11 +220,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player - .createMessage(this) - .setType(MSG_ADD_MULTIPLE) - .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, + new MessageData<>(index, mediaSources, actionOnCompletion))); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -261,11 +256,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player - .createMessage(this) - .setType(MSG_REMOVE) - .setMessage(new MessageData<>(index, null, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, + new MessageData<>(index, null, actionOnCompletion))); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -301,11 +293,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player - .createMessage(this) - .setType(MSG_MOVE) - .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, + new MessageData<>(currentIndex, newIndex, actionOnCompletion))); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -438,7 +427,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); + player.sendMessages( + new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); } } } 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 a5f5222820..d796e6936f 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 @@ -561,18 +561,6 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } - /** - * Compares two long values and returns the same value as {@code Long.compare(long, long)}. - * - * @param left The left operand. - * @param right The right operand. - * @return 0, if left == right, a negative value if left < right, or a positive value if left - * > right. - */ - public static int compareLong(long left, long right) { - return left < right ? -1 : left == right ? 0 : 1; - } - /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * 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 5ec45af29f..ff0b8a6bc0 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 @@ -18,17 +18,13 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.PlayerMessage; -import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; -import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -349,63 +345,7 @@ public abstract class Action { Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } - } - /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ - public static final class SendMessages extends Action { - - private final Target target; - private final int windowIndex; - private final long positionMs; - private final boolean deleteAfterDelivery; - - /** - * @param tag A tag to use for logging. - * @param target A message target. - * @param positionMs The position at which the message should be sent, in milliseconds. - */ - public SendMessages(String tag, Target target, long positionMs) { - this( - tag, - target, - /* windowIndex= */ C.INDEX_UNSET, - positionMs, - /* deleteAfterDelivery= */ true); - } - - /** - * @param tag A tag to use for logging. - * @param target A message target. - * @param windowIndex The window index at which the message should be sent, or {@link - * C#INDEX_UNSET} for the current window. - * @param positionMs The position at which the message should be sent, in milliseconds. - * @param deleteAfterDelivery Whether the message will be deleted after delivery. - */ - public SendMessages( - String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { - super(tag, "SendMessages"); - this.target = target; - this.windowIndex = windowIndex; - this.positionMs = positionMs; - this.deleteAfterDelivery = deleteAfterDelivery; - } - - @Override - protected void doActionImpl( - final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - if (target instanceof PlayerTarget) { - ((PlayerTarget) target).setPlayer(player); - } - PlayerMessage message = player.createMessage(target); - if (windowIndex != C.INDEX_UNSET) { - message.setPosition(windowIndex, positionMs); - } else { - message.setPosition(positionMs); - } - message.setHandler(new Handler()); - message.setDeleteAfterDelivery(deleteAfterDelivery); - message.send(); - } } /** 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 2ac487c98e..477071f91f 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 @@ -20,11 +20,8 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.PlayerMessage; -import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -32,7 +29,6 @@ import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; -import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -319,44 +315,6 @@ public final class ActionSchedule { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } - /** - * Schedules sending a {@link PlayerMessage}. - * - * @param positionMs The position in the current window at which the message should be sent, in - * milliseconds. - * @return The builder, for convenience. - */ - public Builder sendMessage(Target target, long positionMs) { - return apply(new SendMessages(tag, target, positionMs)); - } - - /** - * Schedules sending a {@link PlayerMessage}. - * - * @param target A message target. - * @param windowIndex The window index at which the message should be sent. - * @param positionMs The position at which the message should be sent, in milliseconds. - * @return The builder, for convenience. - */ - public Builder sendMessage(Target target, int windowIndex, long positionMs) { - return apply( - new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); - } - - /** - * Schedules to send a {@link PlayerMessage}. - * - * @param target A message target. - * @param windowIndex The window index at which the message should be sent. - * @param positionMs The position at which the message should be sent, in milliseconds. - * @param deleteAfterDelivery Whether the message will be deleted after delivery. - * @return The builder, for convenience. - */ - public Builder sendMessage( - Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { - return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); - } - /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -407,28 +365,7 @@ public final class ActionSchedule { currentDelayMs = 0; return this; } - } - /** - * Provides a wrapper for a {@link Target} which has access to the player when handling messages. - * Can be used with {@link Builder#sendMessage(Target, long)}. - */ - public abstract static class PlayerTarget implements Target { - - private SimpleExoPlayer player; - - /** Handles the message send to the component and additionally provides access to the player. */ - public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); - - /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ - /* package */ void setPlayer(SimpleExoPlayer player) { - this.player = player; - } - - @Override - public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { - handleMessage(player, messageType, message); - } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 797c09d6b6..4a9d79f906 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -171,7 +170,7 @@ public final class FakeTimeline extends Timeline { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; + Object uid = setIds ? periodIndex : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -199,13 +198,11 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - Period period = new Period(); - for (int i = 0; i < getPeriodCount(); i++) { - if (getPeriod(i, period, true).uid.equals(uid)) { - return i; - } + if (!(uid instanceof Integer)) { + return C.INDEX_UNSET; } - return C.INDEX_UNSET; + int index = (Integer) uid; + return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 93c14afc8f..4f31a8b027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,9 +24,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -283,8 +281,7 @@ public class MediaSourceTestRunner { } - private static class EventHandlingExoPlayer extends StubExoPlayer - implements Handler.Callback, PlayerMessage.Sender { + private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { private final Handler handler; @@ -293,33 +290,23 @@ public class MediaSourceTestRunner { } @Override - public PlayerMessage createMessage(PlayerMessage.Target target) { - return new PlayerMessage( - /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); + public void sendMessages(ExoPlayerMessage... messages) { + handler.obtainMessage(0, messages).sendToTarget(); } @Override - public void sendMessage(PlayerMessage message, Listener listener) { - handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); - } - - @Override - @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - Pair messageAndListener = (Pair) msg.obj; - try { - messageAndListener - .first - .getTarget() - .handleMessage( - messageAndListener.first.getType(), messageAndListener.first.getMessage()); - messageAndListener.second.onMessageDelivered(); - messageAndListener.second.onMessageDeleted(); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); + ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; + for (ExoPlayerMessage message : messages) { + try { + message.target.handleMessage(message.messageType, message.message); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); + } } return true; } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 7164fa13ab..1ea83bf1ec 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,7 +19,6 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -147,11 +146,6 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public PlayerMessage createMessage(PlayerMessage.Target target) { - throw new UnsupportedOperationException(); - } - @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); From ec71c05e8bef510e91996ed24273dcd9bfda90eb Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 20 Dec 2017 07:26:49 -0800 Subject: [PATCH 212/417] Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also attempts to update these when the source info is refreshed. A sorted list of pending posts is kept and the player triggers these posts when the playback position moves over the specified position. Issue:#2189 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179683841 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 446 ++++++++++++++++++ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 +++---- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 ++++++++--- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 ++++++++++++ .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 ++- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 + .../android/exoplayer2/testutil/Action.java | 64 +++ .../exoplayer2/testutil/ActionSchedule.java | 85 ++++ .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 18 files changed, 1349 insertions(+), 232 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c7f7ed7bbd..3c45c3449a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler. + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 0a902e2efe..0f8df65959 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -119,9 +119,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, - LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - new VpxVideoSurfaceView(context))); + player + .createMessage(videoRenderer) + .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) + .setMessage(new VpxVideoSurfaceView(context)) + .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 40b4b2d383..70ff878e35 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2; +import android.view.Surface; import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -34,8 +38,10 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.video.DummySurface; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -942,4 +948,444 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } + + public void testSendMessagesDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testMultipleSendMessages() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target80, /* positionMs= */ 80) + .sendMessage(target50, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target50.positionMs >= 50); + assertTrue(target80.positionMs >= 80); + assertTrue(target80.positionMs >= target50.positionMs); + } + + public void testMultipleSendMessagesAtSameTime() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* positionMs= */ 50) + .sendMessage(target2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target1.positionMs >= 50); + assertTrue(target2.positionMs >= 50); + } + + public void testSendMessagesMultiPeriodResolution() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); + long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) + .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) + .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline, null), + /* resetPosition= */ false, + /* resetState= */ true) + .waitForPlaybackState(Player.STATE_READY) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, targetStartFirstPeriod.windowIndex); + assertTrue(targetStartFirstPeriod.positionMs >= 0); + assertEquals(0, targetEndMiddlePeriod.windowIndex); + assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); + assertEquals(1, targetStartMiddlePeriod.windowIndex); + assertTrue(targetStartMiddlePeriod.positionMs >= 0); + assertEquals(1, targetEndLastPeriod.windowIndex); + assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); + } + + public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesRepeatDoesNotRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(1, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage( + target, + /* windowIndex= */ 0, + /* positionMs= */ 50, + /* deleteAfterDelivery= */ false) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveCurrentWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(1, target.windowIndex); + } + + public void testSendMessagesMultiWindowDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMultiWindowAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .waitForTimelineChanged(secondTimeline) + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(0, target.windowIndex); + } + + public void testSendMessagesNonLinearPeriodOrder() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) + .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) + .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForPositionDiscontinuity() + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, target1.windowIndex); + assertEquals(1, target2.windowIndex); + assertEquals(2, target3.windowIndex); + } + + public void testSetAndSwitchSurfaceTest() throws Exception { + final List rendererMessages = new ArrayList<>(); + Renderer videoRenderer = + new FakeRenderer(Builder.VIDEO_FORMAT) { + @Override + public void handleMessage(int what, Object object) throws ExoPlaybackException { + super.handleMessage(what, object); + rendererMessages.add(what); + } + }; + final Surface surface1 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false); + final Surface surface2 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setAndSwitchSurfaceTest") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setVideoSurface(surface1); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setVideoSurface(surface2); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setRenderers(videoRenderer) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, Collections.frequency(rendererMessages, C.MSG_SET_SURFACE)); + } + + private static final class PositionGrabbingMessageTarget extends PlayerTarget { + + public int windowIndex; + public long positionMs; + public int messageCount; + + public PositionGrabbingMessageTarget() { + windowIndex = C.INDEX_UNSET; + positionMs = C.POSITION_UNSET; + } + + @Override + public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + messageCount++; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a4103787d1..8ee9a13c55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index cc767752be..4bd28150bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,40 +34,43 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from - * {@link ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link + * ExoPlayerFactory}. * *

      Player components

      + * *

      ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: + * *

        *
      • A {@link MediaSource} that defines the media to be played, loads the media, and from - * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)} - * at the start of playback. The library modules provide default implementations for regular media - * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) - * and HLS (HlsMediaSource), an implementation for loading single media samples - * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and - * implementations for building more complex MediaSources from simpler ones - * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, - * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and - * {@link ClippingMediaSource}).
      • + * which the loaded media can be read. A MediaSource is injected via {@link + * #prepare(MediaSource)} at the start of playback. The library modules provide default + * implementations for regular media files ({@link ExtractorMediaSource}), DASH + * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an + * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's + * most often used for side-loaded subtitle files, and implementations for building more + * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link + * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link + * LoopingMediaSource} and {@link ClippingMediaSource}). *
      • {@link Renderer}s that render individual components of the media. The library - * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, - * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer - * consumes media from the MediaSource being played. Renderers are injected when the player is - * created.
      • + * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, + * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A + * Renderer consumes media from the MediaSource being played. Renderers are injected when the + * player is created. *
      • A {@link TrackSelector} that selects tracks provided by the MediaSource to be - * consumed by each of the available Renderers. The library provides a default implementation - * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when - * the player is created.
      • + * consumed by each of the available Renderers. The library provides a default implementation + * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected + * when the player is created. *
      • A {@link LoadControl} that controls when the MediaSource buffers more media, and how - * much media is buffered. The library provides a default implementation - * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the - * player is created.
      • + * much media is buffered. The library provides a default implementation ({@link + * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player + * is created. *
      + * *

      An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -81,30 +84,32 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * it's possible to load data from a non-standard source, or through a different network stack. * *

      Threading model

      - *

      The figure below shows ExoPlayer's threading model.

      - *

      - * ExoPlayer's threading model - *

      + * + *

      The figure below shows ExoPlayer's threading model. + * + *

      ExoPlayer's threading
+ * model * *

        - *
      • It is recommended that ExoPlayer instances are created and accessed from a single application - * thread. The application's main thread is ideal. Accessing an instance from multiple threads is - * discouraged, however if an application does wish to do this then it may do so provided that it - * ensures accesses are synchronized.
      • - *
      • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, - * registered listeners will be called on the application's main thread.
      • - *
      • An internal playback thread is responsible for playback. Injected player components such as - * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this - * thread.
      • - *
      • When the application performs an operation on the player, for example a seek, a message is - * delivered to the internal playback thread via a message queue. The internal playback thread - * consumes messages from the queue and performs the corresponding operations. Similarly, when a - * playback event occurs on the internal playback thread, a message is delivered to the application - * thread via a second message queue. The application thread consumes messages from the queue, - * updating the application visible state and calling corresponding listener methods.
      • - *
      • Injected player components may use additional background threads. For example a MediaSource - * may use background threads to load data. These are implementation specific.
      • + *
      • It is recommended that ExoPlayer instances are created and accessed from a single + * application thread. The application's main thread is ideal. Accessing an instance from + * multiple threads is discouraged, however if an application does wish to do this then it may + * do so provided that it ensures accesses are synchronized. + *
      • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that + * case, registered listeners will be called on the application's main thread. + *
      • An internal playback thread is responsible for playback. Injected player components such as + * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this + * thread. + *
      • When the application performs an operation on the player, for example a seek, a message is + * delivered to the internal playback thread via a message queue. The internal playback thread + * consumes messages from the queue and performs the corresponding operations. Similarly, when + * a playback event occurs on the internal playback thread, a message is delivered to the + * application thread via a second message queue. The application thread consumes messages + * from the queue, updating the application visible state and calling corresponding listener + * methods. + *
      • Injected player components may use additional background threads. For example a MediaSource + * may use background threads to load data. These are implementation specific. *
      */ public interface ExoPlayer extends Player { @@ -115,54 +120,28 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** - * A component of an {@link ExoPlayer} that can receive messages on the playback thread. - *

      - * Messages can be delivered to a component via {@link #sendMessages} and - * {@link #blockingSendMessages}. - */ - interface ExoPlayerComponent { + /** @deprecated Use {@link PlayerMessage.Target} instead. */ + @Deprecated + interface ExoPlayerComponent extends PlayerMessage.Target {} - /** - * Handles a message delivered to the component. Called on the playback thread. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - - } - - /** - * Defines a message and a target {@link ExoPlayerComponent} to receive it. - */ + /** @deprecated Use {@link PlayerMessage} instead. */ + @Deprecated final class ExoPlayerMessage { - /** - * The target to receive the message. - */ - public final ExoPlayerComponent target; - /** - * The type of the message. - */ + /** The target to receive the message. */ + public final PlayerMessage.Target target; + /** The type of the message. */ public final int messageType; - /** - * The message. - */ + /** The message. */ public final Object message; - /** - * @param target The target of the message. - * @param messageType The message type. - * @param message The message. - */ - public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { + /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ + @Deprecated + public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } - } /** @@ -236,20 +215,25 @@ public interface ExoPlayer extends Player { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sends messages to their target components. The messages are delivered on the playback thread. - * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player - * as an error. - * - * @param messages The messages to be sent. + * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message + * will be delivered immediately without blocking on the playback thread. The default {@link + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be + * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. + * Alternatively, the message can be sent at a specific window using {@link + * PlayerMessage#setPosition(int, long)}. */ + PlayerMessage createMessage(PlayerMessage.Target target); + + /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ + @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have - * been delivered. - * - * @param messages The messages to be sent. + * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link + * PlayerMessage#blockUntilDelivered()}. */ + @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2869a7668e..afb6428fa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -31,6 +32,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -45,6 +48,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; + private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -113,6 +117,7 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -326,12 +331,47 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { - internalPlayer.sendMessages(messages); + for (ExoPlayerMessage message : messages) { + createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + } + } + + @Override + public PlayerMessage createMessage(Target target) { + return new PlayerMessage( + internalPlayer, + target, + playbackInfo.timeline, + getCurrentWindowIndex(), + internalPlayerHandler); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - internalPlayer.blockingSendMessages(messages); + List playerMessages = new ArrayList<>(); + for (ExoPlayerMessage message : messages) { + playerMessages.add( + createMessage(message.target) + .setType(message.messageType) + .setMessage(message.message) + .send()); + } + boolean wasInterrupted = false; + for (PlayerMessage message : playerMessages) { + boolean blockMessage = true; + while (blockMessage) { + try { + message.blockUntilDelivered(); + blockMessage = false; + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } @Override 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 09b3231467..f3d0e1794b 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 @@ -22,10 +22,10 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,14 +40,19 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; -/** - * Implements the internal behavior of {@link ExoPlayerImpl}. - */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, - PlaybackParameterListener { +/** Implements the internal behavior of {@link ExoPlayerImpl}. */ +/* package */ final class ExoPlayerImplInternal + implements Handler.Callback, + MediaPeriod.Callback, + TrackSelector.InvalidationListener, + MediaSource.Listener, + PlaybackParameterListener, + PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; @@ -108,6 +113,7 @@ import java.io.IOException; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; + private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,13 +126,12 @@ import java.io.IOException; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int customMessagesSent; - private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; + private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -166,6 +171,7 @@ import java.io.IOException; rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); + customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -214,34 +220,15 @@ import java.io.IOException; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void sendMessages(ExoPlayerMessage... messages) { + @Override + public synchronized void sendMessage( + PlayerMessage message, PlayerMessage.Sender.Listener listener) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); + listener.onMessageDeleted(); return; } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - } - - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - return; - } - int messageNumber = customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - boolean wasInterrupted = false; - while (customMessagesProcessed <= messageNumber) { - try { - wait(); - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); } public synchronized void release() { @@ -349,7 +336,7 @@ import java.io.IOException; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + sendMessageInternal((CustomMessageInfo) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -537,8 +524,9 @@ import java.io.IOException; } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); + playbackInfo.positionUs = periodPositionUs; } - playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -656,7 +644,8 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = resolveSeekPosition(seekPosition); + Pair periodPosition = + resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -850,6 +839,11 @@ import java.io.IOException; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); + for (CustomMessageInfo customMessageInfo : customMessageInfos) { + customMessageInfo.listener.onMessageDeleted(); + } + customMessageInfos.clear(); + nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -870,21 +864,153 @@ import java.io.IOException; } } - private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { - try { - for (ExoPlayerMessage message : messages) { - message.target.handleMessage(message.messageType, message.message); + private void sendMessageInternal(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + // If no delivery time is specified, trigger immediate message delivery. + sendCustomMessagesToTarget(customMessageInfo); + } else if (playbackInfo.timeline == null) { + // Still waiting for initial timeline to resolve position. + customMessageInfos.add(customMessageInfo); + } else { + if (resolveCustomMessagePosition(customMessageInfo)) { + customMessageInfos.add(customMessageInfo); + // Ensure new message is inserted according to playback order. + Collections.sort(customMessageInfos); + } else { + customMessageInfo.listener.onMessageDeleted(); } - if (playbackInfo.playbackState == Player.STATE_READY - || playbackInfo.playbackState == Player.STATE_BUFFERING) { - // The message may have caused something to change that now requires us to do work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + + private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { + final Runnable handleMessageRunnable = + new Runnable() { + @Override + public void run() { + try { + customMessageInfo + .message + .getTarget() + .handleMessage( + customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + customMessageInfo.listener.onMessageDelivered(); + if (customMessageInfo.message.getDeleteAfterDelivery()) { + customMessageInfo.listener.onMessageDeleted(); + } + // The message may have caused something to change that now requires us to do + // work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + }; + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } + + private void resolveCustomMessagePositions() { + for (int i = customMessageInfos.size() - 1; i >= 0; i--) { + if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { + // Remove messages if new position can't be resolved. + customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.remove(i); } - } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + } + // Re-sort messages by playback order. + Collections.sort(customMessageInfos); + } + + private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in current timeline. + Pair periodPosition = + resolveSeekPosition( + new SeekPosition( + customMessageInfo.message.getTimeline(), + customMessageInfo.message.getWindowIndex(), + C.msToUs(customMessageInfo.message.getPositionMs())), + /* trySubsequentPeriods= */ false); + if (periodPosition == null) { + return false; } + customMessageInfo.setResolvedPosition( + periodPosition.first, + periodPosition.second, + playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); + } else { + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + customMessageInfo.resolvedPeriodIndex = index; + } + return true; + } + + private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { + if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { + return; + } + // If this is the first call from the start position, include oldPeriodPositionUs in potential + // trigger positions. + if (playbackInfo.startPositionUs == oldPeriodPositionUs) { + oldPeriodPositionUs--; + } + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) + int currentPeriodIndex = playbackInfo.periodId.periodIndex; + CustomMessageInfo prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + while (prevInfo != null + && (prevInfo.resolvedPeriodIndex > currentPeriodIndex + || (prevInfo.resolvedPeriodIndex == currentPeriodIndex + && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { + nextCustomMessageInfoIndex--; + prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + } + CustomMessageInfo nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && (nextInfo.resolvedPeriodIndex < currentPeriodIndex + || (nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { + nextCustomMessageInfoIndex++; + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + } + // Check if any message falls within the covered time span. + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs + && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { + sendCustomMessagesToTarget(nextInfo); + if (nextInfo.message.getDeleteAfterDelivery()) { + customMessageInfos.remove(nextCustomMessageInfoIndex); + } else { + nextCustomMessageInfoIndex++; + } + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; } } @@ -1034,12 +1160,14 @@ import java.io.IOException; Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + Pair periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1224,11 +1352,14 @@ import java.io.IOException; * internal timeline. * * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition(SeekPosition seekPosition) { + private Pair resolveSeekPosition( + SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1257,12 +1388,14 @@ import java.io.IOException; // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, - C.TIME_UNSET); + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition( + timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + } } // We didn't find one. Give up. return null; @@ -1802,7 +1935,45 @@ import java.io.IOException; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } + } + private static final class CustomMessageInfo implements Comparable { + + public final PlayerMessage message; + public final PlayerMessage.Sender.Listener listener; + + public int resolvedPeriodIndex; + public long resolvedPeriodTimeUs; + public @Nullable Object resolvedPeriodUid; + + public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + this.message = message; + this.listener = listener; + } + + public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { + resolvedPeriodIndex = periodIndex; + resolvedPeriodTimeUs = periodTimeUs; + resolvedPeriodUid = periodUid; + } + + @Override + public int compareTo(@NonNull CustomMessageInfo other) { + if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { + // CustomMessageInfos with a resolved period position are always smaller. + return resolvedPeriodUid != null ? -1 : 1; + } + if (resolvedPeriodUid == null) { + // Don't sort message with unresolved positions. + return 0; + } + // Sort resolved media times by period index and then by period position. + int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; + if (comparePeriodIndex != 0) { + return comparePeriodIndex; + } + return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); + } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 978f4f7a97..593d3d1fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java new file mode 100644 index 0000000000..420eb60a48 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2017 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; + +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; + +/** + * Defines a player message which can be sent with a {@link Sender} and received by a {@link + * Target}. + */ +public final class PlayerMessage { + + /** A target for messages. */ + public interface Target { + + /** + * Handles a message delivered to the target. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + } + + /** A sender for messages. */ + public interface Sender { + + /** A listener for message events triggered by the sender. */ + interface Listener { + + /** Called when the message has been delivered. */ + void onMessageDelivered(); + + /** Called when the message has been deleted. */ + void onMessageDeleted(); + } + + /** + * Sends a message. + * + * @param message The message to be sent. + * @param listener The listener to listen to message events. + */ + void sendMessage(PlayerMessage message, Listener listener); + } + + private final Target target; + private final Sender sender; + private final Timeline timeline; + + private int type; + private Object message; + private Handler handler; + private int windowIndex; + private long positionMs; + private boolean deleteAfterDelivery; + private boolean isSent; + private boolean isDelivered; + private boolean isDeleted; + + /** + * Creates a new message. + * + * @param sender The {@link Sender} used to send the message. + * @param target The {@link Target} the message is sent to. + * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If + * set to {@link Timeline#EMPTY}, any position can be specified. + * @param defaultWindowIndex The default window index in the {@code timeline} when no other window + * index is specified. + * @param defaultHandler The default handler to send the message on when no other handler is + * specified. + */ + public PlayerMessage( + Sender sender, + Target target, + Timeline timeline, + int defaultWindowIndex, + Handler defaultHandler) { + this.sender = sender; + this.target = target; + this.timeline = timeline; + this.handler = defaultHandler; + this.windowIndex = defaultWindowIndex; + this.positionMs = C.TIME_UNSET; + this.deleteAfterDelivery = true; + } + + /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ + public Timeline getTimeline() { + return timeline; + } + + /** Returns the target the message is sent to. */ + public Target getTarget() { + return target; + } + + /** + * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param messageType The custom message type. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setType(int messageType) { + Assertions.checkState(!isSent); + this.type = messageType; + return this; + } + + /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + public int getType() { + return type; + } + + /** + * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param message The custom message. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setMessage(@Nullable Object message) { + Assertions.checkState(!isSent); + this.message = message; + return this; + } + + /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ + public Object getMessage() { + return message; + } + + /** + * Sets the handler the message is delivered on. + * + * @param handler A {@link Handler}. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setHandler(Handler handler) { + Assertions.checkState(!isSent); + this.handler = handler; + return this; + } + + /** Returns the handler the message is delivered on. */ + public Handler getHandler() { + return handler; + } + + /** + * Sets a position in the current window at which the message will be delivered. + * + * @param positionMs The position in the current window at which the message will be sent, in + * milliseconds. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(long positionMs) { + Assertions.checkState(!isSent); + this.positionMs = positionMs; + return this; + } + + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + + /** + * Sets a position in a window at which the message will be delivered. + * + * @param windowIndex The index of the window at which the message will be sent. + * @param positionMs The position in the window with index {@code windowIndex} at which the + * message will be sent, in milliseconds. + * @return This message. + * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not + * empty and the provided window index is not within the bounds of the timeline. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(int windowIndex, long positionMs) { + Assertions.checkState(!isSent); + Assertions.checkArgument(positionMs != C.TIME_UNSET); + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); + } + this.windowIndex = windowIndex; + this.positionMs = positionMs; + return this; + } + + /** Returns window index at which the message will be delivered. */ + public int getWindowIndex() { + return windowIndex; + } + + /** + * Sets whether the message will be deleted after delivery. If false, the message will be resent + * if playback reaches the specified position again. Only allowed to be false if a position is set + * with {@link #setPosition(long)}. + * + * @param deleteAfterDelivery Whether the message is deleted after delivery. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { + Assertions.checkState(!isSent); + this.deleteAfterDelivery = deleteAfterDelivery; + return this; + } + + /** Returns whether the message will be deleted after delivery. */ + public boolean getDeleteAfterDelivery() { + return deleteAfterDelivery; + } + + /** + * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated + * out of the player as an error using {@link + * Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage send() { + Assertions.checkState(!isSent); + if (positionMs == C.TIME_UNSET) { + Assertions.checkArgument(deleteAfterDelivery); + } + isSent = true; + sender.sendMessage( + this, + new Sender.Listener() { + @Override + public void onMessageDelivered() { + synchronized (PlayerMessage.this) { + isDelivered = true; + PlayerMessage.this.notifyAll(); + } + } + + @Override + public void onMessageDeleted() { + synchronized (PlayerMessage.this) { + isDeleted = true; + PlayerMessage.this.notifyAll(); + } + } + }); + return this; + } + + /** + * Blocks until after the message has been delivered or the player is no longer able to deliver + * the message. + * + *

      Note that this method can't be called if the current thread is the same thread used by the + * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + * + * @return Whether the message was delivered successfully. + * @throws IllegalStateException If this method is called before {@link #send()}. + * @throws IllegalStateException If this method is called on the same thread used by the message + * handler set with {@link #setHandler(Handler)}. + * @throws InterruptedException If the current thread is interrupted while waiting for the message + * to be delivered. + */ + public synchronized boolean blockUntilDelivered() throws InterruptedException { + Assertions.checkState(isSent); + Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + while (!isDelivered && !isDeleted) { + wait(); + } + return isDelivered; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 6def1591da..d0a07930e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,22 +15,20 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - *

      - * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + * + *

      Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - *

      - * Renderer state transitions - *

      + * + *

      Renderer state transitions */ -public interface Renderer extends ExoPlayerComponent { +public interface Renderer extends PlayerMessage.Target { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 69369d4229..e2d0ed1422 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,8 +93,6 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; - private final int videoRendererCount; - private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -124,25 +122,6 @@ public class SimpleExoPlayer implements ExoPlayer { renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); - // Obtain counts of video and audio renderers. - int videoRendererCount = 0; - int audioRendererCount = 0; - for (Renderer renderer : renderers) { - switch (renderer.getTrackType()) { - case C.TRACK_TYPE_VIDEO: - videoRendererCount++; - break; - case C.TRACK_TYPE_AUDIO: - audioRendererCount++; - break; - default: - // Don't count other track types. - break; - } - } - this.videoRendererCount = videoRendererCount; - this.audioRendererCount = audioRendererCount; - // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -163,15 +142,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, - videoScalingMode); + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setMessage(videoScalingMode) + .send(); } } - player.sendMessages(messages); } /** @@ -352,15 +331,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, - audioAttributes); + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setMessage(audioAttributes) + .send(); } } - player.sendMessages(messages); } /** @@ -377,14 +356,11 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; - ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); } } - player.sendMessages(messages); } /** @@ -770,6 +746,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.sendMessages(messages); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + return player.createMessage(target); + } + @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -908,22 +889,25 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; + boolean surfaceReplaced = this.surface != null && this.surface != surface; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); + PlayerMessage message = + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); + if (surfaceReplaced) { + try { + message.blockUntilDelivered(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } } - if (this.surface != null && this.surface != surface) { - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); + if (surfaceReplaced) { // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - } else { - player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c410456e7b..54537ba548 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,8 +23,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -42,7 +41,7 @@ import java.util.Map; * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { +public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -147,8 +146,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, - new MessageData<>(index, mediaSource, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD) + .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -220,8 +222,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - new MessageData<>(index, mediaSources, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -256,8 +261,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, - new MessageData<>(index, null, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_REMOVE) + .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -293,8 +301,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_MOVE) + .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -427,8 +438,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.sendMessages( - new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); } } } 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 d796e6936f..a5f5222820 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 @@ -561,6 +561,18 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Compares two long values and returns the same value as {@code Long.compare(long, long)}. + * + * @param left The left operand. + * @param right The right operand. + * @return 0, if left == right, a negative value if left < right, or a positive value if left + * > right. + */ + public static int compareLong(long left, long right) { + return left < right ? -1 : left == right ? 0 : 1; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * 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 ff0b8a6bc0..8145aa0c56 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 @@ -18,13 +18,18 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -345,7 +350,63 @@ public abstract class Action { Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } + } + /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ + public static final class SendMessages extends Action { + + private final Target target; + private final int windowIndex; + private final long positionMs; + private final boolean deleteAfterDelivery; + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param positionMs The position at which the message should be sent, in milliseconds. + */ + public SendMessages(String tag, Target target, long positionMs) { + this( + tag, + target, + /* windowIndex= */ C.INDEX_UNSET, + positionMs, + /* deleteAfterDelivery= */ true); + } + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param windowIndex The window index at which the message should be sent, or {@link + * C#INDEX_UNSET} for the current window. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + */ + public SendMessages( + String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + super(tag, "SendMessages"); + this.target = target; + this.windowIndex = windowIndex; + this.positionMs = positionMs; + this.deleteAfterDelivery = deleteAfterDelivery; + } + + @Override + protected void doActionImpl( + final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (target instanceof PlayerTarget) { + ((PlayerTarget) target).setPlayer(player); + } + PlayerMessage message = player.createMessage(target); + if (windowIndex != C.INDEX_UNSET) { + message.setPosition(windowIndex, positionMs); + } else { + message.setPosition(positionMs); + } + message.setHandler(new Handler()); + message.setDeleteAfterDelivery(deleteAfterDelivery); + message.send(); + } } /** @@ -555,6 +616,9 @@ public abstract class Action { @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (runnable instanceof PlayerRunnable) { + ((PlayerRunnable) runnable).setPlayer(player); + } runnable.run(); } 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 477071f91f..33ce846751 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 @@ -20,8 +20,11 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -29,6 +32,7 @@ import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -315,6 +319,44 @@ public final class ActionSchedule { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param positionMs The position in the current window at which the message should be sent, in + * milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, long positionMs) { + return apply(new SendMessages(tag, target, positionMs)); + } + + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, int windowIndex, long positionMs) { + return apply( + new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); + } + + /** + * Schedules to send a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + * @return The builder, for convenience. + */ + public Builder sendMessage( + Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -365,7 +407,50 @@ public final class ActionSchedule { currentDelayMs = 0; return this; } + } + /** + * Provides a wrapper for a {@link Target} which has access to the player when handling messages. + * Can be used with {@link Builder#sendMessage(Target, long)}. + */ + public abstract static class PlayerTarget implements Target { + + private SimpleExoPlayer player; + + /** Handles the message send to the component and additionally provides access to the player. */ + public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); + + /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + handleMessage(player, messageType, message); + } + } + + /** + * Provides a wrapper for a {@link Runnable} which has access to the player. Can be used with + * {@link Builder#executeRunnable(Runnable)}. + */ + public abstract static class PlayerRunnable implements Runnable { + + private SimpleExoPlayer player; + + /** Executes Runnable with reference to player. */ + public abstract void run(SimpleExoPlayer player); + + /** Sets the player to be passed to {@link #run(SimpleExoPlayer)} . */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void run() { + run(player); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 4a9d79f906..797c09d6b6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -170,7 +171,7 @@ public final class FakeTimeline extends Timeline { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? periodIndex : null; + Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -198,11 +199,13 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; + Period period = new Period(); + for (int i = 0; i < getPeriodCount(); i++) { + if (getPeriod(i, period, true).uid.equals(uid)) { + return i; + } } - int index = (Integer) uid; - return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; + return C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 4f31a8b027..93c14afc8f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -281,7 +283,8 @@ public class MediaSourceTestRunner { } - private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static class EventHandlingExoPlayer extends StubExoPlayer + implements Handler.Callback, PlayerMessage.Sender { private final Handler handler; @@ -290,23 +293,33 @@ public class MediaSourceTestRunner { } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); + public PlayerMessage createMessage(PlayerMessage.Target target) { + return new PlayerMessage( + /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); } @Override + public void sendMessage(PlayerMessage message, Listener listener) { + handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + } + + @Override + @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + Pair messageAndListener = (Pair) msg.obj; + try { + messageAndListener + .first + .getTarget() + .handleMessage( + messageAndListener.first.getType(), messageAndListener.first.getMessage()); + messageAndListener.second.onMessageDelivered(); + messageAndListener.second.onMessageDeleted(); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); } return true; } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 1ea83bf1ec..7164fa13ab 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -146,6 +147,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + throw new UnsupportedOperationException(); + } + @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); From 7fb296dab98c007718227fc50ad47d689e3c40d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 03:25:13 -0800 Subject: [PATCH 213/417] Initialize sample streams in FakeAdaptiveMediaPeriod. This prevents NPE when release or stop is called before tracks have been selected. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179911907 --- .../testutil/FakeAdaptiveMediaPeriod.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index ff2a9b23cd..1a3e69f029 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -44,13 +44,18 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private ChunkSampleStream[] sampleStreams; private SequenceableLoader sequenceableLoader; - public FakeAdaptiveMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, - Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, long durationUs) { + public FakeAdaptiveMediaPeriod( + TrackGroupArray trackGroupArray, + EventDispatcher eventDispatcher, + Allocator allocator, + FakeChunkSource.Factory chunkSourceFactory, + long durationUs) { super(trackGroupArray); this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.chunkSourceFactory = chunkSourceFactory; this.durationUs = durationUs; + this.sampleStreams = newSampleStreamArray(0); } @Override @@ -62,13 +67,12 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod } @Override - public void prepare(Callback callback, long positionUs) { + public synchronized void prepare(Callback callback, long positionUs) { super.prepare(callback, positionUs); this.callback = callback; } @Override - @SuppressWarnings("unchecked") public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams, @@ -79,7 +83,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod validStreams.add((ChunkSampleStream) stream); } } - this.sampleStreams = validStreams.toArray(new ChunkSampleStream[validStreams.size()]); + this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size())); this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return returnPositionUs; } @@ -131,4 +135,8 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod callback.onContinueLoadingRequested(this); } + @SuppressWarnings("unchecked") + private static ChunkSampleStream[] newSampleStreamArray(int length) { + return new ChunkSampleStream[length]; + } } From 6f4110f3f873c79e9369537e23f1b3479d0d5700 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Dec 2017 05:28:52 -0800 Subject: [PATCH 214/417] Fix buffer re-evaluation edge cases ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179917833 --- .../source/chunk/ChunkSampleStream.java | 58 ++++++++----------- 1 file changed, 25 insertions(+), 33 deletions(-) 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 85c4b12241..a96bc2dcd0 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 @@ -408,8 +408,31 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading() || isPendingReset()) { return; } - int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - discardUpstreamMediaChunks(queueSize); + + int currentQueueSize = mediaChunks.size(); + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (currentQueueSize <= preferredQueueSize) { + return; + } + + int newQueueSize = currentQueueSize; + for (int i = preferredQueueSize; i < currentQueueSize; i++) { + if (!haveReadFromMediaChunk(i)) { + newQueueSize = i; + break; + } + } + if (newQueueSize == currentQueueSize) { + return; + } + + long endTimeUs = getLastMediaChunk().endTimeUs; + BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } + loadingFinished = false; + eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); } // Internal methods @@ -483,37 +506,6 @@ public class ChunkSampleStream implements SampleStream, S return mediaChunks.get(mediaChunks.size() - 1); } - /** - * Discard upstream media chunks until the queue length is equal to the length specified, but - * avoid discarding any chunk whose samples have been read by either primary sample stream or - * embedded sample streams. - * - * @param desiredQueueSize The desired length of the queue. The final queue size after discarding - * maybe larger than this if there are chunks after the specified position that have been read - * by either primary sample stream or embedded sample streams. - */ - private void discardUpstreamMediaChunks(int desiredQueueSize) { - if (mediaChunks.size() <= desiredQueueSize) { - return; - } - - int firstIndexToRemove = desiredQueueSize; - for (int i = firstIndexToRemove; i < mediaChunks.size(); i++) { - if (!haveReadFromMediaChunk(i)) { - firstIndexToRemove = i; - break; - } - } - - if (firstIndexToRemove == mediaChunks.size()) { - return; - } - long endTimeUs = getLastMediaChunk().endTimeUs; - BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove); - loadingFinished = false; - eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); - } - /** * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample * queues. From 35d4cbf99f6dae832a5a8e0ff3f46e03474376e2 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 22 Dec 2017 05:33:11 -0800 Subject: [PATCH 215/417] Fix a bug that makes ClippingMediaSource not stop in some occasions. If ClippingMediaSource contains a child MediaSource with embedded metadata stream, and the embedded stream is being used, it can lead to ClippingMediaSource not be able to stop after the clipping end point. The reason being the metadata stream cannot read anymore sample, but it's also not end of source at that point. This CL fix this by changing the condition to check if the child stream cannot read anymore sample and it has read past the clipping end point. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179918038 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 5685b8b70b..3f2c5ec894 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -299,9 +299,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); return C.RESULT_FORMAT_READ; } - if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ - && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + if (endUs != C.TIME_END_OF_SOURCE + && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) + || (result == C.RESULT_NOTHING_READ + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; From c6529344db8128314ff77980bf66275fa743cb27 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 05:37:39 -0800 Subject: [PATCH 216/417] Introduce Handler interface allowing to switch to other Handlers. Especially this removes the need for the Clock interface to directly implement Handler methods. Instead, we have a separate Handler interface and the FakeClock is able to construct such a Handler. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179918255 --- .../google/android/exoplayer2/util/Clock.java | 20 +-- .../exoplayer2/util/HandlerWrapper.java | 76 +++++++++ .../android/exoplayer2/util/SystemClock.java | 9 +- .../exoplayer2/util/SystemHandler.java | 93 +++++++++++ .../android/exoplayer2/testutil/Action.java | 56 ++++--- .../exoplayer2/testutil/ActionSchedule.java | 70 ++++---- .../exoplayer2/testutil/ExoHostedTest.java | 8 +- .../testutil/ExoPlayerTestRunner.java | 46 ++++-- .../exoplayer2/testutil/FakeClock.java | 149 +++++++++++++++--- 9 files changed, 414 insertions(+), 113 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java 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 9619ed53ea..7731cca68c 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 @@ -15,13 +15,12 @@ */ package com.google.android.exoplayer2.util; -import android.os.Handler; - /** - * An interface through which system clocks can be read. The {@link #DEFAULT} implementation - * must be used for all non-test cases. + * An interface through which system clocks can be read. The {@link #DEFAULT} implementation must be + * used for all non-test cases. Implementations must also be able to create a {@link HandlerWrapper} + * which uses the underlying clock to schedule delayed messages. */ -public interface Clock { +public interface Clock extends HandlerWrapper.Factory { /** * Default {@link Clock} to use for all non-test cases. @@ -37,15 +36,4 @@ public interface Clock { * @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); - - /** - * Post a {@link Runnable} on a {@link Handler} thread with a delay measured by this clock. - * @see Handler#postDelayed(Runnable, long) - * - * @param handler The {@link Handler} to post the {@code runnable} on. - * @param runnable A {@link Runnable} to be posted. - * @param delayMs The delay in milliseconds as measured by this clock. - */ - void postDelayed(Handler handler, Runnable runnable, long delayMs); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java new file mode 100644 index 0000000000..25f9c9bb38 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 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 android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.Nullable; + +/** + * An interface to call through to an {@link Handler}. The {@link Factory#DEFAULT} factory must be + * used for all non-test cases. + */ +public interface HandlerWrapper { + + /** A factory for handler instances. */ + interface Factory { + + /** Default HandlerWrapper factory to use for all non-test cases. */ + Factory DEFAULT = new SystemHandler.Factory(); + + /** + * Creates a HandlerWrapper running a specified looper and using a specified callback for + * messages. + * + * @see Handler#Handler(Looper, Handler.Callback). + */ + HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); + } + + /** @see Handler#getLooper(). */ + Looper getLooper(); + + /** @see Handler#obtainMessage(int). */ + Message obtainMessage(int what); + + /** @see Handler#obtainMessage(int, Object). */ + Message obtainMessage(int what, Object obj); + + /** @see Handler#obtainMessage(int, int, int). */ + Message obtainMessage(int what, int arg1, int arg2); + + /** @see Handler#obtainMessage(int, int, int, Object). */ + Message obtainMessage(int what, int arg1, int arg2, Object obj); + + /** @see Handler#sendEmptyMessage(int). */ + boolean sendEmptyMessage(int what); + + /** @see Handler#sendEmptyMessageDelayed(int, long). */ + boolean sendEmptyMessageDelayed(int what, long delayMs); + + /** @see Handler#removeMessages(int). */ + void removeMessages(int what); + + /** @see Handler#removeCallbacksAndMessages(Object). */ + void removeCallbacksAndMessages(Object token); + + /** @see Handler#post(Runnable). */ + boolean post(Runnable runnable); + + /** @see Handler#postDelayed(Runnable, long). */ + boolean postDelayed(Runnable runnable, long delayMs); +} 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 272c3f43ec..8a5bdf549f 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 @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.util; -import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.support.annotation.Nullable; /** * The standard implementation of {@link Clock}. @@ -33,8 +35,7 @@ import android.os.Handler; } @Override - public void postDelayed(Handler handler, Runnable runnable, long delayMs) { - handler.postDelayed(runnable, delayMs); + public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { + return HandlerWrapper.Factory.DEFAULT.createHandler(looper, callback); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java new file mode 100644 index 0000000000..b9fe771053 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017 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 android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; + +/** The standard implementation of {@link HandlerWrapper}. */ +/* package */ final class SystemHandler implements HandlerWrapper { + + /* package */ static final class Factory implements HandlerWrapper.Factory { + + @Override + public HandlerWrapper createHandler(Looper looper, Callback callback) { + return new SystemHandler(new android.os.Handler(looper, callback)); + } + } + + private final android.os.Handler handler; + + private SystemHandler(android.os.Handler handler) { + this.handler = handler; + } + + @Override + public Looper getLooper() { + return handler.getLooper(); + } + + @Override + public Message obtainMessage(int what) { + return handler.obtainMessage(what); + } + + @Override + public Message obtainMessage(int what, Object obj) { + return handler.obtainMessage(what, obj); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2) { + return handler.obtainMessage(what, arg1, arg2); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2, Object obj) { + return handler.obtainMessage(what, arg1, arg2, obj); + } + + @Override + public boolean sendEmptyMessage(int what) { + return handler.sendEmptyMessage(what); + } + + @Override + public boolean sendEmptyMessageDelayed(int what, long delayMs) { + return handler.sendEmptyMessageDelayed(what, delayMs); + } + + @Override + public void removeMessages(int what) { + handler.removeMessages(what); + } + + @Override + public void removeCallbacksAndMessages(Object token) { + handler.removeCallbacksAndMessages(token); + } + + @Override + public boolean post(Runnable runnable) { + return handler.post(runnable); + } + + @Override + public boolean postDelayed(Runnable runnable, long delayMs) { + return handler.postDelayed(runnable, delayMs); + } +} 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 8145aa0c56..7d2a1fd03f 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -31,6 +30,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.HandlerWrapper; /** * Base class for actions to perform during playback tests. @@ -58,15 +58,19 @@ public abstract class Action { * @param handler The handler to use to pass to the next action. * @param nextAction The next action to schedule immediately after this action finished. */ - public final void doActionAndScheduleNext(SimpleExoPlayer player, - MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { + public final void doActionAndScheduleNext( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper handler, + ActionNode nextAction) { Log.i(tag, description); doActionAndScheduleNextImpl(player, trackSelector, surface, handler, nextAction); } /** * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, MappingTrackSelector, Surface, - * Handler, ActionNode)} to perform the action and to schedule the next action node. + * HandlerWrapper, ActionNode)} to perform the action and to schedule the next action node. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. @@ -74,8 +78,12 @@ public abstract class Action { * @param handler The handler to use to pass to the next action. * @param nextAction The next action to schedule immediately after this action finished. */ - protected void doActionAndScheduleNextImpl(SimpleExoPlayer player, - MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { + protected void doActionAndScheduleNextImpl( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper handler, + ActionNode nextAction) { doActionImpl(player, trackSelector, surface); if (nextAction != null) { nextAction.schedule(player, trackSelector, surface, handler); @@ -84,14 +92,14 @@ public abstract class Action { /** * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, MappingTrackSelector, Surface, - * Handler, ActionNode)} to perform the action. + * HandlerWrapper, ActionNode)} to perform the action. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. * @param surface The surface to use when applying actions. */ - protected abstract void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface); + protected abstract void doActionImpl( + SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface); /** * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. @@ -403,7 +411,7 @@ public abstract class Action { } else { message.setPosition(positionMs); } - message.setHandler(new Handler()); + message.setHandler(new android.os.Handler()); message.setDeleteAfterDelivery(deleteAfterDelivery); message.send(); } @@ -449,8 +457,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; @@ -493,8 +504,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; @@ -533,8 +547,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; @@ -575,8 +592,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; 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 33ce846751..f152bb4eb8 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; @@ -46,7 +45,7 @@ import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; /** * Schedules a sequence of {@link Action}s for execution during a test. @@ -87,8 +86,12 @@ public final class ActionSchedule { * @param callback A {@link Callback} to notify when the action schedule finishes, or null if no * notification is needed. */ - /* package */ void start(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface, Handler mainHandler, @Nullable Callback callback) { + /* package */ void start( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper mainHandler, + @Nullable Callback callback) { callbackAction.setCallback(callback); rootNode.schedule(player, trackSelector, surface, mainHandler); } @@ -99,7 +102,6 @@ public final class ActionSchedule { public static final class Builder { private final String tag; - private final Clock clock; private final ActionNode rootNode; private long currentDelayMs; @@ -109,17 +111,8 @@ public final class ActionSchedule { * @param tag A tag to use for logging. */ public Builder(String tag) { - this(tag, Clock.DEFAULT); - } - - /** - * @param tag A tag to use for logging. - * @param clock A clock to use for measuring delays. - */ - public Builder(String tag, Clock clock) { this.tag = tag; - this.clock = clock; - rootNode = new ActionNode(new RootAction(tag), clock, 0); + rootNode = new ActionNode(new RootAction(tag), 0); previousNode = rootNode; } @@ -141,7 +134,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder apply(Action action) { - return appendActionNode(new ActionNode(action, clock, currentDelayMs)); + return appendActionNode(new ActionNode(action, currentDelayMs)); } /** @@ -152,7 +145,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder repeat(Action action, long intervalMs) { - return appendActionNode(new ActionNode(action, clock, currentDelayMs, intervalMs)); + return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs)); } /** @@ -459,7 +452,6 @@ public final class ActionSchedule { /* package */ static final class ActionNode implements Runnable { private final Action action; - private final Clock clock; private final long delayMs; private final long repeatIntervalMs; @@ -468,27 +460,24 @@ public final class ActionSchedule { private SimpleExoPlayer player; private MappingTrackSelector trackSelector; private Surface surface; - private Handler mainHandler; + private HandlerWrapper mainHandler; /** * @param action The wrapped action. - * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. */ - public ActionNode(Action action, Clock clock, long delayMs) { - this(action, clock, delayMs, C.TIME_UNSET); + public ActionNode(Action action, long delayMs) { + this(action, delayMs, C.TIME_UNSET); } /** * @param action The wrapped action. - * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. * @param repeatIntervalMs The interval between one execution and the next repetition. If set to * {@link C#TIME_UNSET}, the action is executed once only. */ - public ActionNode(Action action, Clock clock, long delayMs, long repeatIntervalMs) { + public ActionNode(Action action, long delayMs, long repeatIntervalMs) { this.action = action; - this.clock = clock; this.delayMs = delayMs; this.repeatIntervalMs = repeatIntervalMs; } @@ -503,16 +492,19 @@ public final class ActionSchedule { } /** - * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node - * will be scheduled immediately after {@link #action} is executed. + * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node will + * be scheduled immediately after {@link #action} is executed. * * @param player The player to which actions should be applied. * @param trackSelector The track selector to which actions should be applied. * @param surface The surface to use when applying actions. * @param mainHandler A handler associated with the main thread of the host activity. */ - public void schedule(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface, Handler mainHandler) { + public void schedule( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper mainHandler) { this.player = player; this.trackSelector = trackSelector; this.surface = surface; @@ -520,7 +512,7 @@ public final class ActionSchedule { if (delayMs == 0 && Looper.myLooper() == mainHandler.getLooper()) { run(); } else { - clock.postDelayed(mainHandler, this, delayMs); + mainHandler.postDelayed(this, delayMs); } } @@ -528,13 +520,15 @@ public final class ActionSchedule { public void run() { action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { - clock.postDelayed(mainHandler, new Runnable() { - @Override - public void run() { - action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, null); - clock.postDelayed(mainHandler, this, repeatIntervalMs); - } - }, repeatIntervalMs); + mainHandler.postDelayed( + new Runnable() { + @Override + public void run() { + action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, null); + mainHandler.postDelayed(this, repeatIntervalMs); + } + }, + repeatIntervalMs); } } @@ -577,7 +571,7 @@ public final class ActionSchedule { SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface, - Handler handler, + HandlerWrapper handler, ActionNode nextAction) { Assertions.checkArgument(nextAction == null); if (callback != null) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ab63087f95..3a5f3ccd7a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.os.ConditionVariable; -import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.view.Surface; @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import junit.framework.Assert; @@ -72,7 +73,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private final ConditionVariable testFinished; private ActionSchedule pendingSchedule; - private Handler actionHandler; + private HandlerWrapper actionHandler; private MappingTrackSelector trackSelector; private SimpleExoPlayer player; private Surface surface; @@ -187,7 +188,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen player.addAudioDebugListener(this); player.addVideoDebugListener(this); player.setPlayWhenReady(true); - actionHandler = new Handler(); + actionHandler = + HandlerWrapper.Factory.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); // Schedule any pending actions. if (pendingSchedule != null) { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 4905fc2233..f100b4fac1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; @@ -38,6 +37,8 @@ import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; @@ -91,6 +92,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + private Clock clock; private PlayerFactory playerFactory; private Timeline timeline; private Object manifest; @@ -236,6 +238,18 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } + /** + * Sets the {@link Clock} to be used by the test runner. The default value is {@link + * Clock#DEFAULT}. + * + * @param clock A {@link Clock} to be used by the test runner. + * @return This builder. + */ + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + /** * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. @@ -312,19 +326,25 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (renderers == null) { renderers = new Renderer[] {new FakeRenderer(supportedFormats)}; } - renderersFactory = new RenderersFactory() { - @Override - public Renderer[] createRenderers(Handler eventHandler, - VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput) { - return renderers; - } - }; + renderersFactory = + new RenderersFactory() { + @Override + public Renderer[] createRenderers( + android.os.Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + return renderers; + } + }; } if (loadControl == null) { loadControl = new DefaultLoadControl(); } + if (clock == null) { + clock = Clock.DEFAULT; + } if (playerFactory == null) { playerFactory = new PlayerFactory() { @Override @@ -344,6 +364,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener expectedPlayerEndedCount = 1; } return new ExoPlayerTestRunner( + clock, playerFactory, mediaSource, renderersFactory, @@ -368,7 +389,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private final @Nullable AudioRendererEventListener audioRendererEventListener; private final HandlerThread playerThread; - private final Handler handler; + private final HandlerWrapper handler; private final CountDownLatch endedCountDownLatch; private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; @@ -383,6 +404,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private boolean playerWasPrepared; private ExoPlayerTestRunner( + Clock clock, PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, @@ -411,7 +433,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); - this.handler = new Handler(playerThread.getLooper()); + this.handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null); } // Called on the test thread to run the test. 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 843e5858d8..c78a8c03e0 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 @@ -15,19 +15,21 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; import java.util.ArrayList; import java.util.List; -/** - * Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. - */ +/** Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. */ public final class FakeClock implements Clock { - private long currentTimeMs; private final List wakeUpTimes; - private final List handlerPosts; + private final List handlerMessages; + + private long currentTimeMs; /** * Create {@link FakeClock} with an arbitrary initial timestamp. @@ -37,7 +39,7 @@ public final class FakeClock implements Clock { public FakeClock(long initialTimeMs) { this.currentTimeMs = initialTimeMs; this.wakeUpTimes = new ArrayList<>(); - this.handlerPosts = new ArrayList<>(); + this.handlerMessages = new ArrayList<>(); } /** @@ -53,10 +55,9 @@ public final class FakeClock implements Clock { break; } } - for (int i = handlerPosts.size() - 1; i >= 0; i--) { - if (handlerPosts.get(i).postTime <= currentTimeMs) { - HandlerPostData postData = handlerPosts.remove(i); - postData.handler.post(postData.runnable); + for (int i = handlerMessages.size() - 1; i >= 0; i--) { + if (handlerMessages.get(i).maybeSendToTarget(currentTimeMs)) { + handlerMessages.remove(i); } } } @@ -84,27 +85,131 @@ public final class FakeClock implements Clock { } @Override - public synchronized void postDelayed(Handler handler, Runnable runnable, long delayMs) { - if (delayMs <= 0) { - handler.post(runnable); - } else { - handlerPosts.add(new HandlerPostData(currentTimeMs + delayMs, handler, runnable)); - } + public HandlerWrapper createHandler(Looper looper, Callback callback) { + return new ClockHandler(looper, callback); } - private static final class HandlerPostData { + /** Adds a handler post to list of pending messages. */ + protected synchronized void addDelayedHandlerMessage( + HandlerWrapper handler, Runnable runnable, long delayMs) { + handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, runnable)); + } - public final long postTime; - public final Handler handler; - public final Runnable runnable; + /** Adds an empty handler message to list of pending messages. */ + protected synchronized void addDelayedHandlerMessage( + HandlerWrapper handler, int message, long delayMs) { + handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, message)); + } - public HandlerPostData(long postTime, Handler handler, Runnable runnable) { + /** Message data saved to send messages or execute runnables at a later time on a Handler. */ + private static final class HandlerMessageData { + + private final long postTime; + private final HandlerWrapper handler; + private final Runnable runnable; + private final int message; + + public HandlerMessageData(long postTime, HandlerWrapper handler, Runnable runnable) { this.postTime = postTime; this.handler = handler; this.runnable = runnable; + this.message = 0; } + public HandlerMessageData(long postTime, HandlerWrapper handler, int message) { + this.postTime = postTime; + this.handler = handler; + this.runnable = null; + this.message = message; + } + + /** Sends the message and returns whether the message was sent to its target. */ + public boolean maybeSendToTarget(long currentTimeMs) { + if (postTime <= currentTimeMs) { + if (runnable != null) { + handler.post(runnable); + } else { + handler.sendEmptyMessage(message); + } + return true; + } + return false; + } } + /** HandlerWrapper implementation using the enclosing Clock to schedule delayed messages. */ + private final class ClockHandler implements HandlerWrapper { + + private final android.os.Handler handler; + + public ClockHandler(Looper looper, Callback callback) { + handler = new android.os.Handler(looper, callback); + } + + @Override + public Looper getLooper() { + return handler.getLooper(); + } + + @Override + public Message obtainMessage(int what) { + return handler.obtainMessage(what); + } + + @Override + public Message obtainMessage(int what, Object obj) { + return handler.obtainMessage(what, obj); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2) { + return handler.obtainMessage(what, arg1, arg2); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2, Object obj) { + return handler.obtainMessage(what, arg1, arg2, obj); + } + + @Override + public boolean sendEmptyMessage(int what) { + return handler.sendEmptyMessage(what); + } + + @Override + public boolean sendEmptyMessageDelayed(int what, long delayMs) { + if (delayMs <= 0) { + return handler.sendEmptyMessage(what); + } else { + addDelayedHandlerMessage(this, what, delayMs); + return true; + } + } + + @Override + public void removeMessages(int what) { + handler.removeMessages(what); + } + + @Override + public void removeCallbacksAndMessages(Object token) { + handler.removeCallbacksAndMessages(token); + } + + @Override + public boolean post(Runnable runnable) { + return handler.post(runnable); + } + + @Override + public boolean postDelayed(Runnable runnable, long delayMs) { + if (delayMs <= 0) { + return handler.post(runnable); + } else { + addDelayedHandlerMessage(this, runnable, delayMs); + return true; + } + } + } } From 61b9e846a86fde4b97b2cda27b3850ba475262b5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 06:36:46 -0800 Subject: [PATCH 217/417] Allow setting a Clock for the main playback thread. This allows to inject a FakeClock for tests. Other playback components (e.g. some media sources) still use SystemClock but they can be amended in the future if needed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179921889 --- .../android/exoplayer2/DefaultMediaClock.java | 10 ------ .../android/exoplayer2/ExoPlayerFactory.java | 3 +- .../android/exoplayer2/ExoPlayerImpl.java | 8 +++-- .../exoplayer2/ExoPlayerImplInternal.java | 26 +++++++-------- .../android/exoplayer2/SimpleExoPlayer.java | 32 ++++++++++++++++--- .../exoplayer2/util/HandlerWrapper.java | 14 ++++++-- .../exoplayer2/util/StandaloneMediaClock.java | 7 ---- .../exoplayer2/util/SystemHandler.java | 11 +++++-- .../exoplayer2/testutil/FakeClock.java | 3 +- .../testutil/FakeSimpleExoPlayer.java | 7 ++-- 10 files changed, 74 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 5f342bc722..ed57cec70c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -46,16 +46,6 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; private @Nullable Renderer rendererClockSource; private @Nullable MediaClock rendererClock; - /** - * Creates a new instance with listener for playback parameter changes. - * - * @param listener A {@link PlaybackParameterListener} to listen for playback parameter - * changes. - */ - public DefaultMediaClock(PlaybackParameterListener listener) { - this(listener, Clock.DEFAULT); - } - /** * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use * for the standalone clock implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index b647e541bc..821671e34e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Clock; /** * A factory for {@link ExoPlayer} instances. @@ -160,7 +161,7 @@ public final class ExoPlayerFactory { */ public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl); + return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index afb6428fa5..4e97a47924 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.List; @@ -75,9 +76,11 @@ import java.util.concurrent.CopyOnWriteArraySet; * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param clock The {@link Clock} that will be used by the instance. */ @SuppressLint("HandlerLeak") - public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { + public ExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); @@ -116,7 +119,8 @@ import java.util.concurrent.CopyOnWriteArraySet; repeatMode, shuffleModeEnabled, eventHandler, - this); + this, + clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } 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 f3d0e1794b..c5fdf38bfa 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 @@ -39,6 +39,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -102,7 +104,7 @@ import java.util.Collections; private final TrackSelector trackSelector; private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; - private final Handler handler; + private final HandlerWrapper handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; private final ExoPlayer player; @@ -126,7 +128,6 @@ import java.util.Collections; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; @@ -146,7 +147,8 @@ import java.util.Collections; @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, Handler eventHandler, - ExoPlayer player) { + ExoPlayer player, + Clock clock) { this.renderers = renderers; this.trackSelector = trackSelector; this.emptyTrackSelectorResult = emptyTrackSelectorResult; @@ -170,7 +172,7 @@ import java.util.Collections; renderers[i].setIndex(i); rendererCapabilities[i] = renderers[i].getCapabilities(); } - mediaClock = new DefaultMediaClock(this); + mediaClock = new DefaultMediaClock(this, clock); customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); @@ -183,7 +185,7 @@ import java.util.Collections; internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); - handler = new Handler(internalPlaybackThread.getLooper(), this); + handler = clock.createHandler(internalPlaybackThread.getLooper(), this); } public void prepare(MediaSource mediaSource, boolean resetPosition) { @@ -527,7 +529,6 @@ import java.util.Collections; maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); playbackInfo.positionUs = periodPositionUs; } - elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE @@ -549,16 +550,19 @@ import java.util.Collections; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); + long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; + for (Renderer renderer : enabledRenderers) { // TODO: Each renderer should return the maximum delay before which it wishes to be called // again. The minimum of these values should then be used as the delay before the next // invocation of this method. - renderer.render(rendererPositionUs, elapsedRealtimeUs); + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); allRenderersEnded = allRenderersEnded && renderer.isEnded(); // Determine whether the renderer is ready (or ended). We override to assume the renderer is // ready if it needs the next sample stream. This is necessary to avoid getting stuck if @@ -625,13 +629,7 @@ import java.util.Collections; private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { handler.removeMessages(MSG_DO_SOME_WORK); - long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs; - long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime(); - if (nextOperationDelayMs <= 0) { - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else { - handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs); - } + handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, intervalMs, thisOperationStartTimeMs); } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e2d0ed1422..d4346a65e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; @@ -109,8 +110,28 @@ public class SimpleExoPlayer implements ExoPlayer { private AudioAttributes audioAttributes; private float audioVolume; - protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, - LoadControl loadControl) { + /** + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + */ + protected SimpleExoPlayer( + RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { + this(renderersFactory, trackSelector, loadControl, Clock.DEFAULT); + } + + /** + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + */ + protected SimpleExoPlayer( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + Clock clock) { componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); @@ -129,7 +150,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. - player = createExoPlayerImpl(renderers, trackSelector, loadControl); + player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); } /** @@ -864,11 +885,12 @@ public class SimpleExoPlayer implements ExoPlayer { * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param clock The {@link Clock} that will be used by this instance. * @return A new {@link ExoPlayer} instance. */ protected ExoPlayer createExoPlayerImpl( - Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl); + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { + return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock); } private void removeSurfaceCallbacks() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index 25f9c9bb38..b9f3a750d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -59,8 +59,18 @@ public interface HandlerWrapper { /** @see Handler#sendEmptyMessage(int). */ boolean sendEmptyMessage(int what); - /** @see Handler#sendEmptyMessageDelayed(int, long). */ - boolean sendEmptyMessageDelayed(int what, long delayMs); + /** + * Variant of {@code Handler#sendEmptyMessageDelayed(int, long)} which also takes a reference time + * measured by {@code android.os.SystemClock#elapsedRealtime()} to which the delay is added. + * + * @param what The message identifier. + * @param delayMs The delay in milliseconds to send the message. This delay is added to the {@code + * referenceTimeMs}. + * @param referenceTimeMs The time which the delay is added to. Always measured with {@code + * android.os.SystemClock#elapsedRealtime()}. + * @return Whether the message was successfully enqueued on the Handler thread. + */ + boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs); /** @see Handler#removeMessages(int). */ void removeMessages(int what); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index 3c0ec2a854..b1f53416fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -31,13 +31,6 @@ public final class StandaloneMediaClock implements MediaClock { private long baseElapsedMs; private PlaybackParameters playbackParameters; - /** - * Creates a new standalone media clock. - */ - public StandaloneMediaClock() { - this(Clock.DEFAULT); - } - /** * Creates a new standalone media clock using the given {@link Clock} implementation. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java index b9fe771053..e99c626057 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; /** The standard implementation of {@link HandlerWrapper}. */ /* package */ final class SystemHandler implements HandlerWrapper { @@ -67,8 +68,14 @@ import android.os.Message; } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs) { - return handler.sendEmptyMessageDelayed(what, delayMs); + public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { + long targetMessageTime = referenceTimeMs + delayMs; + long remainingDelayMs = targetMessageTime - SystemClock.elapsedRealtime(); + if (remainingDelayMs <= 0) { + return handler.sendEmptyMessage(what); + } else { + return handler.sendEmptyMessageDelayed(what, remainingDelayMs); + } } @Override 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 c78a8c03e0..83ecbacdde 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 @@ -177,7 +177,8 @@ public final class FakeClock implements Clock { } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs) { + public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { + // Ignore referenceTimeMs measured by SystemClock and just send with requested delay. if (delayMs <= 0) { return handler.sendEmptyMessage(what); } else { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index a8ba3b3420..591b94a9cd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; import java.util.Arrays; import java.util.concurrent.CopyOnWriteArraySet; @@ -58,13 +59,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, FakeClock clock) { - super (renderersFactory, trackSelector, loadControl); + super(renderersFactory, trackSelector, loadControl, clock); player.setFakeClock(clock); } @Override - protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + protected ExoPlayer createExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); return player; } From f2bb2d27be3cd34654d555970910bd8d459ff77a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Dec 2017 07:35:30 -0800 Subject: [PATCH 218/417] Add support for extracting 32-bit float WAVE Issue: #3379 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179925320 --- RELEASENOTES.md | 2 ++ .../extractor/wav/WavHeaderReader.java | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c45c3449a..3bc55476c2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,8 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. +* Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). ### 2.6.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 0e99380a1c..d0810a0629 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -31,6 +31,8 @@ import java.io.IOException; /** Integer PCM audio data. */ private static final int TYPE_PCM = 0x0001; + /** Float PCM audio data. */ + private static final int TYPE_FLOAT = 0x0003; /** Extended WAVE format. */ private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; @@ -87,14 +89,22 @@ import java.io.IOException; + blockAlignment); } - @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); - return null; + @C.PcmEncoding int encoding; + switch (type) { + case TYPE_PCM: + case TYPE_WAVE_FORMAT_EXTENSIBLE: + encoding = Util.getPcmEncoding(bitsPerSample); + break; + case TYPE_FLOAT: + encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; + break; + default: + Log.e(TAG, "Unsupported WAV format type: " + type); + return null; } - if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { - Log.e(TAG, "Unsupported WAV format type: " + type); + if (encoding == C.ENCODING_INVALID) { + Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type); return null; } From 410e614cfd1c42299bce27dbcbf4d695b2943511 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 07:43:36 -0800 Subject: [PATCH 219/417] Run custom messages executed on playback thread immediately. This ensures message order if multiple custom messages running on the playback thread and direct player commands are called immedately after each other. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179925852 --- .../exoplayer2/ExoPlayerImplInternal.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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 c5fdf38bfa..1c680d4aba 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 @@ -904,13 +904,17 @@ import java.util.Collections; } } }; - handler.post( - new Runnable() { - @Override - public void run() { - customMessageInfo.message.getHandler().post(handleMessageRunnable); - } - }); + if (customMessageInfo.message.getHandler().getLooper() == handler.getLooper()) { + handleMessageRunnable.run(); + } else { + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } } private void resolveCustomMessagePositions() { From f279f3c843ff9825b2e323d58feb0096a29e64a7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 08:39:21 -0800 Subject: [PATCH 220/417] Replace FakeExoPlayer with real player running with fake clock. This ensures that simulated playbacks always use the current player implementation. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179929911 --- .../testutil/ExoPlayerTestRunner.java | 67 +-- .../exoplayer2/testutil/FakeClock.java | 2 +- .../testutil/FakeSimpleExoPlayer.java | 541 ------------------ 3 files changed, 16 insertions(+), 594 deletions(-) delete mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index f100b4fac1..cefe94b6c7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -19,7 +19,6 @@ import android.os.HandlerThread; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Player; @@ -32,11 +31,11 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; @@ -59,26 +58,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener */ public static final class Builder { - /** - * Factory to create an {@link SimpleExoPlayer} instance. The player will be created on its own - * {@link HandlerThread}. - */ - public interface PlayerFactory { - - /** - * Creates a new {@link SimpleExoPlayer} using the provided renderers factory, track selector, - * and load control. - * - * @param renderersFactory A {@link RenderersFactory} to be used for the new player. - * @param trackSelector A {@link MappingTrackSelector} to be used for the new player. - * @param loadControl A {@link LoadControl} to be used for the new player. - * @return A new {@link SimpleExoPlayer}. - */ - SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, - MappingTrackSelector trackSelector, LoadControl loadControl); - - } - /** * A generic video {@link Format} which can be used to set up media sources and renderers. */ @@ -93,7 +72,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); private Clock clock; - private PlayerFactory playerFactory; private Timeline timeline; private Object manifest; private MediaSource mediaSource; @@ -223,21 +201,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } - /** - * Sets the {@link PlayerFactory} which creates the {@link SimpleExoPlayer} to be used by the - * test runner. The default value is a {@link SimpleExoPlayer} with the renderers provided by - * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)}, the - * track selector provided by {@link #setTrackSelector(MappingTrackSelector)} and the load - * control provided by {@link #setLoadControl(LoadControl)}. - * - * @param playerFactory A {@link PlayerFactory} to create the player. - * @return This builder. - */ - public Builder setExoPlayer(PlayerFactory playerFactory) { - this.playerFactory = playerFactory; - return this; - } - /** * Sets the {@link Clock} to be used by the test runner. The default value is {@link * Clock#DEFAULT}. @@ -345,15 +308,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (clock == null) { clock = Clock.DEFAULT; } - if (playerFactory == null) { - playerFactory = new PlayerFactory() { - @Override - public SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, - MappingTrackSelector trackSelector, LoadControl loadControl) { - return ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl); - } - }; - } if (mediaSource == null) { if (timeline == null) { timeline = new FakeTimeline(1); @@ -365,7 +319,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } return new ExoPlayerTestRunner( clock, - playerFactory, mediaSource, renderersFactory, trackSelector, @@ -378,7 +331,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } } - private final PlayerFactory playerFactory; + private final Clock clock; private final MediaSource mediaSource; private final RenderersFactory renderersFactory; private final MappingTrackSelector trackSelector; @@ -405,7 +358,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private ExoPlayerTestRunner( Clock clock, - PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, @@ -415,7 +367,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Nullable VideoRendererEventListener videoRendererEventListener, @Nullable AudioRendererEventListener audioRendererEventListener, int expectedPlayerEndedCount) { - this.playerFactory = playerFactory; + this.clock = clock; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; @@ -451,7 +403,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Override public void run() { try { - player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player = new TestSimpleExoPlayer(renderersFactory, trackSelector, loadControl, clock); player.addListener(ExoPlayerTestRunner.this); if (eventListener != null) { player.addListener(eventListener); @@ -685,4 +637,15 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener actionScheduleFinishedCountDownLatch.countDown(); } + /** SimpleExoPlayer implementation using a custom Clock. */ + private static final class TestSimpleExoPlayer extends SimpleExoPlayer { + + public TestSimpleExoPlayer( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + Clock clock) { + super(renderersFactory, trackSelector, loadControl, clock); + } + } } 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 83ecbacdde..49656eef99 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 @@ -24,7 +24,7 @@ import java.util.ArrayList; import java.util.List; /** Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. */ -public final class FakeClock implements Clock { +public class FakeClock implements Clock { private final List wakeUpTimes; private final List handlerMessages; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java deleted file mode 100644 index 591b94a9cd..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Copyright (C) 2017 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 android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; -import com.google.android.exoplayer2.trackselection.TrackSelectorResult; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.Arrays; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Fake {@link SimpleExoPlayer} which runs a simplified copy of the playback loop as fast as - * possible without waiting. It does only support single period timelines and does not support - * updates during playback (like seek, timeline changes, repeat mode changes). - */ -public class FakeSimpleExoPlayer extends SimpleExoPlayer { - - private FakeExoPlayer player; - - public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, - LoadControl loadControl, FakeClock clock) { - super(renderersFactory, trackSelector, loadControl, clock); - player.setFakeClock(clock); - } - - @Override - protected ExoPlayer createExoPlayerImpl( - Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { - this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); - return player; - } - - private static class FakeExoPlayer extends StubExoPlayer implements MediaSource.Listener, - MediaPeriod.Callback, Runnable { - - private final Renderer[] renderers; - private final TrackSelector trackSelector; - private final LoadControl loadControl; - private final CopyOnWriteArraySet eventListeners; - private final HandlerThread playbackThread; - private final Handler playbackHandler; - private final Handler eventListenerHandler; - - private FakeClock clock; - private MediaSource mediaSource; - private Timeline timeline; - private Object manifest; - private MediaPeriod mediaPeriod; - private TrackSelectorResult selectorResult; - - private boolean isStartingUp; - private boolean isLoading; - private int playbackState; - private long rendererPositionUs; - private long durationUs; - private volatile long currentPositionMs; - private volatile long bufferedPositionMs; - - public FakeExoPlayer(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { - this.renderers = renderers; - this.trackSelector = trackSelector; - this.loadControl = loadControl; - this.eventListeners = new CopyOnWriteArraySet<>(); - Looper eventListenerLooper = Looper.myLooper(); - this.eventListenerHandler = new Handler(eventListenerLooper != null ? eventListenerLooper - : Looper.getMainLooper()); - this.playbackThread = new HandlerThread("FakeExoPlayer Thread"); - playbackThread.start(); - this.playbackHandler = new Handler(playbackThread.getLooper()); - this.isStartingUp = true; - this.isLoading = false; - this.playbackState = Player.STATE_IDLE; - this.durationUs = C.TIME_UNSET; - } - - public void setFakeClock(FakeClock clock) { - this.clock = clock; - } - - @Override - public void addListener(Player.EventListener listener) { - eventListeners.add(listener); - } - - @Override - public void removeListener(Player.EventListener listener) { - eventListeners.remove(listener); - } - - @Override - public int getPlaybackState() { - return playbackState; - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - if (!playWhenReady) { - throw new UnsupportedOperationException(); - } - } - - @Override - public boolean getPlayWhenReady() { - return true; - } - - @Override - public int getRepeatMode() { - return Player.REPEAT_MODE_OFF; - } - - @Override - public boolean getShuffleModeEnabled() { - return false; - } - - @Override - public boolean isLoading() { - return isLoading; - } - - @Override - public PlaybackParameters getPlaybackParameters() { - return PlaybackParameters.DEFAULT; - } - - @Override - public void stop() { - stop(/* reset= */ false); - } - - @Override - public void stop(boolean reset) { - stopPlayback(/* quitPlaybackThread= */ false); - } - - @Override - @SuppressWarnings("ThreadJoinLoop") - public void release() { - stopPlayback(/* quitPlaybackThread= */ true); - while (playbackThread.isAlive()) { - try { - playbackThread.join(); - } catch (InterruptedException e) { - // Ignore interrupt. - } - } - } - - @Override - public int getRendererCount() { - return renderers.length; - } - - @Override - public int getRendererType(int index) { - return renderers[index].getTrackType(); - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - return selectorResult != null ? selectorResult.groups : null; - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return selectorResult != null ? selectorResult.selections : null; - } - - @Nullable - @Override - public Object getCurrentManifest() { - return manifest; - } - - @Override - public Timeline getCurrentTimeline() { - return timeline; - } - - @Override - public int getCurrentPeriodIndex() { - return 0; - } - - @Override - public int getCurrentWindowIndex() { - return 0; - } - - @Override - public int getNextWindowIndex() { - return C.INDEX_UNSET; - } - - @Override - public int getPreviousWindowIndex() { - return C.INDEX_UNSET; - } - - @Override - public long getDuration() { - return C.usToMs(durationUs); - } - - @Override - public long getCurrentPosition() { - return currentPositionMs; - } - - @Override - public long getBufferedPosition() { - return bufferedPositionMs == C.TIME_END_OF_SOURCE ? getDuration() : bufferedPositionMs; - } - - @Override - public int getBufferedPercentage() { - long duration = getDuration(); - return duration == C.TIME_UNSET ? 0 : (int) (getBufferedPosition() * 100 / duration); - } - - @Override - public boolean isCurrentWindowDynamic() { - return false; - } - - @Override - public boolean isCurrentWindowSeekable() { - return false; - } - - @Override - public boolean isPlayingAd() { - return false; - } - - @Override - public int getCurrentAdGroupIndex() { - return 0; - } - - @Override - public int getCurrentAdIndexInAdGroup() { - return 0; - } - - @Override - public long getContentPosition() { - return getCurrentPosition(); - } - - @Override - public Looper getPlaybackLooper() { - return playbackThread.getLooper(); - } - - @Override - public void prepare(MediaSource mediaSource) { - prepare(mediaSource, true, true); - } - - @Override - public void prepare(final MediaSource mediaSource, boolean resetPosition, boolean resetState) { - if (!resetPosition || !resetState) { - throw new UnsupportedOperationException(); - } - this.mediaSource = mediaSource; - playbackHandler.post(new Runnable() { - @Override - public void run() { - mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this); - } - }); - } - - // MediaSource.Listener - - @Override - public void onSourceInfoRefreshed(MediaSource source, final Timeline timeline, - final @Nullable Object manifest) { - if (this.timeline != null) { - throw new UnsupportedOperationException(); - } - Assertions.checkArgument(timeline.getPeriodCount() == 1); - Assertions.checkArgument(timeline.getWindowCount() == 1); - final ConditionVariable waitForNotification = new ConditionVariable(); - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener eventListener : eventListeners) { - FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; - FakeExoPlayer.this.timeline = timeline; - FakeExoPlayer.this.manifest = manifest; - eventListener.onTimelineChanged(timeline, manifest, - Player.TIMELINE_CHANGE_REASON_PREPARED); - waitForNotification.open(); - } - } - }); - waitForNotification.block(); - this.mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), loadControl.getAllocator()); - mediaPeriod.prepare(this, 0); - } - - // MediaPeriod.Callback - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - maybeContinueLoading(); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - try { - initializePlaybackLoop(); - } catch (ExoPlaybackException e) { - handlePlayerError(e); - } - } - - // Runnable (Playback loop). - - @Override - public void run() { - try { - maybeContinueLoading(); - mediaPeriod.discardBuffer(rendererPositionUs, /* toKeyframe= */ false); - boolean allRenderersEnded = true; - boolean allRenderersReadyOrEnded = true; - if (playbackState == Player.STATE_READY) { - for (Renderer renderer : renderers) { - renderer.render(rendererPositionUs, C.msToUs(clock.elapsedRealtime())); - if (!renderer.isEnded()) { - allRenderersEnded = false; - } - if (!(renderer.isReady() || renderer.isEnded())) { - allRenderersReadyOrEnded = false; - } - } - } - if (rendererPositionUs >= durationUs && allRenderersEnded) { - changePlaybackState(Player.STATE_ENDED); - return; - } - long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); - if (playbackState == Player.STATE_BUFFERING && allRenderersReadyOrEnded - && haveSufficientBuffer(!isStartingUp, rendererPositionUs, bufferedPositionUs)) { - changePlaybackState(Player.STATE_READY); - isStartingUp = false; - } else if (playbackState == Player.STATE_READY && !allRenderersReadyOrEnded) { - changePlaybackState(Player.STATE_BUFFERING); - } - // Advance simulated time by 10ms. - clock.advanceTime(10); - if (playbackState == Player.STATE_READY) { - rendererPositionUs += 10000; - } - this.currentPositionMs = C.usToMs(rendererPositionUs); - this.bufferedPositionMs = C.usToMs(bufferedPositionUs); - playbackHandler.post(this); - } catch (ExoPlaybackException e) { - handlePlayerError(e); - } - } - - // Internal logic - - private void initializePlaybackLoop() throws ExoPlaybackException { - Assertions.checkNotNull(clock); - trackSelector.init(new InvalidationListener() { - @Override - public void onTrackSelectionsInvalidated() { - throw new IllegalStateException(); - } - }); - RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; - for (int i = 0; i < renderers.length; i++) { - rendererCapabilities[i] = renderers[i].getCapabilities(); - } - selectorResult = trackSelector.selectTracks(rendererCapabilities, - mediaPeriod.getTrackGroups()); - SampleStream[] sampleStreams = new SampleStream[renderers.length]; - boolean[] mayRetainStreamFlags = new boolean[renderers.length]; - Arrays.fill(mayRetainStreamFlags, true); - mediaPeriod.selectTracks( - selectorResult.selections.getAll(), - mayRetainStreamFlags, - sampleStreams, - new boolean[renderers.length], - /* positionUs = */ 0); - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener eventListener : eventListeners) { - eventListener.onTracksChanged(selectorResult.groups, selectorResult.selections); - } - } - }); - - loadControl.onPrepared(); - loadControl.onTracksSelected(renderers, selectorResult.groups, selectorResult.selections); - - for (int i = 0; i < renderers.length; i++) { - TrackSelection selection = selectorResult.selections.get(i); - Format[] formats = new Format[selection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = selection.getFormat(j); - } - renderers[i].enable(selectorResult.rendererConfigurations[i], formats, sampleStreams[i], 0, - false, 0); - renderers[i].setCurrentStreamFinal(); - } - - rendererPositionUs = 0; - changePlaybackState(Player.STATE_BUFFERING); - playbackHandler.post(this); - } - - private void maybeContinueLoading() { - boolean newIsLoading = false; - long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { - long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; - if (loadControl.shouldContinueLoading(bufferedDurationUs, 1f)) { - newIsLoading = true; - mediaPeriod.continueLoading(rendererPositionUs); - } - } - if (newIsLoading != isLoading) { - isLoading = newIsLoading; - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener eventListener : eventListeners) { - eventListener.onLoadingChanged(isLoading); - } - } - }); - } - } - - private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, - long bufferedPositionUs) { - return bufferedPositionUs == C.TIME_END_OF_SOURCE - || loadControl.shouldStartPlayback( - bufferedPositionUs - rendererPositionUs, 1f, rebuffering); - } - - private void handlePlayerError(final ExoPlaybackException e) { - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener listener : eventListeners) { - listener.onPlayerError(e); - } - } - }); - changePlaybackState(Player.STATE_ENDED); - } - - private void changePlaybackState(final int playbackState) { - this.playbackState = playbackState; - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener listener : eventListeners) { - listener.onPlayerStateChanged(true, playbackState); - } - } - }); - } - - private void releaseMedia() { - if (mediaSource != null) { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - mediaPeriod = null; - } - mediaSource.releaseSource(); - mediaSource = null; - } - } - - private void stopPlayback(final boolean quitPlaybackThread) { - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - releaseMedia(); - changePlaybackState(Player.STATE_IDLE); - if (quitPlaybackThread) { - playbackThread.quit(); - } - } - }); - } - - } - -} From 8f1ef6a29aeedfdf8934e85b9bf9089fe06c6dfb Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 27 Dec 2017 09:02:40 -0800 Subject: [PATCH 221/417] Typo fixes Issue:#3631 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180197723 --- README.md | 2 +- RELEASENOTES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecfe3eb96f..7f35329516 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ individually. In addition to library modules, ExoPlayer has multiple extension modules that depend on external libraries to provide additional functionality. Some -extensions are available from JCenter, whereas others must be built manaully. +extensions are available from JCenter, whereas others must be built manually. Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3bc55476c2..43e860b000 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -221,7 +221,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to to connect ExoPlayer with +* MediaSession extension: Provides an easy to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From ad80784c19981eb95ad22550cd41f445b5b7d101 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 28 Dec 2017 02:14:20 -0800 Subject: [PATCH 222/417] Fix parameter order in DefaultLoadControl constructor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180254437 --- .../java/com/google/android/exoplayer2/DefaultLoadControl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 3708500d9f..26873fcf2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -90,8 +90,8 @@ public class DefaultLoadControl implements LoadControl { allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } From 2bd704d8335292e36f0bcd84767282881c3df05b Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 Jan 2018 05:35:14 -0800 Subject: [PATCH 223/417] Add missing override for reevaluateBuffer in FakeAdaptiveMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180538379 --- .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 1a3e69f029..7b9fe3db07 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -96,6 +96,12 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod } } + @Override + public void reevaluateBuffer(long positionUs) { + super.reevaluateBuffer(positionUs); + sequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public long getBufferedPositionUs() { super.getBufferedPositionUs(); From 88abb153bb18c9b15be0d8e15ea673cfd9dccf3e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 06:15:13 -0800 Subject: [PATCH 224/417] Force audio renderers to report same position when not started Whilst the previous behavior was WAI and had the advantage of updating the position to be more exact when known, there were a couple of disadvantages: 1. If seeking to the very end of a period in a playlist when paused, the position adjustment could trigger a position discontinuity to the next period. 2. We de-duplicate seeks to the current playback position. The position adjustment can prevent this from being effective. This is particularly important with the new SeekParameters support. When seeking to nearest sync point it's often possible to de-duplicate seeks, but we cannot do so if the playback position adjusts away from the sync point's time. Issue: #2439 Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180540736 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 19 +++++--- .../audio/SimpleDecoderAudioRenderer.java | 45 +++++++++++-------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 43e860b000..b0027d75a1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -78,6 +78,8 @@ ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Prevent period transitions when seeking to the end of a period when paused + ([#2439](https://github.com/google/ExoPlayer/issues/2439)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 25ad847f7e..b4459e42aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -364,6 +364,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onStopped() { audioSink.pause(); + updateCurrentPosition(); super.onStopped(); } @@ -393,11 +394,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public long getPositionUs() { - long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; + if (getState() == STATE_STARTED) { + updateCurrentPosition(); } return currentPositionUs; } @@ -466,6 +464,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + private void updateCurrentPosition() { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { + currentPositionUs = + allowPositionDiscontinuity + ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d9ad549104..16a85fe1f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -459,11 +459,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public long getPositionUs() { - long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; + if (getState() == STATE_STARTED) { + updateCurrentPosition(); } return currentPositionUs; } @@ -510,6 +507,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onStopped() { audioSink.pause(); + updateCurrentPosition(); } @Override @@ -540,6 +538,22 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case C.MSG_SET_VOLUME: + audioSink.setVolume((Float) message); + break; + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioSink.setAudioAttributes(audioAttributes); + break; + default: + super.handleMessage(messageType, message); + break; + } + } + private void maybeInitDecoder() throws ExoPlaybackException { if (decoder != null) { return; @@ -625,19 +639,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher.inputFormatChanged(newFormat); } - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case C.MSG_SET_VOLUME: - audioSink.setVolume((Float) message); - break; - case C.MSG_SET_AUDIO_ATTRIBUTES: - AudioAttributes audioAttributes = (AudioAttributes) message; - audioSink.setAudioAttributes(audioAttributes); - break; - default: - super.handleMessage(messageType, message); - break; + private void updateCurrentPosition() { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { + currentPositionUs = + allowPositionDiscontinuity + ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; } } From 884f64017fd407aff53bc6e756a73d0f083d9cc1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 07:00:04 -0800 Subject: [PATCH 225/417] Typo fix ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180543378 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b0027d75a1..9f48db0be8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -223,7 +223,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to connect ExoPlayer with +* MediaSession extension: Provides an easy way to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From 22f8ee37d429e9ed0304da00d507035d7e7ef887 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 Jan 2018 07:12:25 -0800 Subject: [PATCH 226/417] Clean-up of player message handling. Some readability fixes for PlayerMessage and the handling in ExoPlayerImplInternal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180544294 --- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 2 +- .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 95 +++++++++---------- .../android/exoplayer2/PlayerMessage.java | 77 ++++++--------- .../android/exoplayer2/SimpleExoPlayer.java | 30 +++--- .../DynamicConcatenatingMediaSource.java | 10 +- .../testutil/MediaSourceTestRunner.java | 16 +--- 8 files changed, 107 insertions(+), 129 deletions(-) diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 0f8df65959..c5485d3f96 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -122,7 +122,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { player .createMessage(videoRenderer) .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) - .setMessage(new VpxVideoSurfaceView(context)) + .setPayload(new VpxVideoSurfaceView(context)) .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 4bd28150bc..a9980f9803 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -217,7 +217,7 @@ public interface ExoPlayer extends Player { /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * will be delivered immediately without blocking on the playback thread. The default {@link - * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. * Alternatively, the message can be sent at a specific window using {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 4e97a47924..b5f6e623eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -336,7 +336,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { for (ExoPlayerMessage message : messages) { - createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + createMessage(message.target).setType(message.messageType).setPayload(message.message).send(); } } @@ -357,7 +357,7 @@ import java.util.concurrent.CopyOnWriteArraySet; playerMessages.add( createMessage(message.target) .setType(message.messageType) - .setMessage(message.message) + .setPayload(message.message) .send()); } boolean wasInterrupted = false; 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 1c680d4aba..65f43ae684 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 @@ -79,6 +79,7 @@ import java.util.Collections; private static final int MSG_CUSTOM = 12; private static final int MSG_SET_REPEAT_MODE = 13; private static final int MSG_SET_SHUFFLE_ENABLED = 14; + private static final int MSG_SEND_MESSAGE_TO_TARGET = 15; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -223,14 +224,13 @@ import java.util.Collections; } @Override - public synchronized void sendMessage( - PlayerMessage message, PlayerMessage.Sender.Listener listener) { + public synchronized void sendMessage(PlayerMessage message) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); - listener.onMessageDeleted(); + message.markAsProcessed(/* isDelivered= */ false); return; } - handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); + handler.obtainMessage(MSG_CUSTOM, message).sendToTarget(); } public synchronized void release() { @@ -338,7 +338,10 @@ import java.util.Collections; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessageInternal((CustomMessageInfo) msg.obj); + sendMessageInternal((PlayerMessage) msg.obj); + break; + case MSG_SEND_MESSAGE_TO_TARGET: + sendCustomMessageToTargetThread((PlayerMessage) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -838,7 +841,7 @@ import java.util.Collections; if (resetState) { mediaPeriodInfoSequence.setTimeline(null); for (CustomMessageInfo customMessageInfo : customMessageInfos) { - customMessageInfo.listener.onMessageDeleted(); + customMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } customMessageInfos.clear(); nextCustomMessageInfoIndex = 0; @@ -862,58 +865,54 @@ import java.util.Collections; } } - private void sendMessageInternal(CustomMessageInfo customMessageInfo) { - if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + private void sendMessageInternal(PlayerMessage message) { + if (message.getPositionMs() == C.TIME_UNSET) { // If no delivery time is specified, trigger immediate message delivery. - sendCustomMessagesToTarget(customMessageInfo); + sendCustomMessageToTarget(message); } else if (playbackInfo.timeline == null) { // Still waiting for initial timeline to resolve position. - customMessageInfos.add(customMessageInfo); + customMessageInfos.add(new CustomMessageInfo(message)); } else { + CustomMessageInfo customMessageInfo = new CustomMessageInfo(message); if (resolveCustomMessagePosition(customMessageInfo)) { customMessageInfos.add(customMessageInfo); // Ensure new message is inserted according to playback order. Collections.sort(customMessageInfos); } else { - customMessageInfo.listener.onMessageDeleted(); + message.markAsProcessed(/* isDelivered= */ false); } } } - private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { - final Runnable handleMessageRunnable = - new Runnable() { - @Override - public void run() { - try { - customMessageInfo - .message - .getTarget() - .handleMessage( - customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); - } catch (ExoPlaybackException e) { - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - } finally { - customMessageInfo.listener.onMessageDelivered(); - if (customMessageInfo.message.getDeleteAfterDelivery()) { - customMessageInfo.listener.onMessageDeleted(); - } - // The message may have caused something to change that now requires us to do - // work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } - } - }; - if (customMessageInfo.message.getHandler().getLooper() == handler.getLooper()) { - handleMessageRunnable.run(); + private void sendCustomMessageToTarget(PlayerMessage message) { + if (message.getHandler().getLooper() == handler.getLooper()) { + deliverCustomMessage(message); + // The message may have caused something to change that now requires us to do work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); } else { - handler.post( - new Runnable() { - @Override - public void run() { - customMessageInfo.message.getHandler().post(handleMessageRunnable); - } - }); + handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET, message).sendToTarget(); + } + } + + private void sendCustomMessageToTargetThread(final PlayerMessage message) { + message + .getHandler() + .post( + new Runnable() { + @Override + public void run() { + deliverCustomMessage(message); + } + }); + } + + private void deliverCustomMessage(PlayerMessage message) { + try { + message.getTarget().handleMessage(message.getType(), message.getPayload()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + message.markAsProcessed(/* isDelivered= */ true); } } @@ -921,7 +920,7 @@ import java.util.Collections; for (int i = customMessageInfos.size() - 1; i >= 0; i--) { if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { // Remove messages if new position can't be resolved. - customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.get(i).message.markAsProcessed(/* isDelivered= */ false); customMessageInfos.remove(i); } } @@ -1003,7 +1002,7 @@ import java.util.Collections; && nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendCustomMessagesToTarget(nextInfo); + sendCustomMessageToTarget(nextInfo.message); if (nextInfo.message.getDeleteAfterDelivery()) { customMessageInfos.remove(nextCustomMessageInfoIndex); } else { @@ -1942,15 +1941,13 @@ import java.util.Collections; private static final class CustomMessageInfo implements Comparable { public final PlayerMessage message; - public final PlayerMessage.Sender.Listener listener; public int resolvedPeriodIndex; public long resolvedPeriodTimeUs; public @Nullable Object resolvedPeriodUid; - public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + public CustomMessageInfo(PlayerMessage message) { this.message = message; - this.listener = listener; } public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 420eb60a48..1e8a89e102 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -32,32 +32,21 @@ public final class PlayerMessage { * Handles a message delivered to the target. * * @param messageType The message type. - * @param message The message. + * @param payload The message payload. * @throws ExoPlaybackException If an error occurred whilst handling the message. */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; + void handleMessage(int messageType, Object payload) throws ExoPlaybackException; } /** A sender for messages. */ public interface Sender { - /** A listener for message events triggered by the sender. */ - interface Listener { - - /** Called when the message has been delivered. */ - void onMessageDelivered(); - - /** Called when the message has been deleted. */ - void onMessageDeleted(); - } - /** * Sends a message. * * @param message The message to be sent. - * @param listener The listener to listen to message events. */ - void sendMessage(PlayerMessage message, Listener listener); + void sendMessage(PlayerMessage message); } private final Target target; @@ -65,14 +54,14 @@ public final class PlayerMessage { private final Timeline timeline; private int type; - private Object message; + private Object payload; private Handler handler; private int windowIndex; private long positionMs; private boolean deleteAfterDelivery; private boolean isSent; private boolean isDelivered; - private boolean isDeleted; + private boolean isProcessed; /** * Creates a new message. @@ -112,9 +101,9 @@ public final class PlayerMessage { } /** - * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * Sets the message type forwarded to {@link Target#handleMessage(int, Object)}. * - * @param messageType The custom message type. + * @param messageType The message type. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ @@ -124,27 +113,27 @@ public final class PlayerMessage { return this; } - /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + /** Returns the message type forwarded to {@link Target#handleMessage(int, Object)}. */ public int getType() { return type; } /** - * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * Sets the message payload forwarded to {@link Target#handleMessage(int, Object)}. * - * @param message The custom message. + * @param payload The message payload. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ - public PlayerMessage setMessage(@Nullable Object message) { + public PlayerMessage setPayload(@Nullable Object payload) { Assertions.checkState(!isSent); - this.message = message; + this.payload = payload; return this; } - /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ - public Object getMessage() { - return message; + /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */ + public Object getPayload() { + return payload; } /** @@ -248,25 +237,7 @@ public final class PlayerMessage { Assertions.checkArgument(deleteAfterDelivery); } isSent = true; - sender.sendMessage( - this, - new Sender.Listener() { - @Override - public void onMessageDelivered() { - synchronized (PlayerMessage.this) { - isDelivered = true; - PlayerMessage.this.notifyAll(); - } - } - - @Override - public void onMessageDeleted() { - synchronized (PlayerMessage.this) { - isDeleted = true; - PlayerMessage.this.notifyAll(); - } - } - }); + sender.sendMessage(this); return this; } @@ -287,9 +258,23 @@ public final class PlayerMessage { public synchronized boolean blockUntilDelivered() throws InterruptedException { Assertions.checkState(isSent); Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); - while (!isDelivered && !isDeleted) { + while (!isProcessed) { wait(); } return isDelivered; } + + /** + * Marks the message as processed. Should only be called by a {@link Sender} and may be called + * multiple times. + * + * @param isDelivered Whether the message has been delivered to its target. The message is + * considered as being delivered when this method has been called with {@code isDelivered} set + * to true at least once. + */ + public synchronized void markAsProcessed(boolean isDelivered) { + this.isDelivered |= isDelivered; + isProcessed = true; + notifyAll(); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index d4346a65e1..ec53e5a964 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -168,7 +169,7 @@ public class SimpleExoPlayer implements ExoPlayer { player .createMessage(renderer) .setType(C.MSG_SET_SCALING_MODE) - .setMessage(videoScalingMode) + .setPayload(videoScalingMode) .send(); } } @@ -357,7 +358,7 @@ public class SimpleExoPlayer implements ExoPlayer { player .createMessage(renderer) .setType(C.MSG_SET_AUDIO_ATTRIBUTES) - .setMessage(audioAttributes) + .setPayload(audioAttributes) .send(); } } @@ -379,7 +380,7 @@ public class SimpleExoPlayer implements ExoPlayer { this.audioVolume = audioVolume; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send(); } } } @@ -911,21 +912,22 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - boolean surfaceReplaced = this.surface != null && this.surface != surface; + List messages = new ArrayList<>(); for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - PlayerMessage message = - player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); - if (surfaceReplaced) { - try { - message.blockUntilDelivered(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } + messages.add( + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send()); } } - if (surfaceReplaced) { + if (this.surface != null && this.surface != surface) { + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + try { + for (PlayerMessage message : messages) { + message.blockUntilDelivered(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 54537ba548..c2e208afbe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -149,7 +149,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_ADD) - .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .setPayload(new MessageData<>(index, mediaSource, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -225,7 +225,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_ADD_MULTIPLE) - .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .setPayload(new MessageData<>(index, mediaSources, actionOnCompletion)) .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); @@ -264,7 +264,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_REMOVE) - .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .setPayload(new MessageData<>(index, null, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -304,7 +304,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_MOVE) - .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -438,7 +438,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); + player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send(); } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 93c14afc8f..635d0dd835 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; @@ -299,22 +298,17 @@ public class MediaSourceTestRunner { } @Override - public void sendMessage(PlayerMessage message, Listener listener) { - handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + public void sendMessage(PlayerMessage message) { + handler.obtainMessage(0, message).sendToTarget(); } @Override @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - Pair messageAndListener = (Pair) msg.obj; + PlayerMessage message = (PlayerMessage) msg.obj; try { - messageAndListener - .first - .getTarget() - .handleMessage( - messageAndListener.first.getType(), messageAndListener.first.getMessage()); - messageAndListener.second.onMessageDelivered(); - messageAndListener.second.onMessageDeleted(); + message.getTarget().handleMessage(message.getType(), message.getPayload()); + message.markAsProcessed(/* isDelivered= */ true); } catch (ExoPlaybackException e) { fail("Unexpected ExoPlaybackException."); } From 0821f578e8ca51fda23b9cddf901ed1f594bd493 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:04:24 -0800 Subject: [PATCH 227/417] Remove HandlerWrapper.Factory ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180558741 --- .../google/android/exoplayer2/util/Clock.java | 19 ++++++++++++++---- .../exoplayer2/util/HandlerWrapper.java | 20 ++----------------- .../android/exoplayer2/util/SystemClock.java | 3 ++- ...Handler.java => SystemHandlerWrapper.java} | 13 ++---------- .../exoplayer2/testutil/ExoHostedTest.java | 4 ++-- 5 files changed, 23 insertions(+), 36 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/util/{SystemHandler.java => SystemHandlerWrapper.java} (85%) 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 7731cca68c..43c01bf53a 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 @@ -15,12 +15,15 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; + /** - * An interface through which system clocks can be read. The {@link #DEFAULT} implementation must be - * used for all non-test cases. Implementations must also be able to create a {@link HandlerWrapper} - * which uses the underlying clock to schedule delayed messages. + * An interface through which system clocks can be read and {@link HandlerWrapper}s created. The + * {@link #DEFAULT} implementation must be used for all non-test cases. */ -public interface Clock extends HandlerWrapper.Factory { +public interface Clock { /** * Default {@link Clock} to use for all non-test cases. @@ -36,4 +39,12 @@ public interface Clock extends HandlerWrapper.Factory { * @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); + + /** + * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling + * messages. + * + * @see Handler#Handler(Looper, Handler.Callback). + */ + HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index b9f3a750d7..b101a5e199 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -18,29 +18,13 @@ package com.google.android.exoplayer2.util; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.Nullable; /** - * An interface to call through to an {@link Handler}. The {@link Factory#DEFAULT} factory must be - * used for all non-test cases. + * An interface to call through to a {@link Handler}. Instances must be created by calling {@link + * Clock#createHandler(Looper, Handler.Callback)} on {@link Clock#DEFAULT} for all non-test cases. */ public interface HandlerWrapper { - /** A factory for handler instances. */ - interface Factory { - - /** Default HandlerWrapper factory to use for all non-test cases. */ - Factory DEFAULT = new SystemHandler.Factory(); - - /** - * Creates a HandlerWrapper running a specified looper and using a specified callback for - * messages. - * - * @see Handler#Handler(Looper, Handler.Callback). - */ - HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); - } - /** @see Handler#getLooper(). */ Looper getLooper(); 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 8a5bdf549f..b24a38ea3c 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.support.annotation.Nullable; @@ -36,6 +37,6 @@ import android.support.annotation.Nullable; @Override public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { - return HandlerWrapper.Factory.DEFAULT.createHandler(looper, callback); + return new SystemHandlerWrapper(new Handler(looper, callback)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java similarity index 85% rename from library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index e99c626057..aa290d9313 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -15,25 +15,16 @@ */ package com.google.android.exoplayer2.util; -import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; import android.os.SystemClock; /** The standard implementation of {@link HandlerWrapper}. */ -/* package */ final class SystemHandler implements HandlerWrapper { - - /* package */ static final class Factory implements HandlerWrapper.Factory { - - @Override - public HandlerWrapper createHandler(Looper looper, Callback callback) { - return new SystemHandler(new android.os.Handler(looper, callback)); - } - } +/* package */ final class SystemHandlerWrapper implements HandlerWrapper { private final android.os.Handler handler; - private SystemHandler(android.os.Handler handler) { + public SystemHandlerWrapper(android.os.Handler handler) { this.handler = handler; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 3a5f3ccd7a..2298a2f0cc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -188,8 +189,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen player.addAudioDebugListener(this); player.addVideoDebugListener(this); player.setPlayWhenReady(true); - actionHandler = - HandlerWrapper.Factory.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); + actionHandler = Clock.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); // Schedule any pending actions. if (pendingSchedule != null) { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); From bf3d6028fac610ee3de0009abb7c602457f25b68 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:07:15 -0800 Subject: [PATCH 228/417] Make SsaDecoder more robust against malformed content Issue: #3645 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180559196 --- .../android/exoplayer2/text/ssa/SsaDecoder.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index eec4a1269c..0cb6f66898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -150,6 +150,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { break; } } + if (formatStartIndex == C.INDEX_UNSET + || formatEndIndex == C.INDEX_UNSET + || formatTextIndex == C.INDEX_UNSET) { + // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. + formatKeyCount = 0; + } } /** @@ -161,12 +167,17 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine); + Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); return; } String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) .split(",", formatKeyCount); + if (lineValues.length != formatKeyCount) { + Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); + return; + } + long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); From 2cc044ded10f548b5e7dd89922cd3a494f7898aa Mon Sep 17 00:00:00 2001 From: Alex Cohn Date: Thu, 4 Jan 2018 13:22:13 +0200 Subject: [PATCH 229/417] minimal fix to support NDK r16 --- extensions/vp9/README.md | 2 -- extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 649e4a6ee2..8dc4974430 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,8 +29,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. -Only versions up to NDK 15c are supported currently (see [#3520][]). - ``` NDK_PATH="" ``` diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index 5f058d0551..4aabf2379e 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -102,7 +102,7 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} + ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags="-isystem $ndk/sysroot/usr/include/arm-linux-androideabi -isystem $ndk/sysroot/usr/include" rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid From d3ba207a4b425b019423c0a2a6da81e7bdf6046b Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 3 Jan 2018 02:46:59 -0800 Subject: [PATCH 230/417] Refactor CacheDataSource Simplified and clarified the code. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180649983 --- .../upstream/cache/CacheDataSource.java | 91 +++++++++---------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index bb2a952b11..5eea140a8b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -97,7 +97,7 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheForUnsetLengthRequests; private DataSource currentDataSource; - private boolean currentRequestUnbounded; + private boolean readingUnknownLengthDataFromUpstream; private Uri uri; private int flags; private String key; @@ -202,7 +202,7 @@ public final class CacheDataSource implements DataSource { } } } - openNextSource(true); + openNextSource(); return bytesRemaining; } catch (IOException e) { handleBeforeThrow(e); @@ -229,15 +229,21 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else { - if (currentRequestUnbounded) { - // We only do unbounded requests to upstream and only when we don't know the actual stream - // length. So we reached the end of stream. - setContentLength(readPosition); - bytesRemaining = 0; + if (readingUnknownLengthDataFromUpstream) { + setCurrentDataSourceBytesRemaining(0); } closeCurrentSource(); if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { - if (openNextSource(false)) { + try { + openNextSource(); + } catch (IOException e) { + if (readingUnknownLengthDataFromUpstream && isCausedByPositionOutOfRange(e)) { + setCurrentDataSourceBytesRemaining(0); + } else { + throw e; + } + } + if (bytesRemaining != 0) { return read(buffer, offset, readLength); } } @@ -270,9 +276,8 @@ public final class CacheDataSource implements DataSource { * Opens the next source. If the cache contains data spanning the current read position then * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is * opened to read from the upstream source and write into the cache. - * @param initial Whether it is the initial open call. */ - private boolean openNextSource(boolean initial) throws IOException { + private void openNextSource() throws IOException { DataSpec dataSpec; CacheSpan span; if (currentRequestIgnoresCache) { @@ -323,48 +328,38 @@ public final class CacheDataSource implements DataSource { } } - currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; - boolean successful = false; - long currentBytesRemaining = 0; - try { - currentBytesRemaining = currentDataSource.open(dataSpec); - successful = true; - } catch (IOException e) { - // if this isn't the initial open call (we had read some bytes) and an unbounded range request - // failed because of POSITION_OUT_OF_RANGE then mute the exception. We are trying to find the - // end of the stream. - if (!initial && currentRequestUnbounded) { - Throwable cause = e; - while (cause != null) { - if (cause instanceof DataSourceException) { - int reason = ((DataSourceException) cause).reason; - if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { - e = null; - break; - } - } - cause = cause.getCause(); - } - } - if (e != null) { - throw e; - } - } + // If the request is unbounded it must be an upstream request. + readingUnknownLengthDataFromUpstream = dataSpec.length == C.LENGTH_UNSET; - // If we did an unbounded request (which means it's to upstream and - // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request - if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) { - bytesRemaining = currentBytesRemaining; - setContentLength(dataSpec.position + bytesRemaining); + long resolvedLength = currentDataSource.open(dataSpec); + if (readingUnknownLengthDataFromUpstream && resolvedLength != C.LENGTH_UNSET) { + setCurrentDataSourceBytesRemaining(resolvedLength); } - return successful; } - private void setContentLength(long length) throws IOException { - // If writing into cache - if (currentDataSource == cacheWriteDataSource) { - cache.setContentLength(key, length); + private static boolean isCausedByPositionOutOfRange(IOException e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DataSourceException) { + int reason = ((DataSourceException) cause).reason; + if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { + return true; + } + } + cause = cause.getCause(); } + return false; + } + + private void setCurrentDataSourceBytesRemaining(long bytesRemaining) throws IOException { + this.bytesRemaining = bytesRemaining; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition + bytesRemaining); + } + } + + private boolean isWritingToCache() { + return currentDataSource == cacheWriteDataSource; } private void closeCurrentSource() throws IOException { @@ -374,7 +369,7 @@ public final class CacheDataSource implements DataSource { try { currentDataSource.close(); currentDataSource = null; - currentRequestUnbounded = false; + readingUnknownLengthDataFromUpstream = false; } finally { if (lockedSpan != null) { cache.releaseHoleSpan(lockedSpan); From 7b9f71b44d0d8a290f6d4e6844a0c73e2a5502fe Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 Jan 2018 05:30:56 -0800 Subject: [PATCH 231/417] Don't kill the process if SimpleDecoder.decode throws. Issue: #3645 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180659855 --- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 13 +++++++--- .../ext/ffmpeg/FfmpegDecoderException.java | 3 +++ .../exoplayer2/ext/flac/FlacDecoder.java | 13 +++++++--- .../ext/flac/FlacDecoderException.java | 3 +++ .../exoplayer2/ext/opus/OpusDecoder.java | 13 +++++++--- .../exoplayer2/ext/vp9/VpxDecoder.java | 5 ++++ .../ext/vp9/VpxDecoderException.java | 6 ++--- .../audio/AudioDecoderException.java | 26 +++++++------------ .../exoplayer2/audio/DefaultAudioSink.java | 8 +++--- .../exoplayer2/decoder/SimpleDecoder.java | 21 ++++++++++++++- .../text/SimpleSubtitleDecoder.java | 5 ++++ .../audio/SimpleDecoderAudioRendererTest.java | 5 ++++ 12 files changed, 83 insertions(+), 38 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 8807738cfa..91bd82ab2a 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -69,18 +69,23 @@ import java.util.List; } @Override - public DecoderInputBuffer createInputBuffer() { + protected DecoderInputBuffer createInputBuffer() { return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); } @Override - public SimpleOutputBuffer createOutputBuffer() { + protected SimpleOutputBuffer createOutputBuffer() { return new SimpleOutputBuffer(this); } @Override - public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) { + return new FfmpegDecoderException("Unexpected decode error", error); + } + + @Override + protected FfmpegDecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { nativeContext = ffmpegReset(nativeContext, extraData); if (nativeContext == 0) { diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java index b4cf327198..d6b5a62450 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java @@ -26,4 +26,7 @@ public final class FfmpegDecoderException extends AudioDecoderException { super(message); } + /* package */ FfmpegDecoderException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 3ecccd8246..15d294a35a 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -70,18 +70,23 @@ import java.util.List; } @Override - public DecoderInputBuffer createInputBuffer() { + protected DecoderInputBuffer createInputBuffer() { return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } @Override - public SimpleOutputBuffer createOutputBuffer() { + protected SimpleOutputBuffer createOutputBuffer() { return new SimpleOutputBuffer(this); } @Override - public FlacDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected FlacDecoderException createUnexpectedDecodeException(Throwable error) { + return new FlacDecoderException("Unexpected decode error", error); + } + + @Override + protected FlacDecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { decoderJni.flush(); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java index 2bdff62935..95d7f87c05 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java @@ -26,4 +26,7 @@ public final class FlacDecoderException extends AudioDecoderException { super(message); } + /* package */ FlacDecoderException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index b4a4622346..f8ec477b88 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -135,18 +135,23 @@ import java.util.List; } @Override - public DecoderInputBuffer createInputBuffer() { + protected DecoderInputBuffer createInputBuffer() { return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); } @Override - public SimpleOutputBuffer createOutputBuffer() { + protected SimpleOutputBuffer createOutputBuffer() { return new SimpleOutputBuffer(this); } @Override - public OpusDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected OpusDecoderException createUnexpectedDecodeException(Throwable error) { + return new OpusDecoderException("Unexpected decode error", error); + } + + @Override + protected OpusDecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { opusReset(nativeDecoderContext); // When seeking to 0, skip number of samples as specified in opus header. When seeking to diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 6a15023c0b..6f8c0a1918 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -99,6 +99,11 @@ import java.nio.ByteBuffer; super.releaseOutputBuffer(buffer); } + @Override + protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { + return new VpxDecoderException("Unexpected decode error", error); + } + @Override protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java index 5f43b503ac..8de14629d3 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java @@ -15,10 +15,8 @@ */ package com.google.android.exoplayer2.ext.vp9; -/** - * Thrown when a libvpx decoder error occurs. - */ -public class VpxDecoderException extends Exception { +/** Thrown when a libvpx decoder error occurs. */ +public final class VpxDecoderException extends Exception { /* package */ VpxDecoderException(String message) { super(message); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java index b5ee052924..ac4f632d62 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java @@ -15,27 +15,21 @@ */ package com.google.android.exoplayer2.audio; -/** - * Thrown when an audio decoder error occurs. - */ -public abstract class AudioDecoderException extends Exception { +/** Thrown when an audio decoder error occurs. */ +public class AudioDecoderException extends Exception { - /** - * @param detailMessage The detail message for this exception. - */ - public AudioDecoderException(String detailMessage) { - super(detailMessage); + /** @param message The detail message for this exception. */ + public AudioDecoderException(String message) { + super(message); } /** - * @param detailMessage The detail message for this exception. - * @param cause the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is - * permitted, and indicates that the cause is nonexistent or - * unknown.) + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. */ - public AudioDecoderException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + public AudioDecoderException(String message, Throwable cause) { + super(message, cause); } } 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 ab4564e2c3..b9a0b8236f 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 @@ -55,11 +55,9 @@ public final class DefaultAudioSink implements AudioSink { */ public static final class InvalidAudioTrackTimestampException extends RuntimeException { - /** - * @param detailMessage The detail message for this exception. - */ - public InvalidAudioTrackTimestampException(String detailMessage) { - super(detailMessage); + /** @param message The detail message for this exception. */ + public InvalidAudioTrackTimestampException(String message) { + super(message); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 1d380ef858..68089d7b41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -219,7 +219,18 @@ public abstract class SimpleDecoder Date: Wed, 3 Jan 2018 05:39:57 -0800 Subject: [PATCH 232/417] Update Cronet extension readme on how to enable Java 8 features. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180660349 --- extensions/cronet/README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 66da774978..ea84b602db 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -19,10 +19,20 @@ and enable the extension: 1. Copy the three jar files into the `libs` directory of this extension 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension - -* In your `settings.gradle` file, add - `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that - applies `core_settings.gradle`. +1. In your `settings.gradle` file, add + `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that + applies `core_settings.gradle`. +1. In all `build.gradle` files where this extension is linked as a dependency, + add + ``` + android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } + ``` + to enable Java 8 features required by the Cronet library. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android From a29fb7b989c676967f806cdff572855203097894 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 Jan 2018 05:59:18 -0800 Subject: [PATCH 233/417] Update release notes to cherry-pick 32-bit WAVE support ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180661355 --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9f48db0be8..5d3d00a544 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,8 +36,6 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. -* Add support for extracting 32-bit WAVE files - ([#3379](https://github.com/google/ExoPlayer/issues/3379)). ### 2.6.1 ### @@ -62,6 +60,8 @@ * Audio: * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). * Fix handling of playback parameter changes while paused when followed by a From 2bc734afec7b8f729f19eae495b13de2dd481c40 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 3 Jan 2018 06:52:55 -0800 Subject: [PATCH 234/417] Replace message delay with send at time in playback loop. This removes the need to calculate the time needed to run the doSomeWork method. Consequently, we can use both the real Clock/Handler and the FakeClock without changing the way the playback loop works and without violating the interfaces of Clock or Handler. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180665647 --- .../exoplayer2/ExoPlayerImplInternal.java | 6 ++- .../google/android/exoplayer2/util/Clock.java | 11 +++-- .../exoplayer2/util/HandlerWrapper.java | 14 +------ .../android/exoplayer2/util/SystemClock.java | 5 +++ .../exoplayer2/util/SystemHandlerWrapper.java | 11 +---- .../exoplayer2/testutil/FakeClock.java | 42 ++++++++++--------- 6 files changed, 40 insertions(+), 49 deletions(-) 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 65f43ae684..8fd508a2f0 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 @@ -117,6 +117,7 @@ import java.util.Collections; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList customMessageInfos; + private final Clock clock; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -159,6 +160,7 @@ import java.util.Collections; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; this.player = player; + this.clock = clock; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -541,7 +543,7 @@ import java.util.Collections; } private void doSomeWork() throws ExoPlaybackException, IOException { - long operationStartTimeMs = SystemClock.elapsedRealtime(); + long operationStartTimeMs = clock.uptimeMillis(); updatePeriods(); if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. @@ -632,7 +634,7 @@ import java.util.Collections; private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { handler.removeMessages(MSG_DO_SOME_WORK); - handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, intervalMs, thisOperationStartTimeMs); + handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs); } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { 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 43c01bf53a..dced6752eb 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,14 +30,13 @@ public interface Clock { */ Clock DEFAULT = new SystemClock(); - /** - * @see android.os.SystemClock#elapsedRealtime() - */ + /** @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); - /** - * @see android.os.SystemClock#sleep(long) - */ + /** @see android.os.SystemClock#uptimeMillis() */ + long uptimeMillis(); + + /** @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index b101a5e199..3ce93f9370 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -43,18 +43,8 @@ public interface HandlerWrapper { /** @see Handler#sendEmptyMessage(int). */ boolean sendEmptyMessage(int what); - /** - * Variant of {@code Handler#sendEmptyMessageDelayed(int, long)} which also takes a reference time - * measured by {@code android.os.SystemClock#elapsedRealtime()} to which the delay is added. - * - * @param what The message identifier. - * @param delayMs The delay in milliseconds to send the message. This delay is added to the {@code - * referenceTimeMs}. - * @param referenceTimeMs The time which the delay is added to. Always measured with {@code - * android.os.SystemClock#elapsedRealtime()}. - * @return Whether the message was successfully enqueued on the Handler thread. - */ - boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs); + /** @see Handler#sendEmptyMessageAtTime(int, long). */ + boolean sendEmptyMessageAtTime(int what, long uptimeMs); /** @see Handler#removeMessages(int). */ void removeMessages(int what); 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 b24a38ea3c..72d3df46e1 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 @@ -30,6 +30,11 @@ import android.support.annotation.Nullable; return android.os.SystemClock.elapsedRealtime(); } + @Override + public long uptimeMillis() { + return android.os.SystemClock.uptimeMillis(); + } + @Override public void sleep(long sleepTimeMs) { android.os.SystemClock.sleep(sleepTimeMs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index aa290d9313..ee469a5b2a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util; import android.os.Looper; import android.os.Message; -import android.os.SystemClock; /** The standard implementation of {@link HandlerWrapper}. */ /* package */ final class SystemHandlerWrapper implements HandlerWrapper { @@ -59,14 +58,8 @@ import android.os.SystemClock; } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { - long targetMessageTime = referenceTimeMs + delayMs; - long remainingDelayMs = targetMessageTime - SystemClock.elapsedRealtime(); - if (remainingDelayMs <= 0) { - return handler.sendEmptyMessage(what); - } else { - return handler.sendEmptyMessageDelayed(what, remainingDelayMs); - } + public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { + return handler.sendEmptyMessageAtTime(what, uptimeMs); } @Override 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 49656eef99..a591546613 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 @@ -67,6 +67,11 @@ public class FakeClock implements Clock { return currentTimeMs; } + @Override + public long uptimeMillis() { + return elapsedRealtime(); + } + @Override public synchronized void sleep(long sleepTimeMs) { if (sleepTimeMs <= 0) { @@ -90,15 +95,23 @@ public class FakeClock implements Clock { } /** Adds a handler post to list of pending messages. */ - protected synchronized void addDelayedHandlerMessage( - HandlerWrapper handler, Runnable runnable, long delayMs) { - handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, runnable)); + protected synchronized boolean addHandlerMessageAtTime( + HandlerWrapper handler, Runnable runnable, long timeMs) { + if (timeMs <= currentTimeMs) { + return handler.post(runnable); + } + handlerMessages.add(new HandlerMessageData(timeMs, handler, runnable)); + return true; } /** Adds an empty handler message to list of pending messages. */ - protected synchronized void addDelayedHandlerMessage( - HandlerWrapper handler, int message, long delayMs) { - handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, message)); + protected synchronized boolean addHandlerMessageAtTime( + HandlerWrapper handler, int message, long timeMs) { + if (timeMs <= currentTimeMs) { + return handler.sendEmptyMessage(message); + } + handlerMessages.add(new HandlerMessageData(timeMs, handler, message)); + return true; } /** Message data saved to send messages or execute runnables at a later time on a Handler. */ @@ -177,14 +190,8 @@ public class FakeClock implements Clock { } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { - // Ignore referenceTimeMs measured by SystemClock and just send with requested delay. - if (delayMs <= 0) { - return handler.sendEmptyMessage(what); - } else { - addDelayedHandlerMessage(this, what, delayMs); - return true; - } + public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { + return addHandlerMessageAtTime(this, what, uptimeMs); } @Override @@ -204,12 +211,7 @@ public class FakeClock implements Clock { @Override public boolean postDelayed(Runnable runnable, long delayMs) { - if (delayMs <= 0) { - return handler.post(runnable); - } else { - addDelayedHandlerMessage(this, runnable, delayMs); - return true; - } + return addHandlerMessageAtTime(this, runnable, uptimeMillis() + delayMs); } } } From 7314e9bddc2635c9426cbe237b237f35f24bb15b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 Jan 2018 08:44:56 -0800 Subject: [PATCH 235/417] DRM fixes - Parse multiple kids from default_KID. It's specified as a whitespace separated list of UUIDs rather than a single UUID. - Opportunistically proceed with playback in cases where the manifest only defines a single SchemeData with the common PSSH UUID. In such cases the manifest isn't saying anything about which specific DRM schemes it supports. Issue: #3630 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180675056 --- RELEASENOTES.md | 3 +++ .../exoplayer2/drm/DefaultDrmSessionManager.java | 15 ++++++++++++--- .../source/dash/manifest/DashManifestParser.java | 11 ++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5d3d00a544..25e4e841e3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,9 @@ positions. * Note: `SeekParameters` are only currently effective when playing `ExtractorMediaSource`s (i.e. progressive streams). +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 08defdccee..9c134970ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -23,6 +23,7 @@ import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; @@ -87,7 +88,6 @@ public class DefaultDrmSessionManager implements DrmSe * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; /** Determines the action to be done after a session acquired. */ @Retention(RetentionPolicy.SOURCE) @@ -109,6 +109,9 @@ public class DefaultDrmSessionManager implements DrmSe /** Number of times to retry for initial provisioning and key request for reporting error. */ public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; + private static final String TAG = "DrmSessionManager"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private final UUID uuid; private final ExoMediaDrm mediaDrm; private final MediaDrmCallback callback; @@ -350,8 +353,14 @@ public class DefaultDrmSessionManager implements DrmSe public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { SchemeData schemeData = getSchemeData(drmInitData, uuid, true); if (schemeData == null) { - // No data for this manager's scheme. - return false; + if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { + // Assume scheme specific data will be added before the session is opened. + Log.w( + TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); + } else { + // No data for this manager's scheme. + return false; + } } String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { 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 73d234fa72..bda2a1fb85 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 @@ -365,9 +365,14 @@ public class DashManifestParser extends DefaultHandler case "urn:mpeg:dash:mp4protection:2011": schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); - if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { - UUID keyId = UUID.fromString(defaultKid); - data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + if (!TextUtils.isEmpty(defaultKid) + && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + String[] defaultKidStrings = defaultKid.split("\\s+"); + UUID[] defaultKids = new UUID[defaultKidStrings.length]; + for (int i = 0; i < defaultKidStrings.length; i++) { + defaultKids[i] = UUID.fromString(defaultKidStrings[i]); + } + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, defaultKids, null); uuid = C.COMMON_PSSH_UUID; } break; From 8e8e53c42d7a63ee5f6703d86902ebb102c10af5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 3 Jan 2018 09:18:11 -0800 Subject: [PATCH 236/417] Add support for Dolby TrueHD passthrough Issue: #2147 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180678595 --- RELEASENOTES.md | 2 + .../java/com/google/android/exoplayer2/C.java | 63 ++++----- .../android/exoplayer2/audio/Ac3Util.java | 52 ++++++- .../exoplayer2/audio/DefaultAudioSink.java | 15 +- .../extractor/mkv/MatroskaExtractor.java | 131 ++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 2 + 6 files changed, 211 insertions(+), 54 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 25e4e841e3..4679a0b376 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,8 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. +* Audio: Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). ### 2.6.1 ### 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 6a35c0c5e8..d6e61c12b1 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 @@ -122,13 +122,22 @@ public final class C { */ public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; - /** - * Represents an audio encoding, or an invalid or unset value. - */ + /** Represents an audio encoding, or an invalid or unset value. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, - ENCODING_DTS, ENCODING_DTS_HD}) + @IntDef({ + Format.NO_VALUE, + ENCODING_INVALID, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_24BIT, + ENCODING_PCM_32BIT, + ENCODING_PCM_FLOAT, + ENCODING_AC3, + ENCODING_E_AC3, + ENCODING_DTS, + ENCODING_DTS_HD, + ENCODING_DOLBY_TRUEHD + }) public @interface Encoding {} /** @@ -138,46 +147,28 @@ public final class C { @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) public @interface PcmEncoding {} - /** - * @see AudioFormat#ENCODING_INVALID - */ + /** @see AudioFormat#ENCODING_INVALID */ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; - /** - * @see AudioFormat#ENCODING_PCM_8BIT - */ + /** @see AudioFormat#ENCODING_PCM_8BIT */ public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; - /** - * @see AudioFormat#ENCODING_PCM_16BIT - */ + /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; - /** - * PCM encoding with 24 bits per sample. - */ + /** PCM encoding with 24 bits per sample. */ public static final int ENCODING_PCM_24BIT = 0x80000000; - /** - * PCM encoding with 32 bits per sample. - */ + /** PCM encoding with 32 bits per sample. */ public static final int ENCODING_PCM_32BIT = 0x40000000; - /** - * @see AudioFormat#ENCODING_PCM_FLOAT - */ + /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** - * @see AudioFormat#ENCODING_AC3 - */ + /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; - /** - * @see AudioFormat#ENCODING_E_AC3 - */ + /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; - /** - * @see AudioFormat#ENCODING_DTS - */ + /** @see AudioFormat#ENCODING_DTS */ public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; - /** - * @see AudioFormat#ENCODING_DTS_HD - */ + /** @see AudioFormat#ENCODING_DTS_HD */ public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; + /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */ + public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; /** * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e9ffab7ace..5797e73740 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -27,9 +27,7 @@ import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; -/** - * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams. - */ +/** Utility methods for parsing Dolby TrueHD and (E-)AC3 syncframes. */ public final class Ac3Util { /** @@ -93,6 +91,17 @@ public final class Ac3Util { } + /** + * The number of samples to store in each output chunk when rechunking TrueHD streams. The number + * of samples extracted from the container corresponding to one syncframe must be an integer + * multiple of this value. + */ + public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 8; + /** + * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count. + */ + public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 12; + /** * The number of new samples per (E-)AC-3 audio block. */ @@ -441,6 +450,43 @@ public final class Ac3Util { : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); } + /** + * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the + * buffer is not the start of a syncframe. + * + * @param syncframe The bytes from which to read the syncframe. Must be at least {@link + * #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes long. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer doesn't + * contain the start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { + // TODO: Link to specification if available. + if (syncframe[4] != (byte) 0xF8 + || syncframe[5] != (byte) 0x72 + || syncframe[6] != (byte) 0x6F + || syncframe[7] != (byte) 0xBA) { + return 0; + } + return 40 << (syncframe[8] & 7); + } + + /** + * Reads the number of audio samples represented by the given TrueHD syncframe, or 0 if the buffer + * is not the start of a syncframe. The buffer's position is not modified. + * + * @param buffer The {@link ByteBuffer} from which to read the syncframe. Must have at least + * {@link #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes remaining. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer is not the + * start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer) { + // TODO: Link to specification if available. + if (buffer.getInt(buffer.position() + 4) != 0xBA6F72F8) { + return 0; + } + return 40 << (buffer.get(buffer.position() + 8) & 0x07); + } + private static int getAc3SyncframeSize(int fscod, int frmsizecod) { int halfFrmsizecod = frmsizecod / 2; if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0 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 b9a0b8236f..e3bf72c541 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 @@ -446,9 +446,12 @@ public final class DefaultAudioSink implements AudioSink { if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { + } else if (outputEncoding == C.ENCODING_DTS) { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); + } else /* outputEncoding == C.ENCODING_DTS_HD || outputEncoding == C.ENCODING_DOLBY_TRUEHD*/ { + // HD passthrough requires a larger buffer to avoid underrun. + bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND); } } bufferSizeUs = @@ -580,6 +583,13 @@ public final class DefaultAudioSink implements AudioSink { if (!isInputPcm && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); + if (framesPerEncodedSample == 0) { + // We still don't know the number of frames per sample, so drop the buffer. + // For TrueHD this can occur after some seek operations, as not every sample starts with + // a syncframe header. If we chunked samples together so the extracted samples always + // started with a syncframe header, the chunks would be too large. + return true; + } } if (drainingPlaybackParameters != null) { @@ -1225,6 +1235,9 @@ public final class DefaultAudioSink implements AudioSink { return Ac3Util.getAc3SyncframeAudioSampleCount(); } else if (encoding == C.ENCODING_E_AC3) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { + return Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT; } else { throw new IllegalStateException("Unexpected audio encoding: " + encoding); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 4b0bbda275..0eb7009c47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; @@ -32,6 +34,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -413,6 +416,9 @@ public final class MatroskaExtractor implements Extractor { reader.reset(); varintReader.reset(); resetSample(); + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).reset(); + } } @Override @@ -431,7 +437,13 @@ public final class MatroskaExtractor implements Extractor { return Extractor.RESULT_SEEK; } } - return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT; + if (!continueReading) { + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).outputPendingSampleMetadata(); + } + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; } /* package */ int getElementType(int id) { @@ -1077,14 +1089,26 @@ public final class MatroskaExtractor implements Extractor { } private void commitSampleToOutput(Track track, long timeUs) { - if (CODEC_ID_SUBRIP.equals(track.codecId)) { - commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY); - } else if (CODEC_ID_ASS.equals(track.codecId)) { - commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET, - SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY); + if (track.trueHdSampleRechunker != null) { + track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + } else { + if (CODEC_ID_SUBRIP.equals(track.codecId)) { + commitSubtitleSample( + track, + SUBRIP_TIMECODE_FORMAT, + SUBRIP_PREFIX_END_TIMECODE_OFFSET, + SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, + SUBRIP_TIMECODE_EMPTY); + } else if (CODEC_ID_ASS.equals(track.codecId)) { + commitSubtitleSample( + track, + SSA_TIMECODE_FORMAT, + SSA_PREFIX_END_TIMECODE_OFFSET, + SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, + SSA_TIMECODE_EMPTY); + } + track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); sampleRead = true; resetSample(); } @@ -1251,6 +1275,10 @@ public final class MatroskaExtractor implements Extractor { } } } else { + if (track.trueHdSampleRechunker != null) { + Assertions.checkState(sampleStrippedBytes.limit() == 0); + track.trueHdSampleRechunker.startSample(input, blockFlags, size); + } while (sampleBytesRead < size) { readToOutput(input, output, size - sampleBytesRead); } @@ -1510,7 +1538,70 @@ public final class MatroskaExtractor implements Extractor { throws IOException, InterruptedException { MatroskaExtractor.this.binaryElement(id, contentsSize, input); } + } + /** + * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples. + */ + private static final class TrueHdSampleRechunker { + + private final byte[] syncframePrefix; + + private boolean foundSyncframe; + private int sampleCount; + private int chunkSize; + private long timeUs; + private @C.BufferFlags int blockFlags; + + public TrueHdSampleRechunker() { + syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; + } + + public void reset() { + foundSyncframe = false; + } + + public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) + throws IOException, InterruptedException { + if (!foundSyncframe) { + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if ((Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == C.INDEX_UNSET)) { + return; + } + foundSyncframe = true; + sampleCount = 0; + } + if (sampleCount == 0) { + // This is the first sample in the chunk, so reset the block flags and chunk size. + this.blockFlags = blockFlags; + chunkSize = 0; + } + chunkSize += size; + } + + public void sampleMetadata(Track track, long timeUs) { + if (!foundSyncframe) { + return; + } + if (sampleCount++ == 0) { + // This is the first sample in the chunk, so update the timestamp. + this.timeUs = timeUs; + } + if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + // We haven't read enough samples to output a chunk. + return; + } + track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); + sampleCount = 0; + } + + public void outputPendingSampleMetadata(Track track) { + if (foundSyncframe && sampleCount > 0) { + track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); + sampleCount = 0; + } + } } private static final class Track { @@ -1573,6 +1664,7 @@ public final class MatroskaExtractor implements Extractor { public int sampleRate = 8000; public long codecDelayNs = 0; public long seekPreRollNs = 0; + @Nullable public TrueHdSampleRechunker trueHdSampleRechunker; // Text elements. public boolean flagForced; @@ -1583,9 +1675,7 @@ public final class MatroskaExtractor implements Extractor { public TrackOutput output; public int nalUnitLengthFieldLength; - /** - * Initializes the track with an output. - */ + /** Initializes the track with an output. */ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException { String mimeType; int maxInputSize = Format.NO_VALUE; @@ -1669,6 +1759,7 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_TRUEHD: mimeType = MimeTypes.AUDIO_TRUEHD; + trueHdSampleRechunker = new TrueHdSampleRechunker(); break; case CODEC_ID_DTS: case CODEC_ID_DTS_EXPRESS: @@ -1786,9 +1877,21 @@ public final class MatroskaExtractor implements Extractor { this.output.format(format); } - /** - * Returns the HDR Static Info as defined in CTA-861.3. - */ + /** Forces any pending sample metadata to be flushed to the output. */ + public void outputPendingSampleMetadata() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.outputPendingSampleMetadata(this); + } + } + + /** Resets any state stored in the track in response to a seek. */ + public void reset() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.reset(); + } + } + + /** Returns the HDR Static Info as defined in CTA-861.3. */ private byte[] getHdrStaticInfo() { // Are all fields present. if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 8307e998a0..3e65a754e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -264,6 +264,8 @@ public final class MimeTypes { return C.ENCODING_DTS; case MimeTypes.AUDIO_DTS_HD: return C.ENCODING_DTS_HD; + case MimeTypes.AUDIO_TRUEHD: + return C.ENCODING_DOLBY_TRUEHD; default: return C.ENCODING_INVALID; } From a314db04ad56999ab9bbcd9bfda984cf905c6eec Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jan 2018 03:13:32 -0800 Subject: [PATCH 237/417] Reformat UI classes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180777553 --- .../exoplayer2/ui/PlaybackControlView.java | 239 ++++++++---------- .../exoplayer2/ui/SimpleExoPlayerView.java | 141 +++++------ 2 files changed, 175 insertions(+), 205 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 7659dff9c6..fefbb0797a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -42,127 +42,118 @@ import java.util.Locale; /** * A view for controlling {@link Player} instances. - *

      - * A PlaybackControlView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

      A PlaybackControlView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. * *

      Attributes

      + * * The following attributes can be set on a PlaybackControlView when used in a layout XML file: + * *

      + * *

        *
      • {@code show_timeout} - The time between the last user interaction and the controls * being automatically hidden, in milliseconds. Use zero if the controls should not * automatically timeout. *
          - *
        • Corresponding method: {@link #setShowTimeoutMs(int)}
        • - *
        • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
        • + *
        • Corresponding method: {@link #setShowTimeoutMs(int)} + *
        • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} *
        - *
      • *
      • {@code rewind_increment} - The duration of the rewind applied when the user taps the * rewind button, in milliseconds. Use zero to disable the rewind button. *
          - *
        • Corresponding method: {@link #setRewindIncrementMs(int)}
        • - *
        • Default: {@link #DEFAULT_REWIND_MS}
        • + *
        • Corresponding method: {@link #setRewindIncrementMs(int)} + *
        • Default: {@link #DEFAULT_REWIND_MS} *
        - *
      • *
      • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. *
          - *
        • Corresponding method: {@link #setFastForwardIncrementMs(int)}
        • - *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS}
        • + *
        • Corresponding method: {@link #setFastForwardIncrementMs(int)} + *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} *
        - *
      • *
      • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat - * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, - * {@code all}, or {@code one|all}. + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, + * or {@code one|all}. *
          - *
        • Corresponding method: {@link #setRepeatToggleModes(int)}
        • - *
        • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
        • + *
        • Corresponding method: {@link #setRepeatToggleModes(int)} + *
        • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES} *
        - *
      • *
      • {@code show_shuffle_button} - Whether the shuffle button is shown. *
          - *
        • Corresponding method: {@link #setShowShuffleButton(boolean)}
        • - *
        • Default: false
        • + *
        • Corresponding method: {@link #setShowShuffleButton(boolean)} + *
        • Default: false *
        - *
      • *
      • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See * below for more details. *
          - *
        • Corresponding method: None
        • - *
        • Default: {@code R.id.exo_playback_control_view}
        • + *
        • Corresponding method: None + *
        • Default: {@code R.id.exo_playback_control_view} *
        - *
      • *
      * *

      Overriding the layout file

      + * * To customize the layout of PlaybackControlView throughout your app, or just for certain * configurations, you can define {@code exo_playback_control_view.xml} layout files in your * application {@code res/layout*} directories. These layouts will override the one provided by the * ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and * binds its children by looking for the following ids: + * *

      + * *

        *
      • {@code exo_play} - The play button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_pause} - The pause button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_ffwd} - The fast forward button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_rew} - The rewind button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_prev} - The previous track button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_next} - The next track button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_repeat_toggle} - The repeat toggle button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_shuffle} - The shuffle button. *
          - *
        • Type: {@link View}
        • + *
        • Type: {@link View} *
        - *
      • *
      • {@code exo_position} - Text view displaying the current playback position. *
          - *
        • Type: {@link TextView}
        • + *
        • Type: {@link TextView} *
        - *
      • *
      • {@code exo_duration} - Text view displaying the current media duration. *
          - *
        • Type: {@link TextView}
        • + *
        • Type: {@link TextView} *
        - *
      • *
      • {@code exo_progress} - Time bar that's updated during playback and allows seeking. *
          - *
        • Type: {@link TimeBar}
        • + *
        • Type: {@link TimeBar} *
        - *
      • *
      - *

      - * All child views are optional and so can be omitted if not required, however where defined they + * + *

      All child views are optional and so can be omitted if not required, however where defined they * must be of the expected type. * *

      Specifying a custom layout file

      + * * Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of * PlaybackControlView throughout your application. It's also possible to customize the layout for a * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} @@ -175,15 +166,11 @@ public class PlaybackControlView extends FrameLayout { ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); } - /** - * @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. - */ + /** @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. */ @Deprecated public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} - /** - * Listener to be notified about changes of the visibility of the UI control. - */ + /** Listener to be notified about changes of the visibility of the UI control. */ public interface VisibilityListener { /** @@ -192,38 +179,25 @@ public class PlaybackControlView extends FrameLayout { * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. */ void onVisibilityChange(int visibility); - } private static final class DefaultControlDispatcher extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} - /** - * @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. - */ + /** @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ @Deprecated public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); - /** - * The default fast forward increment, in milliseconds. - */ + /** The default fast forward increment, in milliseconds. */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; - /** - * The default rewind increment, in milliseconds. - */ + /** The default rewind increment, in milliseconds. */ public static final int DEFAULT_REWIND_MS = 5000; - /** - * The default show timeout, in milliseconds. - */ + /** The default show timeout, in milliseconds. */ public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; - /** - * The default repeat toggle modes. - */ + /** The default repeat toggle modes. */ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; - /** - * The maximum number of windows that can be shown in a multi-window time bar. - */ + /** The maximum number of windows that can be shown in a multi-window time bar. */ public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; @@ -271,19 +245,21 @@ public class PlaybackControlView extends FrameLayout { private long[] extraAdGroupTimesMs; private boolean[] extraPlayedAdGroups; - private final Runnable updateProgressAction = new Runnable() { - @Override - public void run() { - updateProgress(); - } - }; + private final Runnable updateProgressAction = + new Runnable() { + @Override + public void run() { + updateProgress(); + } + }; - private final Runnable hideAction = new Runnable() { - @Override - public void run() { - hide(); - } - }; + private final Runnable hideAction = + new Runnable() { + @Override + public void run() { + hide(); + } + }; public PlaybackControlView(Context context) { this(context, null); @@ -297,8 +273,8 @@ public class PlaybackControlView extends FrameLayout { this(context, attrs, defStyleAttr, attrs); } - public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr, - AttributeSet playbackAttrs) { + public PlaybackControlView( + Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; @@ -307,18 +283,21 @@ public class PlaybackControlView extends FrameLayout { repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; showShuffleButton = false; if (playbackAttrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs, - R.styleable.PlaybackControlView, 0, 0); + TypedArray a = + context + .getTheme() + .obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); try { rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); - fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment, - fastForwardMs); + fastForwardMs = + a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); - controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, - controllerLayoutId); + controllerLayoutId = + a.getResourceId( + R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); - showShuffleButton = a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, - showShuffleButton); + showShuffleButton = + a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, showShuffleButton); } finally { a.recycle(); } @@ -379,17 +358,17 @@ public class PlaybackControlView extends FrameLayout { repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); - repeatOffButtonContentDescription = resources.getString( - R.string.exo_controls_repeat_off_description); - repeatOneButtonContentDescription = resources.getString( - R.string.exo_controls_repeat_one_description); - repeatAllButtonContentDescription = resources.getString( - R.string.exo_controls_repeat_all_description); + repeatOffButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_all_description); } @SuppressWarnings("ResourceType") - private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(TypedArray a, - @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( + TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); } @@ -422,9 +401,9 @@ public class PlaybackControlView extends FrameLayout { /** * Sets whether the time bar should show all windows, as opposed to just the current one. If the - * timeline has a period with unknown duration or more than - * {@link #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a - * single window. + * timeline has a period with unknown duration or more than {@link + * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single + * window. * * @param showMultiWindowTimeBar Whether the time bar should show all windows. */ @@ -443,8 +422,8 @@ public class PlaybackControlView extends FrameLayout { * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad * markers. */ - public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs, - @Nullable boolean[] extraPlayedAdGroups) { + public void setExtraAdGroupMarkers( + @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { if (extraAdGroupTimesMs == null) { this.extraAdGroupTimesMs = new long[0]; this.extraPlayedAdGroups = new boolean[0]; @@ -473,8 +452,10 @@ public class PlaybackControlView extends FrameLayout { */ public void setControlDispatcher( @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { - this.controlDispatcher = controlDispatcher == null - ? new com.google.android.exoplayer2.DefaultControlDispatcher() : controlDispatcher; + this.controlDispatcher = + controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() + : controlDispatcher; } /** @@ -556,9 +537,7 @@ public class PlaybackControlView extends FrameLayout { } } - /** - * Returns whether the shuffle button is shown. - */ + /** Returns whether the shuffle button is shown. */ public boolean getShowShuffleButton() { return showShuffleButton; } @@ -590,9 +569,7 @@ public class PlaybackControlView extends FrameLayout { hideAfterTimeout(); } - /** - * Hides the controller. - */ + /** Hides the controller. */ public void hide() { if (isVisible()) { setVisibility(GONE); @@ -605,9 +582,7 @@ public class PlaybackControlView extends FrameLayout { } } - /** - * Returns whether the controller is currently visible. - */ + /** Returns whether the controller is currently visible. */ public boolean isVisible() { return getVisibility() == VISIBLE; } @@ -664,8 +639,8 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = isSeekable || !window.isDynamic - || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enablePrevious = + isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; } setButtonEnabled(enablePrevious, previousButton); @@ -728,8 +703,8 @@ public class PlaybackControlView extends FrameLayout { if (player == null) { return; } - multiWindowTimeBar = showMultiWindowTimeBar - && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); + multiWindowTimeBar = + showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); } private void updateProgress() { @@ -836,8 +811,8 @@ public class PlaybackControlView extends FrameLayout { if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { mediaTimeDelayMs += mediaTimeUpdatePeriodMs; } - delayMs = playbackSpeed == 1 ? mediaTimeDelayMs - : (long) (mediaTimeDelayMs / playbackSpeed); + delayMs = + playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); } else { delayMs = 200; } @@ -876,7 +851,7 @@ public class PlaybackControlView extends FrameLayout { int previousWindowIndex = player.getPreviousWindowIndex(); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { + || (window.isDynamic && !window.isSeekable))) { seekTo(previousWindowIndex, C.TIME_UNSET); } else { seekTo(0); @@ -1054,8 +1029,8 @@ public class PlaybackControlView extends FrameLayout { return true; } - private final class ComponentListener extends Player.DefaultEventListener implements - TimeBar.OnScrubListener, OnClickListener { + private final class ComponentListener extends Player.DefaultEventListener + implements TimeBar.OnScrubListener, OnClickListener { @Override public void onScrubStart(TimeBar timeBar, long position) { @@ -1104,8 +1079,8 @@ public class PlaybackControlView extends FrameLayout { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); @@ -1127,15 +1102,13 @@ public class PlaybackControlView extends FrameLayout { } else if (pauseButton == view) { controlDispatcher.dispatchSetPlayWhenReady(player, false); } else if (repeatToggleButton == view) { - controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode( - player.getRepeatMode(), repeatToggleModes)); + controlDispatcher.dispatchSetRepeatMode( + player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); } else if (shuffleButton == view) { controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); } } hideAfterTimeout(); } - } - } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index c5a4bc8086..def8925ec3 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -268,26 +268,26 @@ public final class SimpleExoPlayerView extends FrameLayout { boolean controllerAutoShow = true; boolean controllerHideDuringAds = true; if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, - R.styleable.SimpleExoPlayerView, 0, 0); + TypedArray a = + context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); - shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, - shutterColor); - playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, - playerLayoutId); + shutterColor = + a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); + playerLayoutId = + a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); - defaultArtworkId = a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, - defaultArtworkId); + defaultArtworkId = + a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, defaultArtworkId); useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); - controllerShowTimeoutMs = a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, - controllerShowTimeoutMs); - controllerHideOnTouch = a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, - controllerHideOnTouch); - controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, - controllerAutoShow); + controllerShowTimeoutMs = + a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, controllerShowTimeoutMs); + controllerHideOnTouch = + a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = + a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); controllerHideDuringAds = a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); } finally { @@ -313,10 +313,13 @@ public final class SimpleExoPlayerView extends FrameLayout { // Create a surface view and insert it into the content frame, if there is one. if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context) - : new SurfaceView(context); + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = + surfaceType == SURFACE_TYPE_TEXTURE_VIEW + ? new TextureView(context) + : new SurfaceView(context); surfaceView.setLayoutParams(params); contentFrame.addView(surfaceView, 0); } else { @@ -372,8 +375,10 @@ public final class SimpleExoPlayerView extends FrameLayout { * @param oldPlayerView The old view to detach from the player. * @param newPlayerView The new view to attach to the player. */ - public static void switchTargetView(@NonNull SimpleExoPlayer player, - @Nullable SimpleExoPlayerView oldPlayerView, @Nullable SimpleExoPlayerView newPlayerView) { + public static void switchTargetView( + @NonNull SimpleExoPlayer player, + @Nullable SimpleExoPlayerView oldPlayerView, + @Nullable SimpleExoPlayerView newPlayerView) { if (oldPlayerView == newPlayerView) { return; } @@ -389,21 +394,20 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns the player currently set on this view, or null if no player is set. - */ + /** Returns the player currently set on this view, or null if no player is set. */ public SimpleExoPlayer getPlayer() { return player; } /** * Set the {@link SimpleExoPlayer} to use. - *

      - * To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to - * use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} rather - * than this method. If you do wish to use this method directly, be sure to attach the player to - * the new view before calling {@code setPlayer(null)} to detach it from the old one. - * This ordering is significantly more efficient and may allow for more seamless transitions. + * + *

      To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended + * to use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} + * rather than this method. If you do wish to use this method directly, be sure to attach the + * player to the new view before calling {@code setPlayer(null)} to detach it from the + * old one. This ordering is significantly more efficient and may allow for more seamless + * transitions. * * @param player The {@link SimpleExoPlayer} to use. */ @@ -467,9 +471,7 @@ public final class SimpleExoPlayerView extends FrameLayout { contentFrame.setResizeMode(resizeMode); } - /** - * Returns whether artwork is displayed if present in the media. - */ + /** Returns whether artwork is displayed if present in the media. */ public boolean getUseArtwork() { return useArtwork; } @@ -487,9 +489,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns the default artwork to display. - */ + /** Returns the default artwork to display. */ public Bitmap getDefaultArtwork() { return defaultArtwork; } @@ -507,9 +507,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns whether the playback controls can be shown. - */ + /** Returns whether the playback controls can be shown. */ public boolean getUseController() { return useController; } @@ -554,8 +552,8 @@ public final class SimpleExoPlayerView extends FrameLayout { overlayFrameLayout.requestFocus(); return super.dispatchKeyEvent(event); } - boolean isDpadWhenControlHidden = isDpadKey(event.getKeyCode()) && useController - && !controller.isVisible(); + boolean isDpadWhenControlHidden = + isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); maybeShowController(true); return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } @@ -574,17 +572,15 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Shows the playback controls. Does nothing if playback controls are disabled. * - *

      The playback controls are automatically hidden during playback after - * {{@link #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not - * started yet, is paused, has ended or failed. + *

      The playback controls are automatically hidden during playback after {{@link + * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, + * is paused, has ended or failed. */ public void showController() { showController(shouldShowControllerIndefinitely()); } - /** - * Hides the playback controls. Does nothing if playback controls are disabled. - */ + /** Hides the playback controls. Does nothing if playback controls are disabled. */ public void hideController() { if (controller != null) { controller.hide(); @@ -607,8 +603,8 @@ public final class SimpleExoPlayerView extends FrameLayout { * Sets the playback controls timeout. The playback controls are automatically hidden after this * duration of time has elapsed without user input and with playback or buffering in progress. * - * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause - * the controller to remain visible indefinitely. + * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the + * controller to remain visible indefinitely. */ public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { Assertions.checkState(controller != null); @@ -620,9 +616,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns whether the playback controls are hidden by touch events. - */ + /** Returns whether the playback controls are hidden by touch events. */ public boolean getControllerHideOnTouch() { return controllerHideOnTouch; } @@ -680,8 +674,8 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Sets the {@link ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use - * {@link DefaultControlDispatcher}. + * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link + * DefaultControlDispatcher}. */ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { Assertions.checkState(controller != null); @@ -742,11 +736,12 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Gets the view onto which video is rendered. This is a: + * *

        - *
      • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to - * {@code surface_view}.
      • - *
      • {@link TextureView} if {@code surface_type} is {@code texture_view}.
      • - *
      • {@code null} if {@code surface_type} is {@code none}.
      • + *
      • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code + * surface_view}. + *
      • {@link TextureView} if {@code surface_type} is {@code texture_view}. + *
      • {@code null} if {@code surface_type} is {@code none}. *
      * * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. @@ -798,9 +793,7 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } - /** - * Shows the playback controls, but only if forced or shown indefinitely. - */ + /** Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { if (isPlayingAd() && controllerHideDuringAds) { return; @@ -819,8 +812,10 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } int playbackState = player.getPlaybackState(); - return controllerAutoShow && (playbackState == Player.STATE_IDLE - || playbackState == Player.STATE_ENDED || !player.getPlayWhenReady()); + return controllerAutoShow + && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED + || !player.getPlayWhenReady()); } private void showController(boolean showIndefinitely) { @@ -927,15 +922,19 @@ public final class SimpleExoPlayerView extends FrameLayout { @SuppressLint("InlinedApi") private boolean isDpadKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT + return keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; } - private final class ComponentListener extends Player.DefaultEventListener implements TextOutput, - SimpleExoPlayer.VideoListener { + private final class ComponentListener extends Player.DefaultEventListener + implements TextOutput, SimpleExoPlayer.VideoListener { // TextOutput implementation @@ -949,8 +948,8 @@ public final class SimpleExoPlayerView extends FrameLayout { // SimpleExoPlayer.VideoInfoListener implementation @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); @@ -986,7 +985,5 @@ public final class SimpleExoPlayerView extends FrameLayout { hideController(); } } - } - } From 682953c411208a673f59975620b1b55682772c70 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 4 Jan 2018 03:23:27 -0800 Subject: [PATCH 238/417] Fix typos ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180778084 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../google/android/exoplayer2/source/dash/DashMediaPeriod.java | 2 +- 2 files changed, 2 insertions(+), 2 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 284d716582..15c30e8c67 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 extends Player.DefaultEventListener implements A * *

      Ads will be requested automatically when the player is prepared if this method has not been * called, so it is only necessary to call this method if you want to request ads before preparing - * the player + * the player. * * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ 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 2b7b16228e..a8f9203cbf 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 @@ -104,7 +104,7 @@ import java.util.Map; /** * Updates the {@link DashManifest} and the index of this period in the manifest. - *

      + * * @param manifest The updated manifest. * @param periodIndex the new index of this period in the updated manifest. */ From c89cc81b710e7bc5e8f35c56e59cc8f8f99f1a0b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 4 Jan 2018 04:31:44 -0800 Subject: [PATCH 239/417] Configure MediaCodecs for realtime priority ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180782164 --- .../audio/MediaCodecAudioRenderer.java | 5 +++-- .../mediacodec/MediaCodecRenderer.java | 20 +++++++++++++++++++ .../video/MediaCodecVideoRenderer.java | 6 +----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index b4459e42aa..f73d63616b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -240,14 +240,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); + MediaFormat mediaFormat = getMediaFormatForPlayback(format); if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. - passthroughMediaFormat = format.getFrameworkMediaFormatV16(); + passthroughMediaFormat = mediaFormat; passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); codec.configure(passthroughMediaFormat, null, crypto, 0); passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); } else { - codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0); + codec.configure(mediaFormat, null, crypto, 0); passthroughMediaFormat = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index ef7d691c5b..4b1af7e385 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -430,6 +430,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return codecInfo; } + /** + * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} + * for decoding the given {@link Format} for playback. + * + * @param format The format of the media. + * @return The framework media format. + */ + protected final MediaFormat getMediaFormatForPlayback(Format format) { + MediaFormat mediaFormat = format.getFrameworkMediaFormatV16(); + if (Util.SDK_INT >= 23) { + configureMediaFormatForPlaybackV23(mediaFormat); + } + return mediaFormat; + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); @@ -1108,6 +1123,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + @TargetApi(23) + private static void configureMediaFormatForPlaybackV23(MediaFormat mediaFormat) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + } + /** * Returns whether the decoder is known to fail when flushed. *

      diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 41e3c970c4..6900823ebe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -906,19 +906,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. + MediaFormat frameworkMediaFormat = getMediaFormatForPlayback(format); frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. if (codecMaxValues.inputSize != Format.NO_VALUE) { frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); } - // Set FRC workaround. if (deviceNeedsAutoFrcWorkaround) { frameworkMediaFormat.setInteger("auto-frc", 0); } - // Configure tunneling if enabled. if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); } From a1bac99f3bae78e510e367b61dd49a37e4301476 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jan 2018 05:39:42 -0800 Subject: [PATCH 240/417] Fix loadDrmInitData given DASH manifest parser changes DASH manifests can now contain non-null but incomplete DRM init data. Hence using the manifest init data when non-null is not always the correct thing to do. This change merges the sample and manifest formats (which correctly merges the DRM init data) and then uses the result. Issue: #3630 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180787784 --- .../android/exoplayer2/source/dash/DashUtil.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index ed2f916b87..57632225a5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -81,14 +81,11 @@ public final class DashUtil { return null; } } - DrmInitData drmInitData = representation.format.drmInitData; - if (drmInitData != null) { - // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, - // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - return drmInitData; - } + Format manifestFormat = representation.format; Format sampleFormat = DashUtil.loadSampleFormat(dataSource, primaryTrackType, representation); - return sampleFormat == null ? null : sampleFormat.drmInitData; + return sampleFormat == null + ? manifestFormat.drmInitData + : sampleFormat.copyWithManifestFormatInfo(manifestFormat).drmInitData; } /** From b610e1144338a93e89604b095ac1792b72f7a5af Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Fri, 5 Jan 2018 23:10:51 -0500 Subject: [PATCH 241/417] PGS subtitle decoding support --- .../text/SubtitleDecoderFactory.java | 6 +- .../exoplayer2/text/pgs/PgsBuilder.java | 232 ++++++++++++++++++ .../exoplayer2/text/pgs/PgsDecoder.java | 26 ++ .../exoplayer2/text/pgs/PgsSubtitle.java | 54 ++++ 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 6a9b83a015..4720a67bba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; import com.google.android.exoplayer2.text.dvb.DvbDecoder; +import com.google.android.exoplayer2.text.pgs.PgsDecoder; import com.google.android.exoplayer2.text.ssa.SsaDecoder; import com.google.android.exoplayer2.text.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; @@ -80,7 +81,8 @@ public interface SubtitleDecoderFactory { || MimeTypes.APPLICATION_CEA608.equals(mimeType) || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType); + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); } @Override @@ -105,6 +107,8 @@ public interface SubtitleDecoderFactory { return new Cea708Decoder(format.accessibilityChannel); case MimeTypes.APPLICATION_DVBSUBS: return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java new file mode 100644 index 0000000000..e67178314d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java @@ -0,0 +1,232 @@ +/* +* +* Sources for this implementation PGS decoding can be founder below +* +* http://exar.ch/suprip/hddvd.php +* http://forum.doom9.org/showthread.php?t=124105 +* http://www.equasys.de/colorconversion.html + */ + +package com.google.android.exoplayer2.text.pgs; + +import android.graphics.Bitmap; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class PgsBuilder { + + private static final int SECTION_PALETTE = 0x14; + private static final int SECTION_BITMAP_PICTURE = 0x15; + private static final int SECTION_IDENTIFIER = 0x16; + private static final int SECTION_END = 0x80; + + private List list = new ArrayList<>(); + private Holder holder = new Holder(); + + boolean readNextSection(ParsableByteArray buffer) { + + if (buffer.bytesLeft() < 3) + return false; + + int sectionId = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + switch(sectionId) { + case SECTION_PALETTE: + holder.parsePaletteIndexes(buffer, sectionLength); + break; + case SECTION_BITMAP_PICTURE: + holder.fetchBitmapData(buffer, sectionLength); + break; + case SECTION_IDENTIFIER: + holder.fetchIdentifierData(buffer, sectionLength); + break; + case SECTION_END: + list.add(holder); + holder = new Holder(); + break; + default: + buffer.skipBytes(Math.min(sectionLength, buffer.bytesLeft())); + break; + } + return true; + } + + public Subtitle build() { + + if (list.isEmpty()) + return new PgsSubtitle(); + + Cue[] cues = new Cue[list.size()]; + long[] cueStartTimes = new long[list.size()]; + int index = 0; + for (Holder curr : list) { + cues[index] = curr.build(); + cueStartTimes[index++] = curr.start_time; + } + return new PgsSubtitle(cues, cueStartTimes); + } + + private class Holder { + + private int[] colors = null; + private ByteBuffer rle = null; + + Bitmap bitmap = null; + int plane_width = 0; + int plane_height = 0; + int bitmap_width = 0; + int bitmap_height = 0; + public int x = 0; + public int y = 0; + long start_time = 0; + + public Cue build() { + if (rle == null || !createBitmap(new ParsableByteArray(rle.array(), rle.position()))) + return null; + float left = (float) x / plane_width; + float top = (float) y / plane_height; + return new Cue(bitmap, left, Cue.ANCHOR_TYPE_START, top, Cue.ANCHOR_TYPE_START, + (float) bitmap_width / plane_width, (float) bitmap_height / plane_height); + } + + private void parsePaletteIndexes(ParsableByteArray buffer, int dataSize) { + // must be a multi of 5 for index, y, cb, cr, alpha + if (dataSize == 0 || (dataSize - 2) % 5 != 0) + return; + // skip first two bytes + buffer.skipBytes(2); + dataSize -= 2; + colors = new int[256]; + while (dataSize > 0) { + int index = buffer.readUnsignedByte(); + int color_y = buffer.readUnsignedByte() - 16; + int color_cr = buffer.readUnsignedByte() - 128; + int color_cb = buffer.readUnsignedByte() - 128; + int color_alpha = buffer.readUnsignedByte(); + dataSize -= 5; + if (index >= colors.length) + continue; + + int color_r = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 1.793 * color_cr), 0), 255); + int color_g = (int) Math.min(Math.max(Math.round(1.1644 * color_y + (-0.213 * color_cr) + (-0.533 * color_cb)), 0), 255); + int color_b = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 2.112 * color_cb), 0), 255); + //ARGB_8888 + colors[index] = (color_alpha << 24) | (color_r << 16) | (color_g << 8) | color_b; + } + } + + private void fetchBitmapData(ParsableByteArray buffer, int dataSize) { + if (dataSize <= 4) { + buffer.skipBytes(dataSize); + return; + } + // skip id field (2 bytes) + // skip version field + buffer.skipBytes(3); + dataSize -= 3; + + // check to see if this section is an appended section of the base section with + // width and height values + dataSize -= 1; // decrement first + if ((0x80 & buffer.readUnsignedByte()) > 0) { + if (dataSize < 3) { + buffer.skipBytes(dataSize); + return; + } + int full_len = buffer.readUnsignedInt24(); + dataSize -= 3; + if (full_len <= 4) { + buffer.skipBytes(dataSize); + return; + } + bitmap_width = buffer.readUnsignedShort(); + dataSize -= 2; + bitmap_height = buffer.readUnsignedShort(); + dataSize -= 2; + rle = ByteBuffer.allocate(full_len - 4); // don't include width & height + buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); + } else if (rle != null) { + int postSkip = dataSize > rle.capacity() ? dataSize - rle.capacity() : 0; + buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); + buffer.skipBytes(postSkip); + } + } + + private void fetchIdentifierData(ParsableByteArray buffer, int dataSize) { + if (dataSize < 4) { + buffer.skipBytes(dataSize); + return; + } + plane_width = buffer.readUnsignedShort(); + plane_height = buffer.readUnsignedShort(); + dataSize -= 4; + if (dataSize < 15) { + buffer.skipBytes(dataSize); + return; + } + // skip next 11 bytes + buffer.skipBytes(11); + x = buffer.readUnsignedShort(); + y = buffer.readUnsignedShort(); + dataSize -= 15; + buffer.skipBytes(dataSize); + } + + private boolean createBitmap(ParsableByteArray rle) { + if (bitmap_width == 0 || bitmap_height == 0 + || rle == null || rle.bytesLeft() == 0 + || colors == null || colors.length == 0) + return false; + int[] argb = new int[bitmap_width * bitmap_height]; + int currPixel = 0; + int nextbits, pixel_code, switchbits; + int number_of_pixels; + int line = 0; + while (rle.bytesLeft() > 0 && line < bitmap_height) { + boolean end_of_line = false; + do { + nextbits = rle.readUnsignedByte(); + if (nextbits != 0) { + pixel_code = nextbits; + number_of_pixels = 1; + } else { + switchbits = rle.readUnsignedByte(); + if ((switchbits & 0x80) == 0) { + pixel_code = 0; + if ((switchbits & 0x40) == 0) { + if (switchbits > 0) { + number_of_pixels = switchbits; + } else { + end_of_line = true; + ++line; + continue; + } + } else { + number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); + } + } else { + if ((switchbits & 0x40) == 0) { + number_of_pixels = switchbits & 0x3f; + pixel_code = rle.readUnsignedByte(); + } else { + number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); + pixel_code = rle.readUnsignedByte(); + } + } + } + Arrays.fill(argb, currPixel, currPixel + number_of_pixels, colors[pixel_code]); + currPixel += number_of_pixels; + } while (!end_of_line); + } + bitmap = Bitmap.createBitmap(argb, 0, bitmap_width, bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); + return bitmap != null; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java new file mode 100644 index 0000000000..04c3ecd0a3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -0,0 +1,26 @@ +package com.google.android.exoplayer2.text.pgs; + +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.ParsableByteArray; + +@SuppressWarnings("unused") +public class PgsDecoder extends SimpleSubtitleDecoder { + + @SuppressWarnings("unused") + public PgsDecoder() { + super("PgsDecoder"); + } + + @Override + protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { + ParsableByteArray buffer = new ParsableByteArray(data, size); + PgsBuilder builder = new PgsBuilder(); + do { + if (!builder.readNextSection(buffer)) + break; + } while (buffer.bytesLeft() > 0); + return builder.build(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java new file mode 100644 index 0000000000..affb2aa15b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.text.pgs; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +import java.util.Collections; +import java.util.List; + +public class PgsSubtitle implements Subtitle { + + private final Cue[] cues; + private final long[] cueTimesUs; + + PgsSubtitle() { + this.cues = null; + this.cueTimesUs = new long[0]; + } + + PgsSubtitle(Cue[] cues, long[] cueTimesUs) { + this.cues = cues; + this.cueTimesUs = cueTimesUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { +return cueTimesUs.length; +} + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < cueTimesUs.length); + return cueTimesUs[index]; + } + + @Override + public List getCues(long timeUs) { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1 || cues == null || cues[index] == null) { + // timeUs is earlier than the start of the first cue, or we have an empty cue. + return Collections.emptyList(); + } + else + return Collections.singletonList(cues[index]); + } +} From ca0c090c1a98aa37bfaf6d85e1ae681f6d6f5236 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 23 Dec 2017 13:25:45 -0500 Subject: [PATCH 242/417] add support in mediacodecaudiorenderer for 24bit pcm to float --- .../audio/FloatResamplingAudioProcessor.java | 171 ++++++++++++++++++ .../audio/MediaCodecAudioRenderer.java | 108 +++++++++-- 2 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java new file mode 100644 index 0000000000..28d2eca25f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -0,0 +1,171 @@ +package com.google.android.exoplayer2.audio; + + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ +/* package */ final class FloatResamplingAudioProcessor implements AudioProcessor { + + private int sampleRateHz; + private static final double PCM_INT32_FLOAT = 1.0 / 0x7fffffff; + + private int channelCount; + @C.PcmEncoding + private int sourceEncoding; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ + public FloatResamplingAudioProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + sourceEncoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws AudioProcessor.UnhandledFormatException { + if (encoding != C.ENCODING_PCM_24BIT) { + throw new AudioProcessor.UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.sourceEncoding == encoding) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.sourceEncoding = encoding; + + return true; + } + + @Override + public boolean isActive() { return sourceEncoding == C.ENCODING_PCM_24BIT; } + + @Override + public int getOutputChannelCount() { return channelCount; } + + @Override + public int getOutputEncoding() { return C.ENCODING_PCM_FLOAT; } + + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int offset = inputBuffer.position(); + int limit = inputBuffer.limit(); + int size = limit - offset; + + int resampledSize; + switch (sourceEncoding) { + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 4; + break; + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + if (buffer.capacity() < resampledSize) { + buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + + // Samples are little endian. + switch (sourceEncoding) { + case C.ENCODING_PCM_24BIT: + // 24->32 bit resampling. + for (int i = offset; i < limit; i += 3) { + int val = (inputBuffer.get(i) << 8) & 0x0000ff00 | (inputBuffer.get(i + 1) << 16) & 0x00ff0000 | + (inputBuffer.get(i + 2) << 24) & 0xff000000; + writePcm32bitFloat(val, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + sourceEncoding = C.ENCODING_INVALID; + } + + /** + * Converts the provided value into 32-bit float PCM and writes to buffer. + * + * @param val 32-bit int value to convert to 32-bit float [-1.0, 1.0] + * @param buffer The output buffer. + */ + private static void writePcm32bitFloat(int val, ByteBuffer buffer) { + float convVal = (float) (PCM_INT32_FLOAT * val); + int bits = Float.floatToIntBits(convVal); + if (bits == 0x7fc00000) + bits = Float.floatToIntBits((float) 0.0); + buffer.put((byte) (bits & 0xff)); + buffer.put((byte) ((bits >> 8) & 0xff)); + buffer.put((byte) ((bits >> 16) & 0xff)); + buffer.put((byte) ((bits >> 24) & 0xff)); + } + +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f73d63616b..d5e1b6ab03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -58,6 +58,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int encoderPadding; private long currentPositionUs; private boolean allowPositionDiscontinuity; + private final boolean dontDither24bitPCM; + private ByteBuffer resampledBuffer; + private FloatResamplingAudioProcessor floatResamplingAudioProcessor; /** * @param mediaCodecSelector A decoder selector. @@ -137,7 +140,37 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), + false); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM + * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before + * output. + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, boolean dontDither24bitPCM, + AudioProcessor... audioProcessors) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), + dontDither24bitPCM); } /** @@ -158,9 +191,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + eventListener, audioSink, false); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioSink audioSink, + boolean dontDither24bitPCM) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; + this.dontDither24bitPCM = dontDither24bitPCM; audioSink.setListener(new AudioSinkListener()); } @@ -268,10 +326,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; + + // if the input is 24bit pcm audio and we explicitly said not to dither then convert it to float + if (dontDither24bitPCM && newFormat.pcmEncoding == C.ENCODING_PCM_24BIT) { + if (floatResamplingAudioProcessor == null) + floatResamplingAudioProcessor = new FloatResamplingAudioProcessor(); + pcmEncoding = floatResamplingAudioProcessor.getOutputEncoding(); + } else { + // If the input format is anything other than PCM then we assume that the audio decoder will + // output 16-bit PCM. + pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding + : C.ENCODING_PCM_16BIT; + floatResamplingAudioProcessor = null; + } + channelCount = newFormat.channelCount; encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; @@ -302,9 +370,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { + if (floatResamplingAudioProcessor != null) + floatResamplingAudioProcessor.configure(sampleRate, channelCount, C.ENCODING_PCM_24BIT); audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, encoderPadding); - } catch (AudioSink.ConfigurationException e) { + } catch (AudioSink.ConfigurationException | AudioProcessor.UnhandledFormatException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -420,19 +490,35 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codec.releaseOutputBuffer(bufferIndex, false); return true; } - if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); + resampledBuffer = null; return true; } try { - if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; + if (floatResamplingAudioProcessor != null) { + boolean draining = resampledBuffer != null; + if (!draining) { + floatResamplingAudioProcessor.queueInput(buffer); + resampledBuffer = floatResamplingAudioProcessor.getOutput(); + } + if (audioSink.handleBuffer(resampledBuffer, bufferPresentationTimeUs)) + resampledBuffer = null; + if (!draining) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } + } + else { + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); From 821ea0e58b94ffa2eb95a7934c173aa44752fe2a Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 6 Jan 2018 00:26:18 -0500 Subject: [PATCH 243/417] moved floatresample into defaultaudiosink and added new constructor in defaultaudiosync to use that resample when audio input is 24/32bit pcm and the new flag is enabled --- .../exoplayer2/audio/DefaultAudioSink.java | 39 ++++++- .../audio/FloatResamplingAudioProcessor.java | 15 ++- .../audio/MediaCodecAudioRenderer.java | 108 ++---------------- 3 files changed, 60 insertions(+), 102 deletions(-) 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 e3bf72c541..ee5ca17bc7 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 @@ -164,10 +164,12 @@ public final class DefaultAudioSink implements AudioSink { public static boolean failOnSpuriousAudioTimestamp = false; @Nullable private final AudioCapabilities audioCapabilities; + private final boolean canConvertHiResPcmToFloat; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] availableAudioProcessors; + private final AudioProcessor[] hiResAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; @@ -180,6 +182,7 @@ public final class DefaultAudioSink implements AudioSink { private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; private boolean isInputPcm; + private boolean shouldUpResPCMAudio; private int inputSampleRate; private int sampleRate; private int channelConfig; @@ -233,6 +236,8 @@ public final class DefaultAudioSink implements AudioSink { private boolean hasData; private long lastFeedElapsedRealtimeMs; + + /** * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. @@ -241,7 +246,23 @@ public final class DefaultAudioSink implements AudioSink { */ public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { + this(audioCapabilities, audioProcessors, false); + } + + /** + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. + * @param canConvertHiResPcmToFloat Flag to convert > 16bit PCM Audio to 32bit Float PCM Audio to + * avoid dithering the input audio. If enabled other audio processors that expect 16bit PCM + * are disabled + */ + public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, + AudioProcessor[] audioProcessors, boolean canConvertHiResPcmToFloat) { + this.audioCapabilities = audioCapabilities; + this.canConvertHiResPcmToFloat = canConvertHiResPcmToFloat; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { try { @@ -265,6 +286,8 @@ public final class DefaultAudioSink implements AudioSink { availableAudioProcessors[2] = trimmingAudioProcessor; System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length); availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; + hiResAvailableAudioProcessors = new AudioProcessor[1]; + hiResAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -342,15 +365,20 @@ public final class DefaultAudioSink implements AudioSink { int channelCount = inputChannelCount; int sampleRate = inputSampleRate; isInputPcm = isEncodingPcm(inputEncoding); + shouldUpResPCMAudio = canConvertHiResPcmToFloat && + (inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); if (isInputPcm) { - pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); + pcmFrameSize = Util.getPcmFrameSize(shouldUpResPCMAudio + ? C.ENCODING_PCM_FLOAT : inputEncoding, channelCount); } @C.Encoding int encoding = inputEncoding; boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; if (processingEnabled) { + AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? + hiResAvailableAudioProcessors : availableAudioProcessors; trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + for (AudioProcessor audioProcessor : activeAudioProcessors) { try { flush |= audioProcessor.configure(sampleRate, channelCount, encoding); } catch (AudioProcessor.UnhandledFormatException e) { @@ -460,7 +488,9 @@ public final class DefaultAudioSink implements AudioSink { private void resetAudioProcessors() { ArrayList newAudioProcessors = new ArrayList<>(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? + hiResAvailableAudioProcessors : availableAudioProcessors; + for (AudioProcessor audioProcessor : activeAudioProcessors) { if (audioProcessor.isActive()) { newAudioProcessors.add(audioProcessor); } else { @@ -967,6 +997,9 @@ public final class DefaultAudioSink implements AudioSink { for (AudioProcessor audioProcessor : availableAudioProcessors) { audioProcessor.reset(); } + for (AudioProcessor audioProcessor : hiResAvailableAudioProcessors) { + audioProcessor.reset(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index 28d2eca25f..b0f48d43fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -36,7 +36,7 @@ import java.nio.ByteOrder; @Override public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) throws AudioProcessor.UnhandledFormatException { - if (encoding != C.ENCODING_PCM_24BIT) { + if (encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { throw new AudioProcessor.UnhandledFormatException(sampleRateHz, channelCount, encoding); } if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount @@ -51,7 +51,9 @@ import java.nio.ByteOrder; } @Override - public boolean isActive() { return sourceEncoding == C.ENCODING_PCM_24BIT; } + public boolean isActive() { + return sourceEncoding == C.ENCODING_PCM_24BIT || sourceEncoding == C.ENCODING_PCM_32BIT; + } @Override public int getOutputChannelCount() { return channelCount; } @@ -76,6 +78,8 @@ import java.nio.ByteOrder; resampledSize = (size / 3) * 4; break; case C.ENCODING_PCM_32BIT: + resampledSize = size; + break; case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: @@ -103,6 +107,13 @@ import java.nio.ByteOrder; } break; case C.ENCODING_PCM_32BIT: + // 32->32 bit conversion. + for (int i = offset; i < limit; i += 4) { + int val = inputBuffer.get(i) & 0x000000ff | (inputBuffer.get(i) << 8) & 0x0000ff00 | + (inputBuffer.get(i + 1) << 16) & 0x00ff0000 | (inputBuffer.get(i + 2) << 24) & 0xff000000; + writePcm32bitFloat(val, buffer); + } + break; case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index d5e1b6ab03..f73d63616b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -58,9 +58,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int encoderPadding; private long currentPositionUs; private boolean allowPositionDiscontinuity; - private final boolean dontDither24bitPCM; - private ByteBuffer resampledBuffer; - private FloatResamplingAudioProcessor floatResamplingAudioProcessor; /** * @param mediaCodecSelector A decoder selector. @@ -140,37 +137,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), - false); - } - - /** - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * begin in parallel with key acquisition. This parameter specifies whether the renderer is - * permitted to play clear regions of encrypted media files before {@code drmSessionManager} - * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM - * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before - * output. - */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, - @Nullable AudioCapabilities audioCapabilities, boolean dontDither24bitPCM, - AudioProcessor... audioProcessors) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), - dontDither24bitPCM); + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); } /** @@ -191,34 +158,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, audioSink, false); - } - - /** - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * begin in parallel with key acquisition. This parameter specifies whether the renderer is - * permitted to play clear regions of encrypted media files before {@code drmSessionManager} - * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioSink The sink to which audio will be output. - * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM - */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, AudioSink audioSink, - boolean dontDither24bitPCM) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; - this.dontDither24bitPCM = dontDither24bitPCM; audioSink.setListener(new AudioSinkListener()); } @@ -326,20 +268,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); - - // if the input is 24bit pcm audio and we explicitly said not to dither then convert it to float - if (dontDither24bitPCM && newFormat.pcmEncoding == C.ENCODING_PCM_24BIT) { - if (floatResamplingAudioProcessor == null) - floatResamplingAudioProcessor = new FloatResamplingAudioProcessor(); - pcmEncoding = floatResamplingAudioProcessor.getOutputEncoding(); - } else { - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - floatResamplingAudioProcessor = null; - } - + // If the input format is anything other than PCM then we assume that the audio decoder will + // output 16-bit PCM. + pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding + : C.ENCODING_PCM_16BIT; channelCount = newFormat.channelCount; encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; @@ -370,11 +302,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - if (floatResamplingAudioProcessor != null) - floatResamplingAudioProcessor.configure(sampleRate, channelCount, C.ENCODING_PCM_24BIT); audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, encoderPadding); - } catch (AudioSink.ConfigurationException | AudioProcessor.UnhandledFormatException e) { + } catch (AudioSink.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -490,35 +420,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codec.releaseOutputBuffer(bufferIndex, false); return true; } + if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); - resampledBuffer = null; return true; } try { - if (floatResamplingAudioProcessor != null) { - boolean draining = resampledBuffer != null; - if (!draining) { - floatResamplingAudioProcessor.queueInput(buffer); - resampledBuffer = floatResamplingAudioProcessor.getOutput(); - } - if (audioSink.handleBuffer(resampledBuffer, bufferPresentationTimeUs)) - resampledBuffer = null; - if (!draining) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; - } - } - else { - if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; - } + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); From aaf469ce065205593e99cfbd655e6f57b7763d62 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sun, 14 Jan 2018 10:55:54 -0500 Subject: [PATCH 244/417] code review changes and fix for discontinuity --- .../exoplayer2/audio/DefaultAudioSink.java | 43 ++++++++++--------- .../audio/FloatResamplingAudioProcessor.java | 20 +++++++-- 2 files changed, 38 insertions(+), 25 deletions(-) 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 ee5ca17bc7..892fd428ff 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 @@ -168,8 +168,8 @@ public final class DefaultAudioSink implements AudioSink { private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; - private final AudioProcessor[] availableAudioProcessors; - private final AudioProcessor[] hiResAvailableAudioProcessors; + private final AudioProcessor[] toIntPcmAvailableAudioProcessors; + private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; @@ -189,6 +189,7 @@ public final class DefaultAudioSink implements AudioSink { private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; private boolean processingEnabled; + private boolean canApplyPlaybackParams; private int bufferSize; private long bufferSizeUs; @@ -280,14 +281,14 @@ public final class DefaultAudioSink implements AudioSink { channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); - availableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; - availableAudioProcessors[0] = new ResamplingAudioProcessor(); - availableAudioProcessors[1] = channelMappingAudioProcessor; - availableAudioProcessors[2] = trimmingAudioProcessor; - System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length); - availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; - hiResAvailableAudioProcessors = new AudioProcessor[1]; - hiResAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); + toIntPcmAvailableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; + toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor(); + toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor; + toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor; + System.arraycopy(audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length); + toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; + toFloatPcmAvailableAudioProcessors = new AudioProcessor[1]; + toFloatPcmAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -368,17 +369,17 @@ public final class DefaultAudioSink implements AudioSink { shouldUpResPCMAudio = canConvertHiResPcmToFloat && (inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); if (isInputPcm) { - pcmFrameSize = Util.getPcmFrameSize(shouldUpResPCMAudio - ? C.ENCODING_PCM_FLOAT : inputEncoding, channelCount); + pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); } @C.Encoding int encoding = inputEncoding; boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + canApplyPlaybackParams = processingEnabled && !shouldUpResPCMAudio; if (processingEnabled) { - AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? - hiResAvailableAudioProcessors : availableAudioProcessors; + AudioProcessor[] availableAudioProcessors = shouldUpResPCMAudio ? + toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); - for (AudioProcessor audioProcessor : activeAudioProcessors) { + for (AudioProcessor audioProcessor : availableAudioProcessors) { try { flush |= audioProcessor.configure(sampleRate, channelCount, encoding); } catch (AudioProcessor.UnhandledFormatException e) { @@ -488,9 +489,9 @@ public final class DefaultAudioSink implements AudioSink { private void resetAudioProcessors() { ArrayList newAudioProcessors = new ArrayList<>(); - AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? - hiResAvailableAudioProcessors : availableAudioProcessors; - for (AudioProcessor audioProcessor : activeAudioProcessors) { + AudioProcessor[] availableAudioProcessors = shouldUpResPCMAudio ? + toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; + for (AudioProcessor audioProcessor : availableAudioProcessors) { if (audioProcessor.isActive()) { newAudioProcessors.add(audioProcessor); } else { @@ -838,7 +839,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (isInitialized() && !processingEnabled) { + if (isInitialized() && !canApplyPlaybackParams) { // The playback parameters are always the default if processing is disabled. this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; @@ -994,10 +995,10 @@ public final class DefaultAudioSink implements AudioSink { public void release() { reset(); releaseKeepSessionIdAudioTrack(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) { audioProcessor.reset(); } - for (AudioProcessor audioProcessor : hiResAvailableAudioProcessors) { + for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) { audioProcessor.reset(); } audioSessionId = C.AUDIO_SESSION_ID_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index b0f48d43fa..f7073f1275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -1,3 +1,18 @@ +/* + * 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.audio; @@ -173,10 +188,7 @@ import java.nio.ByteOrder; int bits = Float.floatToIntBits(convVal); if (bits == 0x7fc00000) bits = Float.floatToIntBits((float) 0.0); - buffer.put((byte) (bits & 0xff)); - buffer.put((byte) ((bits >> 8) & 0xff)); - buffer.put((byte) ((bits >> 16) & 0xff)); - buffer.put((byte) ((bits >> 24) & 0xff)); + buffer.putInt(bits); } } \ No newline at end of file From 373935aeb66c5e19275fa9c42409913aa04fafc2 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 5 Jan 2018 01:22:41 -0800 Subject: [PATCH 245/417] Make CacheDataSource detect cache availability change In certain conditions CacheDataSource switch to reading from upstream without writing back to cache. This change makes it detect the change of these conditions and switch to reading from or writing to cache. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180901463 --- RELEASENOTES.md | 2 + .../upstream/cache/CacheDataSource.java | 117 +++++++++------- .../exoplayer2/upstream/cache/CacheUtil.java | 22 ++- .../upstream/cache/CacheDataSourceTest.java | 132 ++++++++++++++++-- 4 files changed, 201 insertions(+), 72 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4679a0b376..e167fe94b6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). +* CacheDataSource: Check periodically if it's possible to read from/write to + cache after deciding to bypass cache. ### 2.6.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 5eea140a8b..2b151943a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -86,6 +86,9 @@ public final class CacheDataSource implements DataSource { } + /** Minimum number of bytes to read before checking cache for availability. */ + private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024; + private final Cache cache; private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; @@ -97,16 +100,17 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheForUnsetLengthRequests; private DataSource currentDataSource; - private boolean readingUnknownLengthDataFromUpstream; + private boolean currentDataSpecLengthUnset; private Uri uri; private int flags; private String key; private long readPosition; private long bytesRemaining; - private CacheSpan lockedSpan; + private CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; + private long checkCachePosition; /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for @@ -219,8 +223,11 @@ public final class CacheDataSource implements DataSource { return C.RESULT_END_OF_INPUT; } try { + if (readPosition >= checkCachePosition) { + openNextSource(); + } int bytesRead = currentDataSource.read(buffer, offset, readLength); - if (bytesRead >= 0) { + if (bytesRead != C.RESULT_END_OF_INPUT) { if (currentDataSource == cacheReadDataSource) { totalCachedBytesRead += bytesRead; } @@ -228,28 +235,18 @@ public final class CacheDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - } else { - if (readingUnknownLengthDataFromUpstream) { - setCurrentDataSourceBytesRemaining(0); - } - closeCurrentSource(); - if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { - try { - openNextSource(); - } catch (IOException e) { - if (readingUnknownLengthDataFromUpstream && isCausedByPositionOutOfRange(e)) { - setCurrentDataSourceBytesRemaining(0); - } else { - throw e; - } - } - if (bytesRemaining != 0) { - return read(buffer, offset, readLength); - } - } + } else if (currentDataSpecLengthUnset) { + setBytesRemaining(0); + } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { + openNextSource(); + return read(buffer, offset, readLength); } return bytesRead; } catch (IOException e) { + if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { + setBytesRemaining(0); + return C.RESULT_END_OF_INPUT; + } handleBeforeThrow(e); throw e; } @@ -278,62 +275,76 @@ public final class CacheDataSource implements DataSource { * opened to read from the upstream source and write into the cache. */ private void openNextSource() throws IOException { - DataSpec dataSpec; - CacheSpan span; + CacheSpan nextSpan; if (currentRequestIgnoresCache) { - span = null; + nextSpan = null; } else if (blockOnCache) { try { - span = cache.startReadWrite(key, readPosition); + nextSpan = cache.startReadWrite(key, readPosition); } catch (InterruptedException e) { throw new InterruptedIOException(); } } else { - span = cache.startReadWriteNonBlocking(key, readPosition); + nextSpan = cache.startReadWriteNonBlocking(key, readPosition); } - if (span == null) { + DataSpec nextDataSpec; + DataSource nextDataSource; + if (nextSpan == null) { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. - currentDataSource = upstreamDataSource; - dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); - } else if (span.isCached) { + nextDataSource = upstreamDataSource; + nextDataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); + } else if (nextSpan.isCached) { // Data is cached, read from cache. - Uri fileUri = Uri.fromFile(span.file); - long filePosition = readPosition - span.position; - long length = span.length - filePosition; + Uri fileUri = Uri.fromFile(nextSpan.file); + long filePosition = readPosition - nextSpan.position; + long length = nextSpan.length - filePosition; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } - dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); - currentDataSource = cacheReadDataSource; + nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); + nextDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. long length; - if (span.isOpenEnded()) { + if (nextSpan.isOpenEnded()) { length = bytesRemaining; } else { - length = span.length; + length = nextSpan.length; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } } - dataSpec = new DataSpec(uri, readPosition, length, key, flags); + nextDataSpec = new DataSpec(uri, readPosition, length, key, flags); if (cacheWriteDataSource != null) { - currentDataSource = cacheWriteDataSource; - lockedSpan = span; + nextDataSource = cacheWriteDataSource; } else { - currentDataSource = upstreamDataSource; - cache.releaseHoleSpan(span); + nextDataSource = upstreamDataSource; + cache.releaseHoleSpan(nextSpan); + nextSpan = null; } } - // If the request is unbounded it must be an upstream request. - readingUnknownLengthDataFromUpstream = dataSpec.length == C.LENGTH_UNSET; + if (nextDataSource == upstreamDataSource) { + checkCachePosition = readPosition + MIN_READ_BEFORE_CHECKING_CACHE; + if (currentDataSource == upstreamDataSource) { + return; + } + } else { + checkCachePosition = Long.MAX_VALUE; + } + closeCurrentSource(); - long resolvedLength = currentDataSource.open(dataSpec); - if (readingUnknownLengthDataFromUpstream && resolvedLength != C.LENGTH_UNSET) { - setCurrentDataSourceBytesRemaining(resolvedLength); + if (nextSpan != null && nextSpan.isHoleSpan()) { + currentHoleSpan = nextSpan; + } + currentDataSource = nextDataSource; + currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; + + long resolvedLength = nextDataSource.open(nextDataSpec); + if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { + setBytesRemaining(resolvedLength); } } @@ -351,7 +362,7 @@ public final class CacheDataSource implements DataSource { return false; } - private void setCurrentDataSourceBytesRemaining(long bytesRemaining) throws IOException { + private void setBytesRemaining(long bytesRemaining) throws IOException { this.bytesRemaining = bytesRemaining; if (isWritingToCache()) { cache.setContentLength(key, readPosition + bytesRemaining); @@ -369,11 +380,11 @@ public final class CacheDataSource implements DataSource { try { currentDataSource.close(); currentDataSource = null; - readingUnknownLengthDataFromUpstream = false; + currentDataSpecLengthUnset = false; } finally { - if (lockedSpan != null) { - cache.releaseHoleSpan(lockedSpan); - lockedSpan = null; + if (currentHoleSpan != null) { + cache.releaseHoleSpan(currentHoleSpan); + currentHoleSpan = null; } } } 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 cf2dedbe54..c612ea3739 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -110,12 +111,13 @@ public final class CacheUtil { * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param counters Counters to update during caching. + * @param counters If not null, updated during caching. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted. */ - public static void cache(DataSpec dataSpec, Cache cache, DataSource upstream, - CachingCounters counters) throws IOException, InterruptedException { + public static void cache( + DataSpec dataSpec, Cache cache, DataSource upstream, @Nullable CachingCounters counters) + throws IOException, InterruptedException { cache(dataSpec, cache, new CacheDataSource(cache, upstream), new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false); } @@ -131,15 +133,21 @@ public final class CacheUtil { * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters Counters to update during caching. + * @param counters If not null, updated during caching. * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been * reached unexpectedly. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted. */ - public static void cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, - byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters, boolean enableEOFException) + public static void cache( + DataSpec dataSpec, + Cache cache, + CacheDataSource dataSource, + byte[] buffer, + PriorityTaskManager priorityTaskManager, + int priority, + @Nullable CachingCounters counters, + boolean enableEOFException) throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); 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 e92f072dc2..4a2ca8c535 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream.cache; -import static android.net.Uri.EMPTY; import static com.google.android.exoplayer2.C.LENGTH_UNSET; import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCacheEmpty; import static com.google.common.truth.Truth.assertThat; @@ -51,14 +50,16 @@ public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final int MAX_CACHE_FILE_SIZE = 3; - private static final String KEY_1 = "key 1"; - private static final String KEY_2 = "key 2"; + private Uri testDataUri; + private String testDataKey; private File tempFolder; private SimpleCache cache; @Before public void setUp() throws Exception { + testDataUri = Uri.parse("test_data"); + testDataKey = CacheUtil.generateKey(testDataUri); tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } @@ -116,7 +117,7 @@ public final class CacheDataSourceTest { // If the user try to access off range then it should throw an IOException try { cacheDataSource = createCacheDataSource(false, false); - cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length, 5, KEY_1)); + cacheDataSource.open(new DataSpec(testDataUri, TEST_DATA.length, 5, testDataKey)); fail(); } catch (IOException e) { // success @@ -128,7 +129,7 @@ public final class CacheDataSourceTest { // Read partial at EOS but don't cross it so length is unknown CacheDataSource cacheDataSource = createCacheDataSource(false, true); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); - assertThat(cache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); + assertThat(cache.getContentLength(testDataKey)).isEqualTo(LENGTH_UNSET); // Now do an unbounded request for whole data. This will cause a bounded request from upstream. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. @@ -136,12 +137,16 @@ public final class CacheDataSourceTest { assertReadDataContentLength(cacheDataSource, true, true); // Now the length set correctly do an unbounded request with offset - assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, - LENGTH_UNSET, KEY_1))).isEqualTo(2); + assertThat( + cacheDataSource.open( + new DataSpec(testDataUri, TEST_DATA.length - 2, LENGTH_UNSET, testDataKey))) + .isEqualTo(2); // An unbounded request with offset for not cached content - assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, - LENGTH_UNSET, KEY_2))).isEqualTo(LENGTH_UNSET); + assertThat( + cacheDataSource.open( + new DataSpec(Uri.parse("notCachedUri"), TEST_DATA.length - 2, LENGTH_UNSET, null))) + .isEqualTo(LENGTH_UNSET); } @Test @@ -159,6 +164,107 @@ public final class CacheDataSourceTest { assertCacheEmpty(cache); } + @Test + public void testSwitchToCacheSourceWithReadOnlyCacheDataSource() throws Exception { + // Create a fake data source with a 1 MB default data. + FakeDataSource upstream = new FakeDataSource(); + FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1); + // Insert an action just before the end of the data to fail the test if reading from upstream + // reaches end of the data. + fakeData + .appendReadAction( + new Runnable() { + @Override + public void run() { + fail("Read from upstream shouldn't reach to the end of the data."); + } + }) + .appendReadData(1); + // Create cache read-only CacheDataSource. + CacheDataSource cacheDataSource = + new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null); + + // Open source and read some data from upstream as the data hasn't cached yet. + DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + cacheDataSource.open(dataSpec); + byte[] buffer = new byte[1024]; + cacheDataSource.read(buffer, 0, buffer.length); + + // Cache the data. + // Although we use another FakeDataSource instance, it shouldn't matter. + FakeDataSource upstream2 = + new FakeDataSource( + new FakeDataSource() + .getDataSet() + .newDefaultData() + .appendReadData(1024 * 1024) + .endData()); + CacheUtil.cache(dataSpec, cache, upstream2, null); + + // Read the rest of the data. + while (true) { + if (cacheDataSource.read(buffer, 0, buffer.length) == C.RESULT_END_OF_INPUT) { + break; + } + } + cacheDataSource.close(); + } + + @Test + public void testSwitchToCacheSourceWithNonBlockingCacheDataSource() throws Exception { + // Create a fake data source with a 1 MB default data. + FakeDataSource upstream = new FakeDataSource(); + FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1); + // Insert an action just before the end of the data to fail the test if reading from upstream + // reaches end of the data. + fakeData + .appendReadAction( + new Runnable() { + @Override + public void run() { + fail("Read from upstream shouldn't reach to the end of the data."); + } + }) + .appendReadData(1); + + // Lock the content on the cache. + SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(testDataKey, 0); + assertThat(cacheSpan).isNotNull(); + assertThat(cacheSpan.isHoleSpan()).isTrue(); + + // Create non blocking CacheDataSource. + CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0); + + // Open source and read some data from upstream without writing to cache as the data is locked. + DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + cacheDataSource.open(dataSpec); + byte[] buffer = new byte[1024]; + cacheDataSource.read(buffer, 0, buffer.length); + + // Unlock the span. + cache.releaseHoleSpan(cacheSpan); + assertCacheEmpty(cache); + + // Cache the data. + // Although we use another FakeDataSource instance, it shouldn't matter. + FakeDataSource upstream2 = + new FakeDataSource( + new FakeDataSource() + .getDataSet() + .newDefaultData() + .appendReadData(1024 * 1024) + .endData()); + CacheUtil.cache(dataSpec, cache, upstream2, null); + + // Read the rest of the data. + while (true) { + if (cacheDataSource.read(buffer, 0, buffer.length) == C.RESULT_END_OF_INPUT) { + break; + } + } + cacheDataSource.close(); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { // Read all data from upstream and write to cache @@ -179,8 +285,10 @@ public final class CacheDataSourceTest { boolean unboundedRequest, boolean unknownLength) throws IOException { int length = unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length; assertReadData(cacheDataSource, unknownLength, 0, length); - assertWithMessage("When the range specified, CacheDataSource doesn't reach EOS so shouldn't " - + "cache content length").that(cache.getContentLength(KEY_1)) + assertWithMessage( + "When the range specified, CacheDataSource doesn't reach EOS so shouldn't " + + "cache content length") + .that(cache.getContentLength(testDataKey)) .isEqualTo(!unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length); } @@ -190,7 +298,7 @@ public final class CacheDataSourceTest { if (length != C.LENGTH_UNSET) { testDataLength = Math.min(testDataLength, length); } - assertThat(cacheDataSource.open(new DataSpec(EMPTY, position, length, KEY_1))) + assertThat(cacheDataSource.open(new DataSpec(testDataUri, position, length, testDataKey))) .isEqualTo(unknownLength ? length : testDataLength); byte[] buffer = new byte[100]; From 5364962dca36136d0765aefb885af61015fb503e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 5 Jan 2018 07:35:28 -0800 Subject: [PATCH 246/417] Automatically apply rotation for TextureView in SimpleExoPlayer. If SimpleExoPlayer is using TextView as output, we can handle video rotation by automatically applying a matrix transformation to the TextureView when we have this information available from the video (from video's metadata). GitHub: #91 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180925571 --- RELEASENOTES.md | 3 + .../exoplayer2/ui/SimpleExoPlayerView.java | 74 ++++++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e167fe94b6..997c3047c4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### dev-v2 (not yet released) ### +* SimpleExoPlayerView: Automatically apply video rotation if + `SimpleExoPlayerView` is configured to use `TextureView` + ([#91](https://github.com/google/ExoPlayer/issues/91)). * Player interface: * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index def8925ec3..6e69a31fd9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -22,6 +22,8 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; @@ -224,6 +226,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private boolean controllerAutoShow; private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; + private int textureViewRotation; public SimpleExoPlayerView(Context context) { this(context, null); @@ -920,6 +923,31 @@ public final class SimpleExoPlayerView extends FrameLayout { aspectRatioFrame.setResizeMode(resizeMode); } + /** Applies a texture rotation to a {@link TextureView}. */ + private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + float textureViewWidth = textureView.getWidth(); + float textureViewHeight = textureView.getHeight(); + if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { + textureView.setTransform(null); + } else { + Matrix transformMatrix = new Matrix(); + float pivotX = textureViewWidth / 2; + float pivotY = textureViewHeight / 2; + transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); + + // After rotation, scale the rotated texture to fit the TextureView size. + RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); + RectF rotatedTextureRect = new RectF(); + transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); + transformMatrix.postScale( + textureViewWidth / rotatedTextureRect.width(), + textureViewHeight / rotatedTextureRect.height(), + pivotX, + pivotY); + textureView.setTransform(transformMatrix); + } + } + @SuppressLint("InlinedApi") private boolean isDpadKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_DPAD_UP @@ -934,7 +962,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } private final class ComponentListener extends Player.DefaultEventListener - implements TextOutput, SimpleExoPlayer.VideoListener { + implements TextOutput, SimpleExoPlayer.VideoListener, OnLayoutChangeListener { // TextOutput implementation @@ -950,10 +978,32 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame != null) { - float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; - contentFrame.setAspectRatio(aspectRatio); + if (contentFrame == null) { + return; } + float videoAspectRatio = + (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; + + if (surfaceView instanceof TextureView) { + // Try to apply rotation transformation when our surface is a TextureView. + if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { + // We will apply a rotation 90/270 degree to the output texture of the TextureView. + // In this case, the output video's width and height will be swapped. + videoAspectRatio = 1 / videoAspectRatio; + } + if (textureViewRotation != 0) { + surfaceView.removeOnLayoutChangeListener(this); + } + textureViewRotation = unappliedRotationDegrees; + if (textureViewRotation != 0) { + // The texture view's dimensions might be changed after layout step. + // So add an OnLayoutChangeListener to apply rotation after layout step. + surfaceView.addOnLayoutChangeListener(this); + } + applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); + } + + contentFrame.setAspectRatio(videoAspectRatio); } @Override @@ -985,5 +1035,21 @@ public final class SimpleExoPlayerView extends FrameLayout { hideController(); } } + + // OnLayoutChangeListener implementation + + @Override + public void onLayoutChange( + View view, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + applyTextureViewRotation((TextureView) view, textureViewRotation); + } } } From 4b018b4d19c6dc658ba1eaaf72ab9610224a3687 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 5 Jan 2018 08:14:43 -0800 Subject: [PATCH 247/417] Document how unset length request are cached ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180929422 --- .../com/google/android/exoplayer2/upstream/DataSpec.java | 3 ++- .../android/exoplayer2/upstream/cache/CacheDataSink.java | 5 +++++ .../android/exoplayer2/upstream/cache/CacheDataSource.java | 7 ++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index cbe971bc5d..a6b89a334d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -49,7 +49,8 @@ public final class DataSpec { public static final int FLAG_ALLOW_GZIP = 1 << 0; /** - * Permits content to be cached even if its length can not be resolved. + * Permits content to be cached even if its length can not be resolved. Typically this's the case + * for progressive live streams and when {@link #FLAG_ALLOW_GZIP} is used. */ public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 33b1ca58b0..1af690e10f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -29,6 +29,11 @@ import java.io.OutputStream; /** * Writes data into a cache. + * + *

      If the {@link DataSpec} object used with {@link #open(DataSpec)} method call has the {@code + * length} field set to {@link C#LENGTH_UNSET} but {@link + * DataSpec#FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} isn't set then {@link #write(byte[], int, int)} calls + * are ignored. */ public final class CacheDataSink implements DataSink { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 2b151943a5..f1d50a43e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -35,6 +35,10 @@ import java.lang.annotation.RetentionPolicy; * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache * when possible. When data is not cached it is requested from an upstream {@link DataSource} and * written into the cache. + * + *

      By default requests whose length can not be resolved are not cached. This is to prevent + * caching of progressive live streams, which should usually not be cached. Caching of this kind of + * requests can be enabled per request with {@link DataSpec#FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}. */ public final class CacheDataSource implements DataSource { @@ -67,7 +71,8 @@ public final class CacheDataSource implements DataSource { public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; /** - * A flag indicating that the cache should be bypassed for requests whose lengths are unset. + * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This + * flag is provided for legacy reasons only. */ public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; From 4867748c501412668572b1518e09a4c10792e244 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Jan 2018 09:03:48 -0800 Subject: [PATCH 248/417] Add assertions to check that media sources are not prepared twice. This lets apps fail-fast when they try to reuse media source instances. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180934445 --- .../google/android/exoplayer2/source/ClippingMediaSource.java | 3 ++- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 1 + .../exoplayer2/source/DynamicConcatenatingMediaSource.java | 1 + .../android/exoplayer2/source/ExtractorMediaSource.java | 3 ++- .../google/android/exoplayer2/source/LoopingMediaSource.java | 3 +++ .../com/google/android/exoplayer2/source/MediaSource.java | 2 ++ .../google/android/exoplayer2/source/MergingMediaSource.java | 2 ++ .../android/exoplayer2/source/SingleSampleMediaSource.java | 4 ++++ .../google/android/exoplayer2/source/ads/AdsMediaSource.java | 1 + .../android/exoplayer2/source/dash/DashMediaSource.java | 1 + .../google/android/exoplayer2/source/hls/HlsMediaSource.java | 3 +-- .../exoplayer2/source/smoothstreaming/SsMediaSource.java | 2 +- 12 files changed, 21 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 721950f6b9..42b1cfd1cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -131,7 +131,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.sourceListener = listener; + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); + sourceListener = listener; mediaSource.prepareSource(player, false, this); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 058471f31f..2b089cfdbe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -89,6 +89,7 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.listener = listener; if (mediaSources.length == 0) { listener.onSourceInfoRefreshed(this, Timeline.EMPTY, null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c2e208afbe..5dd4f004fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -331,6 +331,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe @Override public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.player = player; this.listener = listener; preventListenerNotification = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 14453653af..d7dff5a278 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -325,6 +325,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); sourceListener = listener; notifySourceInfoRefreshed(C.TIME_UNSET, false); } @@ -356,7 +357,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public void releaseSource() { - sourceListener = null; + // Do nothing. } // ExtractorMediaPeriod.Listener implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 984820cc6a..5cf110c9ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -36,6 +36,7 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; private int childPeriodCount; + private boolean wasPrepareSourceCalled; /** * Loops the provided source indefinitely. Note that it is usually better to use @@ -61,6 +62,8 @@ public final class LoopingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) { + Assertions.checkState(!wasPrepareSourceCalled, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); + wasPrepareSourceCalled = true; childSource.prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 4a0d8e196d..25da60cb74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -149,6 +149,8 @@ public interface MediaSource { } + String MEDIA_SOURCE_REUSED_ERROR_MESSAGE = "MediaSource instances are not allowed to be reused."; + /** * Starts preparation of the source. *

      diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 79ed864e25..accf82a68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -19,6 +19,7 @@ import android.support.annotation.IntDef; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -97,6 +98,7 @@ public final class MergingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.listener = listener; for (int i = 0; i < mediaSources.length; i++) { final int sourceIndex = i; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index b92085d15e..ef93f74958 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -158,6 +158,8 @@ public final class SingleSampleMediaSource implements MediaSource { private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + private boolean isPrepared; + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will @@ -251,6 +253,8 @@ public final class SingleSampleMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(!isPrepared, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); + isPrepared = true; listener.onSourceInfoRefreshed(this, timeline, null); } 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 0980e9d011..dbff387d82 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 @@ -204,6 +204,7 @@ public final class AdsMediaSource implements MediaSource { @Override public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkArgument(isTopLevelSource); + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.listener = listener; this.player = player; playerHandler = new Handler(); 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 9c0c58c87b..77914d6d45 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 @@ -483,6 +483,7 @@ public final class DashMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); sourceListener = listener; if (sideloadedManifest) { loaderErrorThrower = new LoaderErrorThrower.Dummy(); 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 b628807109..31680af8c4 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 @@ -324,7 +324,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Assertions.checkState(playlistTracker == null); + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, minLoadableRetryCount, this, playlistParser); sourceListener = listener; @@ -361,7 +361,6 @@ public final class HlsMediaSource implements MediaSource, playlistTracker.release(); playlistTracker = null; } - sourceListener = null; } @Override 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 9932db7869..03e8d601f5 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 @@ -419,6 +419,7 @@ public final class SsMediaSource implements MediaSource, @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); sourceListener = listener; if (manifest != null) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); @@ -455,7 +456,6 @@ public final class SsMediaSource implements MediaSource, @Override public void releaseSource() { - sourceListener = null; manifest = null; manifestDataSource = null; manifestLoadStartTimestamp = 0; From c991b80c856f0d2f8cfb8afb8106a0fc05b625ae Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Jan 2018 01:57:30 -0800 Subject: [PATCH 249/417] Rmeove unused variable in Mp4Extractor and HeifExtractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181135589 --- .../android/exoplayer2/extractor/mp4/Mp4Extractor.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 50fc0aec80..2c56f9ac2f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -370,7 +370,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { int firstVideoTrackIndex = C.INDEX_UNSET; long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); - long earliestSampleOffset = Long.MAX_VALUE; Metadata metadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); @@ -423,11 +422,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { firstVideoTrackIndex = tracks.size(); } tracks.add(mp4Track); - - long firstSampleOffset = trackSampleTable.offsets[0]; - if (firstSampleOffset < earliestSampleOffset) { - earliestSampleOffset = firstSampleOffset; - } } this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; From 73892f21b15a90cb5ce6acda470d13c31855d3ad Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 8 Jan 2018 02:23:40 -0800 Subject: [PATCH 250/417] Ubernit line re-order ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181137491 --- .../java/com/google/android/exoplayer2/DefaultLoadControl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 26873fcf2e..af610a8165 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; - targetBufferBytesOverwrite = targetBufferBytes; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + targetBufferBytesOverwrite = targetBufferBytes; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.priorityTaskManager = priorityTaskManager; } From d533a83ae41a19ae33b56bef83683549c52722d3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 8 Jan 2018 02:25:16 -0800 Subject: [PATCH 251/417] Partial revert of DRM fixes ---------------------------------- Original change description: DRM fixes - Parse multiple kids from default_KID. It's specified as a whitespace separated list of UUIDs rather than a single UUID. - Opportunistically proceed with playback in cases where the manifest only defines a single SchemeData with the common PSSH UUID. In such cases the manifest isn't saying anything about which specific DRM schemes it supports. Issue: #3630 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181137621 --- RELEASENOTES.md | 3 --- .../exoplayer2/drm/DefaultDrmSessionManager.java | 12 ++---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 997c3047c4..e318f6a656 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,9 +29,6 @@ positions. * Note: `SeekParameters` are only currently effective when playing `ExtractorMediaSource`s (i.e. progressive streams). -* DRM: Optimistically attempt playback of DRM protected content that does not - declare scheme specific init data - ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 9c134970ff..6a5185a266 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -23,7 +23,6 @@ import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; @@ -109,7 +108,6 @@ public class DefaultDrmSessionManager implements DrmSe /** Number of times to retry for initial provisioning and key request for reporting error. */ public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; - private static final String TAG = "DrmSessionManager"; private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private final UUID uuid; @@ -353,14 +351,8 @@ public class DefaultDrmSessionManager implements DrmSe public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { SchemeData schemeData = getSchemeData(drmInitData, uuid, true); if (schemeData == null) { - if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { - // Assume scheme specific data will be added before the session is opened. - Log.w( - TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); - } else { - // No data for this manager's scheme. - return false; - } + // No data for this manager's scheme. + return false; } String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { From 67d4626701ee93f6df8f7a274af1927ecbec780d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 8 Jan 2018 03:07:49 -0800 Subject: [PATCH 252/417] Add support for non-Extractor content MediaSources in IMA demo Issue: #3676 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181140929 --- RELEASENOTES.md | 2 ++ demos/ima/build.gradle | 1 + .../exoplayer2/imademo/PlayerManager.java | 31 ++++++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e318f6a656..2d14d00a49 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,8 @@ ([#2147](https://github.com/google/ExoPlayer/issues/2147)). * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. +* IMA extension: Add support for playing non-Extractor content MediaSources in + the IMA demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). ### 2.6.1 ### diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 536d8d4662..5225c260f8 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -45,5 +45,6 @@ dependencies { compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'library-dash') compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 51959451d1..0316030ef0 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -32,6 +32,8 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -79,15 +81,10 @@ import com.google.android.exoplayer2.util.Util; // Bind the player to the view. simpleExoPlayerView.setPlayer(player); - // Produces DataSource instances through which media data is loaded. - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, - Util.getUserAgent(context, context.getString(R.string.application_name))); - // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Factory(dataSourceFactory) - .createMediaSource(Uri.parse(contentUrl)); + buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = @@ -126,6 +123,19 @@ import com.google.android.exoplayer2.util.Util; @Override public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return buildMediaSource(uri, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + // IMA does not support Smooth Streaming ads. + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } + + // Internal methods. + + private MediaSource buildMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { @ContentType int type = Util.inferContentType(uri); switch (type) { case C.TYPE_DASH: @@ -133,20 +143,19 @@ import com.google.android.exoplayer2.util.Util; new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, handler, listener); - case C.TYPE_SS: default: throw new IllegalStateException("Unsupported type: " + type); } } - @Override - public int[] getSupportedTypes() { - return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; - } } From d427a1dd62eac30af5bb7519e0c7b437dd64e8db Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 8 Jan 2018 07:06:15 -0800 Subject: [PATCH 253/417] Make Cache.getCachedSpans return empty set rather than null ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181161289 --- .../upstream/cache/CachedRegionTrackerTest.java | 7 +++++++ .../android/exoplayer2/upstream/cache/Cache.java | 15 +++++++++------ .../exoplayer2/upstream/cache/CacheUtil.java | 3 --- .../upstream/cache/CachedRegionTracker.java | 14 ++++++-------- .../exoplayer2/upstream/cache/SimpleCache.java | 3 ++- .../exoplayer2/upstream/cache/CacheAsserts.java | 3 ++- .../upstream/cache/SimpleCacheTest.java | 4 ++-- .../android/exoplayer2/testutil/CacheAsserts.java | 7 ++++--- 8 files changed, 32 insertions(+), 24 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index fc4a9cfed6..2f54ae8972 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; + import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; +import java.util.TreeSet; import org.mockito.Mock; /** @@ -48,6 +53,8 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { protected void setUp() throws Exception { super.setUp(); MockitoUtil.setUpMockito(this); + when(cache.addListener(anyString(), any(Cache.Listener.class))) + .thenReturn(new TreeSet()); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 80ad698fa4..76481bbdf7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.io.File; import java.io.IOException; @@ -80,15 +81,16 @@ public interface Cache { /** * Registers a listener to listen for changes to a given key. - *

      - * No guarantees are made about the thread or threads on which the listener is called, but it is - * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in - * the same order as events occurred. + * + *

      No guarantees are made about the thread or threads on which the listener is called, but it + * is guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and + * in the same order as events occurred. * * @param key The key to listen to. * @param listener The listener to add. * @return The current spans for the key. */ + @NonNull NavigableSet addListener(String key, Listener listener); /** @@ -103,9 +105,10 @@ public interface Cache { * Returns the cached spans for a given cache key. * * @param key The key for which spans should be returned. - * @return The spans for the key. May be null if there are no such spans. + * @return The spans for the key. */ - @Nullable NavigableSet getCachedSpans(String key); + @NonNull + NavigableSet getCachedSpans(String key); /** * Returns all keys in the cache. 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 c612ea3739..2bf5cde8e0 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 @@ -253,9 +253,6 @@ public final class CacheUtil { /** Removes all of the data in the {@code cache} pointed by the {@code key}. */ public static void remove(Cache cache, String key) { NavigableSet cachedSpans = cache.getCachedSpans(key); - if (cachedSpans == null) { - return; - } for (CacheSpan cachedSpan : cachedSpans) { try { cache.removeSpan(cachedSpan); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java index 9559054f6d..9455aed11b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -50,14 +50,12 @@ public final class CachedRegionTracker implements Cache.Listener { synchronized (this) { NavigableSet cacheSpans = cache.addListener(cacheKey, this); - if (cacheSpans != null) { - // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, - // which is why a descending iterator is used here. - Iterator spanIterator = cacheSpans.descendingIterator(); - while (spanIterator.hasNext()) { - CacheSpan span = spanIterator.next(); - mergeSpan(span); - } + // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, + // which is why a descending iterator is used here. + Iterator spanIterator = cacheSpans.descendingIterator(); + while (spanIterator.hasNext()) { + CacheSpan span = spanIterator.next(); + mergeSpan(span); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 599474d6c3..ffac8a35f1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -134,7 +134,8 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { CachedContent cachedContent = index.get(key); - return cachedContent == null || cachedContent.isEmpty() ? null + return cachedContent == null || cachedContent.isEmpty() + ? new TreeSet() : new TreeSet(cachedContent.getSpans()); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java index aa98ad3179..65850a13e7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java @@ -101,7 +101,8 @@ import java.util.ArrayList; public static void assertDataNotCached(Cache cache, String... uriStrings) { for (String uriString : uriStrings) { assertWithMessage("There is cached data for '" + uriString + "'") - .that(cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))).isNull(); + .that(cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))) + .isEmpty(); } } 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 d5894895b1..75a80185b9 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 @@ -77,7 +77,7 @@ public class SimpleCacheTest { assertThat(simpleCache.getKeys()).isEmpty(); NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertThat(cachedSpans == null || cachedSpans.isEmpty()).isTrue(); + assertThat(cachedSpans.isEmpty()).isTrue(); assertThat(simpleCache.getCacheSpace()).isEqualTo(0); assertThat(cacheDir.listFiles()).hasLength(0); @@ -283,7 +283,7 @@ public class SimpleCacheTest { // Although store() has failed, it should remove the first span and add the new one. NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertThat(cachedSpans).isNotNull(); + assertThat(cachedSpans).isNotEmpty(); assertThat(cachedSpans).hasSize(1); assertThat(cachedSpans.pollFirst().position).isEqualTo(15); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 82fff0d4fe..eb53191dc8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; import android.net.Uri; import android.test.MoreAsserts; @@ -29,7 +30,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; -import junit.framework.Assert; /** * Assertion methods for {@link Cache}. @@ -105,8 +105,9 @@ public final class CacheAsserts { /** Asserts that there is no cache content for the given {@code uriStrings}. */ public static void assertDataNotCached(Cache cache, String... uriStrings) { for (String uriString : uriStrings) { - Assert.assertNull("There is cached data for '" + uriString + "',", - cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))); + assertTrue( + "There is cached data for '" + uriString + "',", + cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString))).isEmpty()); } } From 4ee971052bb39acf1f33a52959d77c7204de8498 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 Jan 2018 03:34:40 -0800 Subject: [PATCH 254/417] Improve Extractor partial read tests. Partial reads were performed once using a partial size of 1 byte. This was not enough to detect problems which only occur in combination with IOExceptions. Partial reads are now only applied when no exception is thrown. Moreover, the tests didn't check whether the total number of sampled bytes is what it is supposed to be. Added a field to the data dumps checking the total number of bytes in the sampled data. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181296545 --- .../src/androidTest/assets/bear.flac.0.dump | 1 + .../src/androidTest/assets/bear.flac.1.dump | 1 + .../src/androidTest/assets/bear.flac.2.dump | 1 + .../src/androidTest/assets/bear.flac.3.dump | 1 + .../androidTest/assets/flv/sample.flv.0.dump | 2 + .../androidTest/assets/mkv/sample.mkv.0.dump | 2 + .../androidTest/assets/mkv/sample.mkv.1.dump | 2 + .../androidTest/assets/mkv/sample.mkv.2.dump | 2 + .../androidTest/assets/mkv/sample.mkv.3.dump | 2 + .../subsample_encrypted_altref.webm.0.dump | 1 + .../subsample_encrypted_noaltref.webm.0.dump | 1 + .../androidTest/assets/mp3/bear.mp3.0.dump | 1 + .../androidTest/assets/mp3/bear.mp3.1.dump | 1 + .../androidTest/assets/mp3/bear.mp3.2.dump | 1 + .../androidTest/assets/mp3/bear.mp3.3.dump | 1 + .../assets/mp3/play-trimmed.mp3.0.dump | 1 + .../assets/mp3/play-trimmed.mp3.1.dump | 1 + .../assets/mp3/play-trimmed.mp3.2.dump | 1 + .../assets/mp3/play-trimmed.mp3.3.dump | 1 + .../assets/mp3/play-trimmed.mp3.unklen.dump | 1 + .../androidTest/assets/mp4/sample.mp4.0.dump | 2 + .../androidTest/assets/mp4/sample.mp4.1.dump | 2 + .../androidTest/assets/mp4/sample.mp4.2.dump | 2 + .../androidTest/assets/mp4/sample.mp4.3.dump | 2 + .../assets/mp4/sample_fragmented.mp4.0.dump | 2 + .../mp4/sample_fragmented_sei.mp4.0.dump | 3 + .../androidTest/assets/ogg/bear.opus.0.dump | 1 + .../androidTest/assets/ogg/bear.opus.1.dump | 1 + .../androidTest/assets/ogg/bear.opus.2.dump | 1 + .../androidTest/assets/ogg/bear.opus.3.dump | 1 + .../assets/ogg/bear.opus.unklen.dump | 1 + .../assets/ogg/bear_flac.ogg.0.dump | 1 + .../assets/ogg/bear_flac.ogg.1.dump | 1 + .../assets/ogg/bear_flac.ogg.2.dump | 1 + .../assets/ogg/bear_flac.ogg.3.dump | 1 + .../assets/ogg/bear_flac.ogg.unklen.dump | 1 + .../ogg/bear_flac_noseektable.ogg.0.dump | 1 + .../ogg/bear_flac_noseektable.ogg.1.dump | 1 + .../ogg/bear_flac_noseektable.ogg.2.dump | 1 + .../ogg/bear_flac_noseektable.ogg.3.dump | 1 + .../ogg/bear_flac_noseektable.ogg.unklen.dump | 1 + .../assets/ogg/bear_vorbis.ogg.0.dump | 1 + .../assets/ogg/bear_vorbis.ogg.1.dump | 1 + .../assets/ogg/bear_vorbis.ogg.2.dump | 1 + .../assets/ogg/bear_vorbis.ogg.3.dump | 1 + .../assets/ogg/bear_vorbis.ogg.unklen.dump | 1 + .../assets/rawcc/sample.rawcc.0.dump | 1 + .../androidTest/assets/ts/sample.ac3.0.dump | 1 + .../androidTest/assets/ts/sample.adts.0.dump | 2 + .../androidTest/assets/ts/sample.ps.0.dump | 2 + .../androidTest/assets/ts/sample.ts.0.dump | 2 + .../androidTest/assets/wav/sample.wav.0.dump | 1 + .../androidTest/assets/wav/sample.wav.1.dump | 1 + .../androidTest/assets/wav/sample.wav.2.dump | 1 + .../androidTest/assets/wav/sample.wav.3.dump | 1 + .../testutil/FakeExtractorInput.java | 63 +++++++++++-------- .../exoplayer2/testutil/FakeTrackOutput.java | 1 + 57 files changed, 109 insertions(+), 25 deletions(-) diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump index 6908f5cc93..2a17cbdea6 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 526272 sample count = 33 sample 0: time = 0 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump index 1414443187..412e4a1b8f 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 362432 sample count = 23 sample 0: time = 853333 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump index e343241650..42ebb125d1 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 182208 sample count = 12 sample 0: time = 1792000 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump index 95ab255bd0..958cb0d418 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 18368 sample count = 2 sample 0: time = 2645333 diff --git a/library/core/src/androidTest/assets/flv/sample.flv.0.dump b/library/core/src/androidTest/assets/flv/sample.flv.0.dump index 7a4a74770c..f4502749f5 100644 --- a/library/core/src/androidTest/assets/flv/sample.flv.0.dump +++ b/library/core/src/androidTest/assets/flv/sample.flv.0.dump @@ -26,6 +26,7 @@ track 8: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 9529 sample count = 45 sample 0: time = 112000 @@ -231,6 +232,7 @@ track 9: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 89502 sample count = 30 sample 0: time = 67000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump index 0f005ee5a9..009ff55c23 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 89502 sample count = 30 sample 0: time = 67000 @@ -170,6 +171,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 12120 sample count = 29 sample 0: time = 129000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump index 378f5d7f2a..91396e81b8 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 30995 sample count = 22 sample 0: time = 334000 @@ -138,6 +139,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 8778 sample count = 21 sample 0: time = 408000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump index 80caf24a93..5c56dcc8af 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 10158 sample count = 11 sample 0: time = 700000 @@ -94,6 +95,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 4180 sample count = 10 sample 0: time = 791000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump index c9672ba9c4..cf5a0199fc 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 0 sample count = 0 track 2: format: @@ -50,6 +51,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 1254 sample count = 3 sample 0: time = 1035000 diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump index abc07dc503..62a270eb9e 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -25,6 +25,7 @@ track 1: language = null drmInitData = 1305012705 initializationData: + total output bytes = 39 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index c43a43b576..43e5eed5d1 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -25,6 +25,7 @@ track 1: language = null drmInitData = 1305012705 initializationData: + total output bytes = 24 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump index eca3a6687d..b12a68a60b 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 44544 sample count = 116 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 12abf149c4..abf5b10415 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 29568 sample count = 77 sample 0: time = 928568 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index 3568616e76..813f61b7fc 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 14592 sample count = 38 sample 0: time = 1871586 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump index 8a31fe5e7d..9a0207bd53 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump @@ -25,5 +25,6 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump index 2c0ac67561..6b49619b50 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump index 7cd3486505..77708b16df 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 9529 sample count = 45 sample 0: time = 44000 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump index fcf9402cba..30ed21ef98 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 7464 sample count = 33 sample 0: time = 322639 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump index 5dbb6e1561..640d92722c 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 4019 sample count = 18 sample 0: time = 670938 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump index bac707446d..b4fd4a0b02 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 470 sample count = 3 sample 0: time = 1019238 diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index 736e57693c..ec2cb7b8ce 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 85933 sample count = 30 sample 0: time = 66000 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 5, hash 2B7623A + total output bytes = 18257 sample count = 46 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index 8186a2b9ce..ae012055fe 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 85933 sample count = 30 sample 0: time = 66000 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 5, hash 2B7623A + total output bytes = 18257 sample count = 46 sample 0: time = 0 @@ -378,5 +380,6 @@ track 3: language = null drmInitData = - initializationData: + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump index 4d09067f3b..643972b836 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 25541 sample count = 275 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump index 821351e989..8df1563d90 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 17031 sample count = 184 sample 0: time = 910000 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump index 3aea1e8d74..bed4c46d9c 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 8698 sample count = 92 sample 0: time = 1830000 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump index b49af29f2c..8a9c99250e 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 126 sample count = 1 sample 0: time = 2741000 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump index b2d5a9f3d2..5d2c84b047 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 25541 sample count = 275 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump index 572d1da891..ff22bb2d3e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump index d53f257fd2..50110149fd 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 113666 sample count = 23 sample 0: time = 853333 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump index cdfd6efab8..483ae36721 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 55652 sample count = 12 sample 0: time = 1792000 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump index 9b029d3301..a47407e63d 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 445 sample count = 1 sample 0: time = 2730666 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump index 572d1da891..ff22bb2d3e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump index 1c02c1bbef..32f350efcb 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump index 81d79b8674..3082e8faca 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 113666 sample count = 23 sample 0: time = 853333 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump index f8b00bcb3a..b574409f70 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 55652 sample count = 12 sample 0: time = 1792000 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump index b020618488..f411596b44 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 445 sample count = 1 sample 0: time = 2730666 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index bf135434f4..bdfe90277d 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump index 860e8a3b5b..dd129ce9dc 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 26873 sample count = 180 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump index 11afeb9665..4fb8a74d92 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 17598 sample count = 109 sample 0: time = 896000 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump index f2f97ebcfa..fad8f33d77 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 8658 sample count = 49 sample 0: time = 1821333 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump index 5d5f284cf2..49dca02220 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump @@ -27,5 +27,6 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump index ee1176773e..756be42854 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 26873 sample count = 180 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump index d430d1d8d4..130be06ceb 100644 --- a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump +++ b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 978 sample count = 150 sample 0: time = 37657512133 diff --git a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump index bedffcf198..46028638fe 100644 --- a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 13281 sample count = 8 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ts/sample.adts.0.dump b/library/core/src/androidTest/assets/ts/sample.adts.0.dump index a97cf860d1..132859a00e 100644 --- a/library/core/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.adts.0.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 30797 sample count = 144 sample 0: time = 0 @@ -625,5 +626,6 @@ track 1: language = null drmInitData = - initializationData: + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 41db704d56..e833201692 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -25,6 +25,7 @@ track 192: language = null drmInitData = - initializationData: + total output bytes = 1671 sample count = 4 sample 0: time = 29088 @@ -65,6 +66,7 @@ track 224: drmInitData = - initializationData: data = length 22, hash 743CC6F8 + total output bytes = 44056 sample count = 2 sample 0: time = 40000 diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index e900b94673..39b1565289 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -26,6 +26,7 @@ track 256: drmInitData = - initializationData: data = length 22, hash CE183139 + total output bytes = 45026 sample count = 2 sample 0: time = 33366 @@ -57,6 +58,7 @@ track 257: language = und drmInitData = - initializationData: + total output bytes = 5015 sample count = 4 sample 0: time = 22455 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.0.dump b/library/core/src/androidTest/assets/wav/sample.wav.0.dump index 5d0f4d77f0..32f9d495d2 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.0.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 88200 sample count = 3 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.1.dump b/library/core/src/androidTest/assets/wav/sample.wav.1.dump index e59239bff8..d4758e65b5 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.1.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 58802 sample count = 2 sample 0: time = 333310 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.2.dump b/library/core/src/androidTest/assets/wav/sample.wav.2.dump index c80a260385..ea33c62423 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.2.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 29402 sample count = 1 sample 0: time = 666643 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.3.dump b/library/core/src/androidTest/assets/wav/sample.wav.3.dump index 9f25028923..de0d8f22d0 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.3.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.3.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 2 sample count = 1 sample 0: time = 999977 diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index 5cb11fdd81..7f6398dd5a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -91,23 +91,16 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int read(byte[] target, int offset, int length) throws IOException { + checkIOException(readPosition, failedReadPositions); length = getReadLength(length); - if (readFully(target, offset, length, true)) { - return length; - } - return C.RESULT_END_OF_INPUT; + return readFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; } @Override public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) { - return false; - } - System.arraycopy(data, readPosition, target, offset, length); - readPosition += length; - peekPosition = readPosition; - return true; + checkIOException(readPosition, failedReadPositions); + return readFullyInternal(target, offset, length, allowEndOfInput); } @Override @@ -117,21 +110,15 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int skip(int length) throws IOException { + checkIOException(readPosition, failedReadPositions); length = getReadLength(length); - if (skipFully(length, true)) { - return length; - } - return C.RESULT_END_OF_INPUT; + return skipFullyInternal(length, true) ? length : C.RESULT_END_OF_INPUT; } @Override public boolean skipFully(int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) { - return false; - } - readPosition += length; - peekPosition = readPosition; - return true; + checkIOException(readPosition, failedReadPositions); + return skipFullyInternal(length, allowEndOfInput); } @Override @@ -142,7 +129,8 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) { + checkIOException(peekPosition, failedPeekPositions); + if (!checkXFully(allowEndOfInput, peekPosition, length)) { return false; } System.arraycopy(data, peekPosition, target, offset, length); @@ -157,7 +145,8 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) { + checkIOException(peekPosition, failedPeekPositions); + if (!checkXFully(allowEndOfInput, peekPosition, length)) { return false; } peekPosition += length; @@ -196,13 +185,17 @@ public final class FakeExtractorInput implements ExtractorInput { throw e; } - private boolean checkXFully(boolean allowEndOfInput, int position, int length, - SparseBooleanArray failedPositions) throws IOException { + private void checkIOException(int position, SparseBooleanArray failedPositions) + throws SimulatedIOException { if (simulateIOErrors && !failedPositions.get(position)) { failedPositions.put(position, true); peekPosition = readPosition; throw new SimulatedIOException("Simulated IO error at position: " + position); } + } + + private boolean checkXFully(boolean allowEndOfInput, int position, int length) + throws EOFException { if (length > 0 && position == data.length) { if (allowEndOfInput) { return false; @@ -230,6 +223,26 @@ public final class FakeExtractorInput implements ExtractorInput { return Math.min(requestedLength, data.length - readPosition); } + private boolean readFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) + throws EOFException { + if (!checkXFully(allowEndOfInput, readPosition, length)) { + return false; + } + System.arraycopy(data, readPosition, target, offset, length); + readPosition += length; + peekPosition = readPosition; + return true; + } + + private boolean skipFullyInternal(int length, boolean allowEndOfInput) throws EOFException { + if (!checkXFully(allowEndOfInput, readPosition, length)) { + return false; + } + readPosition += length; + peekPosition = readPosition; + return true; + } + /** * Builder of {@link FakeExtractorInput} instances. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index b14e6f60ef..f8e5407421 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -160,6 +160,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { } dumper.endBlock().endBlock(); + dumper.add("total output bytes", sampleData.length); dumper.add("sample count", sampleTimesUs.size()); for (int i = 0; i < sampleTimesUs.size(); i++) { From ff1bb2f70287d8ddf1d9d1ddd3ccd92724db7ba1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jan 2018 06:52:54 -0800 Subject: [PATCH 255/417] Apply SeekParameters to DASH + SmoothStreaming playbacks Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181314086 --- RELEASENOTES.md | 3 +- .../android/exoplayer2/SeekParameters.java | 18 +++++++++ .../source/ExtractorMediaPeriod.java | 24 +----------- .../source/chunk/ChunkSampleStream.java | 19 ++++++++- .../exoplayer2/source/chunk/ChunkSource.java | 11 ++++++ .../google/android/exoplayer2/util/Util.java | 39 +++++++++++++++++++ .../source/dash/DashChunkSource.java | 38 +++++++++++++++--- .../source/dash/DashMediaPeriod.java | 5 +++ .../source/dash/DefaultDashChunkSource.java | 19 +++++++++ .../smoothstreaming/DefaultSsChunkSource.java | 39 +++++++++++++------ .../source/smoothstreaming/SsChunkSource.java | 25 ++++++++++-- .../source/smoothstreaming/SsMediaPeriod.java | 5 +++ .../exoplayer2/testutil/FakeChunkSource.java | 14 +++++++ 13 files changed, 213 insertions(+), 46 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2d14d00a49..80381075b8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,8 +27,7 @@ performed. The `SeekParameters` class contains defaults for exact seeking and seeking to the closest sync points before, either side or after specified seek positions. - * Note: `SeekParameters` are only currently effective when playing - `ExtractorMediaSource`s (i.e. progressive streams). + * Note: `SeekParameters` are not currently supported when playing HLS streams. * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java index 8643b3999e..2df9840cf8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -69,4 +69,22 @@ public final class SeekParameters { this.toleranceBeforeUs = toleranceBeforeUs; this.toleranceAfterUs = toleranceAfterUs; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekParameters other = (SeekParameters) obj; + return toleranceBeforeUs == other.toleranceBeforeUs + && toleranceAfterUs == other.toleranceAfterUs; + } + + @Override + public int hashCode() { + return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index e5d1fae7bd..76d9d22648 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -378,28 +378,8 @@ import java.util.Arrays; return 0; } SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); - long minPositionUs = - Util.subtractWithOverflowDefault( - positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); - long maxPositionUs = - Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); - long firstPointUs = seekPoints.first.timeUs; - boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs; - long secondPointUs = seekPoints.second.timeUs; - boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs; - if (firstPointValid && secondPointValid) { - if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) { - return firstPointUs; - } else { - return secondPointUs; - } - } else if (firstPointValid) { - return firstPointUs; - } else if (secondPointValid) { - return secondPointUs; - } else { - return minPositionUs; - } + return Util.resolveSeekPositionUs( + positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs); } // SampleStream methods. 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 a96bc2dcd0..947664720b 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 @@ -19,6 +19,7 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; @@ -42,7 +43,8 @@ public class ChunkSampleStream implements SampleStream, S private static final String TAG = "ChunkSampleStream"; - private final int primaryTrackType; + public final int primaryTrackType; + private final int[] embeddedTrackTypes; private final boolean[] embeddedTracksSelected; private final T chunkSource; @@ -180,6 +182,21 @@ public class ChunkSampleStream implements SampleStream, S } } + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + // TODO: Using this method to adjust a seek position and then passing the adjusted position to + // seekToUs does not handle small discrepancies between the chunk boundary timestamps obtained + // from the chunk source and the timestamps of the samples in the chunks. + return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + /** * Seeks to the specified position in microseconds. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index b04dc7cbdb..568461c206 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import com.google.android.exoplayer2.SeekParameters; import java.io.IOException; import java.util.List; @@ -23,6 +24,16 @@ import java.util.List; */ public interface ChunkSource { + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + /** * If the source is currently having difficulty providing chunks, then this method throws the * underlying error. Otherwise does nothing. 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 a5f5222820..b3cc282717 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 @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.upstream.DataSource; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -762,6 +763,44 @@ public final class Util { return Math.round((double) mediaDuration / speed); } + /** + * Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate + * sync points. + * + * @param positionUs The requested seek position, in microseocnds. + * @param seekParameters The {@link SeekParameters}. + * @param firstSyncUs The first candidate seek point, in micrseconds. + * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code + * firstSyncUs} if there's only one candidate. + * @return The resolved seek position, in microseconds. + */ + public static long resolveSeekPositionUs( + long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) { + if (SeekParameters.EXACT.equals(seekParameters)) { + return positionUs; + } + long minPositionUs = + subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = + addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); + boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs; + boolean secondSyncPositionValid = + minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs; + if (firstSyncPositionValid && secondSyncPositionValid) { + if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) { + return firstSyncUs; + } else { + return secondSyncUs; + } + } else if (firstSyncPositionValid) { + return firstSyncUs; + } else if (secondSyncPositionValid) { + return secondSyncUs; + } else { + return minPositionUs; + } + } + /** * Converts a list of integers to a primitive array. * diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 4e25c0e333..167a8d486c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import android.os.SystemClock; import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -25,15 +26,40 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; */ public interface DashChunkSource extends ChunkSource { + /** Factory for {@link DashChunkSource}s. */ interface Factory { - DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int[] adaptationSetIndices, - TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs, - boolean enableEventMessageTrack, boolean enableCea608Track); - + /** + * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. + * @param manifest The initial manifest. + * @param periodIndex The index of the corresponding period in the manifest. + * @param adaptationSetIndices The indices of the corresponding adaptation sets in the period. + * @param trackSelection The track selection. + * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between + * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, + * specified as the server's unix time minus the local elapsed time. If unknown, set to 0. + * @param enableEventMessageTrack Whether the chunks generated by the source may output an event + * message track. + * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 + * track. + * @return The created {@link DashChunkSource}. + */ + DashChunkSource createDashChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, + int periodIndex, + int[] adaptationSetIndices, + TrackSelection trackSelection, + int type, + long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, + boolean enableCea608Track); } + /** + * Updates the manifest. + * + * @param newManifest The new manifest. + */ void updateManifest(DashManifest newManifest, int periodIndex); - } 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 a8f9203cbf..8a69f98653 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 @@ -309,6 +309,11 @@ import java.util.Map; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + for (ChunkSampleStream sampleStream : sampleStreams) { + if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) { + return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + } return positionUs; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index b254c4f09a..1162762f7c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.SeekMap; @@ -142,6 +143,24 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + // Segments are aligned across representations, so any segment index will do. + for (RepresentationHolder representationHolder : representationHolders) { + if (representationHolder.segmentIndex != null) { + int segmentNum = representationHolder.getSegmentNum(positionUs); + long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum); + long secondSyncUs = + firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1 + ? representationHolder.getSegmentStartTimeUs(segmentNum + 1) + : firstSyncUs; + return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + } + } + // We don't have a segment index to adjust the seek position with yet. + return positionUs; + } + @Override public void updateManifest(DashManifest newManifest, int newPeriodIndex) { try { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 5a6493b702..79014d6f4a 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.mp4.Track; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; @@ -34,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.List; @@ -62,7 +64,7 @@ public class DefaultSsChunkSource implements SsChunkSource { } private final LoaderErrorThrower manifestLoaderErrorThrower; - private final int elementIndex; + private final int streamElementIndex; private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; private final DataSource dataSource; @@ -75,22 +77,25 @@ public class DefaultSsChunkSource implements SsChunkSource { /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifest The initial manifest. - * @param elementIndex The index of the stream element in the manifest. + * @param streamElementIndex The index of the stream element in the manifest. * @param trackSelection The track selection. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param trackEncryptionBoxes Track encryption boxes for the stream. */ - public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, - int elementIndex, TrackSelection trackSelection, DataSource dataSource, + public DefaultSsChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + SsManifest manifest, + int streamElementIndex, + TrackSelection trackSelection, + DataSource dataSource, TrackEncryptionBox[] trackEncryptionBoxes) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; - this.elementIndex = elementIndex; + this.streamElementIndex = streamElementIndex; this.trackSelection = trackSelection; this.dataSource = dataSource; - StreamElement streamElement = manifest.streamElements[elementIndex]; - + StreamElement streamElement = manifest.streamElements[streamElementIndex]; extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()]; for (int i = 0; i < extractorWrappers.length; i++) { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); @@ -106,11 +111,23 @@ public class DefaultSsChunkSource implements SsChunkSource { } } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + StreamElement streamElement = manifest.streamElements[streamElementIndex]; + int chunkIndex = streamElement.getChunkIndex(positionUs); + long firstSyncUs = streamElement.getStartTimeUs(chunkIndex); + long secondSyncUs = + firstSyncUs < positionUs && chunkIndex < streamElement.chunkCount - 1 + ? streamElement.getStartTimeUs(chunkIndex + 1) + : firstSyncUs; + return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + } + @Override public void updateManifest(SsManifest newManifest) { - StreamElement currentElement = manifest.streamElements[elementIndex]; + StreamElement currentElement = manifest.streamElements[streamElementIndex]; int currentElementChunkCount = currentElement.chunkCount; - StreamElement newElement = newManifest.streamElements[elementIndex]; + StreamElement newElement = newManifest.streamElements[streamElementIndex]; if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { // There's no overlap between the old and new elements because at least one is empty. currentManifestChunkOffset += currentElementChunkCount; @@ -155,7 +172,7 @@ public class DefaultSsChunkSource implements SsChunkSource { return; } - StreamElement streamElement = manifest.streamElements[elementIndex]; + StreamElement streamElement = manifest.streamElements[streamElementIndex]; if (streamElement.chunkCount == 0) { // There aren't any chunks for us to load. out.endOfStream = !manifest.isLive; @@ -229,7 +246,7 @@ public class DefaultSsChunkSource implements SsChunkSource { return C.TIME_UNSET; } - StreamElement currentElement = manifest.streamElements[elementIndex]; + StreamElement currentElement = manifest.streamElements[streamElementIndex]; int lastChunkIndex = currentElement.chunkCount - 1; long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex) + currentElement.getChunkDurationUs(lastChunkIndex); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index e8815ff424..48491cd0bd 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -26,14 +26,31 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; */ public interface SsChunkSource extends ChunkSource { + /** Factory for {@link SsChunkSource}s. */ interface Factory { - SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - SsManifest manifest, int elementIndex, TrackSelection trackSelection, + /** + * Creates a new {@link SsChunkSource}. + * + * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. + * @param manifest The initial manifest. + * @param streamElementIndex The index of the corresponding stream element in the manifest. + * @param trackSelection The track selection. + * @param trackEncryptionBoxes Track encryption boxes for the stream. + * @return The created {@link SsChunkSource}. + */ + SsChunkSource createChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + SsManifest manifest, + int streamElementIndex, + TrackSelection trackSelection, TrackEncryptionBox[] trackEncryptionBoxes); - } + /** + * Updates the manifest. + * + * @param newManifest The new manifest. + */ void updateManifest(SsManifest newManifest); - } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 5ee60bdeed..99804ca809 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -185,6 +185,11 @@ import java.util.ArrayList; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + for (ChunkSampleStream sampleStream : sampleStreams) { + if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) { + return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + } return positionUs; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 28f5926bfa..6ff18e0b3d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkSource; @@ -28,6 +29,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + import java.io.IOException; import java.util.List; @@ -71,6 +74,17 @@ public final class FakeChunkSource implements ChunkSource { this.dataSet = dataSet; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + int chunkIndex = dataSet.getChunkIndexByPosition(positionUs); + long firstSyncUs = dataSet.getStartTime(chunkIndex); + long secondSyncUs = + firstSyncUs < positionUs && chunkIndex < dataSet.getChunkCount() - 1 + ? dataSet.getStartTime(chunkIndex + 1) + : firstSyncUs; + return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + } + @Override public void maybeThrowError() throws IOException { // Do nothing. From 96e490d7fef3b5dee8cdcefb36cf35cf939eee78 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 9 Jan 2018 08:04:36 -0800 Subject: [PATCH 256/417] Make it possible to subclass LibvpxVideoRenderer Make LibvpxVideoRenderer non-final and add protected methods to match MediaCodecVideoRenderer. Reorganize methods to separate BaseRenderer, protected and internal methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181320714 --- .../ext/vp9/LibvpxVideoRenderer.java | 740 ++++++++++-------- 1 file changed, 435 insertions(+), 305 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index ac944a7b01..108a89f56a 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -20,7 +20,9 @@ import android.graphics.Canvas; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.CallSuper; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; @@ -43,10 +45,8 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispa import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Decodes and renders video using the native VP9 decoder. - */ -public final class LibvpxVideoRenderer extends BaseRenderer { +/** Decodes and renders video using the native VP9 decoder. */ +public class LibvpxVideoRenderer extends BaseRenderer { @Retention(RetentionPolicy.SOURCE) @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, @@ -101,7 +101,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private final DecoderInputBuffer flagsOnlyBuffer; private final DrmSessionManager drmSessionManager; - private DecoderCounters decoderCounters; private Format format; private VpxDecoder decoder; private VpxInputBuffer inputBuffer; @@ -132,6 +131,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private int consecutiveDroppedFrameCount; private int buffersInCodecCount; + protected DecoderCounters decoderCounters; + /** * @param scaleToFit Whether video frames should be scaled to fit when rendering. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer @@ -196,6 +197,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { decoderReinitializationState = REINITIALIZATION_STATE_NONE; } + // BaseRenderer implementation. + @Override public int supportsFormat(Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { @@ -247,273 +250,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException, - VpxDecoderException { - // Acquire outputBuffer either from nextOutputBuffer or from the decoder. - if (outputBuffer == null) { - if (nextOutputBuffer != null) { - outputBuffer = nextOutputBuffer; - nextOutputBuffer = null; - } else { - outputBuffer = decoder.dequeueOutputBuffer(); - } - if (outputBuffer == null) { - return false; - } - decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; - buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; - } - - if (nextOutputBuffer == null) { - nextOutputBuffer = decoder.dequeueOutputBuffer(); - } - - if (outputBuffer.isEndOfStream()) { - if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { - // We're waiting to re-initialize the decoder, and have now processed all final buffers. - releaseDecoder(); - maybeInitDecoder(); - } else { - outputBuffer.release(); - outputBuffer = null; - outputStreamEnded = true; - } - return false; - } - - if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { - // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. - if (isBufferLate(outputBuffer.timeUs - positionUs)) { - forceRenderFrame = false; - skipBuffer(); - buffersInCodecCount--; - return true; - } - return false; - } - - if (forceRenderFrame) { - forceRenderFrame = false; - renderBuffer(); - buffersInCodecCount--; - return true; - } - - final long nextOutputBufferTimeUs = - nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() - ? nextOutputBuffer.timeUs : C.TIME_UNSET; - - long earlyUs = outputBuffer.timeUs - positionUs; - if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) { - forceRenderFrame = true; - return false; - } else if (shouldDropOutputBuffer( - outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) { - dropBuffer(); - buffersInCodecCount--; - return true; - } - - // If we have yet to render a frame to the current output (either initially or immediately - // following a seek), render one irrespective of the state or current position. - if (!renderedFirstFrame - || (getState() == STATE_STARTED && earlyUs <= 30000)) { - renderBuffer(); - buffersInCodecCount--; - } - return false; - } - - /** - * Returns whether the current frame should be dropped. - * - * @param outputBufferTimeUs The timestamp of the current output buffer. - * @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET} - * if the next output buffer is unavailable. - * @param positionUs The current playback position. - * @param joiningDeadlineMs The joining deadline. - * @return Returns whether to drop the current output buffer. - */ - private boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, - long positionUs, long joiningDeadlineMs) { - return isBufferLate(outputBufferTimeUs - positionUs) - && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); - } - - /** - * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after - * the current playback position, if possible. - * - * @param earlyUs The time until the current buffer should be presented in microseconds. A - * negative value indicates that the buffer is late. - */ - private boolean shouldDropBuffersToKeyframe(long earlyUs) { - return isBufferVeryLate(earlyUs); - } - - private void renderBuffer() { - int bufferMode = outputBuffer.mode; - boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; - boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; - if (!renderRgb && !renderYuv) { - dropBuffer(); - } else { - maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); - if (renderRgb) { - renderRgbFrame(outputBuffer, scaleToFit); - outputBuffer.release(); - } else /* renderYuv */ { - outputBufferRenderer.setOutputBuffer(outputBuffer); - // The renderer will release the buffer. - } - outputBuffer = null; - consecutiveDroppedFrameCount = 0; - decoderCounters.renderedOutputBufferCount++; - maybeNotifyRenderedFirstFrame(); - } - } - - private void dropBuffer() { - updateDroppedBufferCounters(1); - outputBuffer.release(); - outputBuffer = null; - } - - private boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { - int droppedSourceBufferCount = skipSource(positionUs); - if (droppedSourceBufferCount == 0) { - return false; - } - decoderCounters.droppedToKeyframeCount++; - // We dropped some buffers to catch up, so update the decoder counters and flush the codec, - // which releases all pending buffers buffers including the current output buffer. - updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); - flushDecoder(); - return true; - } - - private void updateDroppedBufferCounters(int droppedBufferCount) { - decoderCounters.droppedBufferCount += droppedBufferCount; - droppedFrames += droppedBufferCount; - consecutiveDroppedFrameCount += droppedBufferCount; - decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount, - decoderCounters.maxConsecutiveDroppedBufferCount); - if (droppedFrames >= maxDroppedFramesToNotify) { - maybeNotifyDroppedFrames(); - } - } - - private void skipBuffer() { - decoderCounters.skippedOutputBufferCount++; - outputBuffer.release(); - outputBuffer = null; - } - - private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { - if (bitmap == null || bitmap.getWidth() != outputBuffer.width - || bitmap.getHeight() != outputBuffer.height) { - bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); - } - bitmap.copyPixelsFromBuffer(outputBuffer.data); - Canvas canvas = surface.lockCanvas(null); - if (scale) { - canvas.scale(((float) canvas.getWidth()) / outputBuffer.width, - ((float) canvas.getHeight()) / outputBuffer.height); - } - canvas.drawBitmap(bitmap, 0, 0, null); - surface.unlockCanvasAndPost(canvas); - } - - private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { - if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM - || inputStreamEnded) { - // We need to reinitialize the decoder or the input stream has ended. - return false; - } - - if (inputBuffer == null) { - inputBuffer = decoder.dequeueInputBuffer(); - if (inputBuffer == null) { - return false; - } - } - - if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { - inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; - return false; - } - - int result; - if (waitingForKeys) { - // We've already read an encrypted sample into buffer, and are waiting for keys. - result = C.RESULT_BUFFER_READ; - } else { - result = readSource(formatHolder, inputBuffer, false); - } - - if (result == C.RESULT_NOTHING_READ) { - return false; - } - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; - } - if (inputBuffer.isEndOfStream()) { - inputStreamEnded = true; - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - return false; - } - boolean bufferEncrypted = inputBuffer.isEncrypted(); - waitingForKeys = shouldWaitForKeys(bufferEncrypted); - if (waitingForKeys) { - return false; - } - inputBuffer.flip(); - inputBuffer.colorInfo = formatHolder.format.colorInfo; - decoder.queueInputBuffer(inputBuffer); - buffersInCodecCount++; - decoderReceivedBuffers = true; - decoderCounters.inputBufferCount++; - inputBuffer = null; - return true; - } - - private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { - return false; - } - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; - } - - private void flushDecoder() throws ExoPlaybackException { - waitingForKeys = false; - forceRenderFrame = false; - buffersInCodecCount = 0; - if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { - releaseDecoder(); - maybeInitDecoder(); - } else { - inputBuffer = null; - if (outputBuffer != null) { - outputBuffer.release(); - outputBuffer = null; - } - if (nextOutputBuffer != null) { - nextOutputBuffer.release(); - nextOutputBuffer = null; - } - decoder.flush(); - decoderReceivedBuffers = false; - } - } @Override public boolean isEnded() { @@ -605,42 +341,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private void maybeInitDecoder() throws ExoPlaybackException { - if (decoder != null) { - return; - } + /** + * Called when a decoder has been created and configured. + * + *

      The default implementation is a no-op. + * + * @param name The name of the decoder that was initialized. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. + * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. + */ + @CallSuper + protected void onDecoderInitialized( + String name, long initializedTimestampMs, long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + } - drmSession = pendingDrmSession; - ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - mediaCrypto = drmSession.getMediaCrypto(); - if (mediaCrypto == null) { - DrmSessionException drmError = drmSession.getError(); - if (drmError != null) { - throw ExoPlaybackException.createForRenderer(drmError, getIndex()); - } - // The drm session isn't open yet. - return; + /** + * Flushes the decoder. + * + * @throws ExoPlaybackException If an error occurs reinitializing a decoder. + */ + @CallSuper + protected void flushDecoder() throws ExoPlaybackException { + waitingForKeys = false; + forceRenderFrame = false; + buffersInCodecCount = 0; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; } - } - - try { - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - mediaCrypto, disableLoopFilter); - decoder.setOutputMode(outputMode); - TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - decoderCounters.decoderInitCount++; - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + if (nextOutputBuffer != null) { + nextOutputBuffer.release(); + nextOutputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; } } - private void releaseDecoder() { + /** Releases the decoder. */ + @CallSuper + protected void releaseDecoder() { if (decoder == null) { return; } @@ -657,7 +404,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer { buffersInCodecCount = 0; } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + /** + * Called when a new format is read from the upstream source. + * + * @param newFormat The new format. + * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. + */ + @CallSuper + protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; @@ -692,6 +446,147 @@ public final class LibvpxVideoRenderer extends BaseRenderer { eventDispatcher.inputFormatChanged(format); } + /** + * Called immediately before an input buffer is queued into the decoder. + * + *

      The default implementation is a no-op. + * + * @param buffer The buffer that will be queued. + */ + protected void onQueueInputBuffer(VpxInputBuffer buffer) { + // Do nothing. + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + protected void onProcessedOutputBuffer(long presentationTimeUs) { + buffersInCodecCount--; + } + + /** + * Returns whether the current frame should be dropped. + * + * @param outputBufferTimeUs The timestamp of the current output buffer. + * @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET} + * if the next output buffer is unavailable. + * @param positionUs The current playback position. + * @param joiningDeadlineMs The joining deadline. + * @return Returns whether to drop the current output buffer. + */ + protected boolean shouldDropOutputBuffer( + long outputBufferTimeUs, + long nextOutputBufferTimeUs, + long positionUs, + long joiningDeadlineMs) { + return isBufferLate(outputBufferTimeUs - positionUs) + && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); + } + + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + */ + protected boolean shouldDropBuffersToKeyframe(long earlyUs) { + return isBufferVeryLate(earlyUs); + } + + /** + * Skips the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to skip. + */ + protected void skipOutputBuffer(VpxOutputBuffer outputBuffer) { + decoderCounters.skippedOutputBufferCount++; + outputBuffer.release(); + } + + /** + * Drops the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to drop. + */ + protected void dropOutputBuffer(VpxOutputBuffer outputBuffer) { + updateDroppedBufferCounters(1); + outputBuffer.release(); + } + + /** + * Renders the specified output buffer. + * + *

      The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VpxOutputBuffer#release()} either immediately or in the future. + * + * @param outputBuffer The buffer to render. + */ + protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) { + int bufferMode = outputBuffer.mode; + boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; + boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; + if (!renderRgb && !renderYuv) { + dropOutputBuffer(outputBuffer); + } else { + maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); + if (renderRgb) { + renderRgbFrame(outputBuffer, scaleToFit); + outputBuffer.release(); + } else /* renderYuv */ { + outputBufferRenderer.setOutputBuffer(outputBuffer); + // The renderer will release the buffer. + } + consecutiveDroppedFrameCount = 0; + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); + } + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param positionUs The current playback position, in microseconds. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the decoder. + */ + protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushDecoder(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = + Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); + if (droppedFrames >= maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + } + + // PlayerMessage.Target implementation. + @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { @@ -703,7 +598,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private void setOutput(Surface surface, VpxOutputBufferRenderer outputBufferRenderer) { + // Internal methods. + + private void setOutput( + @Nullable Surface surface, @Nullable VpxOutputBufferRenderer outputBufferRenderer) { // At most one output may be non-null. Both may be null if the output is being cleared. Assertions.checkState(surface == null || outputBufferRenderer == null); if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) { @@ -737,6 +635,238 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; + } + + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } + // The drm session isn't open yet. + return; + } + } + + try { + long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createVpxDecoder"); + decoder = + new VpxDecoder( + NUM_INPUT_BUFFERS, + NUM_OUTPUT_BUFFERS, + INITIAL_INPUT_BUFFER_SIZE, + mediaCrypto, + disableLoopFilter); + decoder.setOutputMode(outputMode); + TraceUtil.endSection(); + long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); + onDecoderInitialized( + decoder.getName(), + decoderInitializedTimestamp, + decoderInitializedTimestamp - decoderInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + } + + private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { + if (decoder == null + || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + + int result; + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer, false); + } + + if (result == C.RESULT_NOTHING_READ) { + return false; + } + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return false; + } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } + inputBuffer.flip(); + inputBuffer.colorInfo = formatHolder.format.colorInfo; + onQueueInputBuffer(inputBuffer); + decoder.queueInputBuffer(inputBuffer); + buffersInCodecCount++; + decoderReceivedBuffers = true; + decoderCounters.inputBufferCount++; + inputBuffer = null; + return true; + } + + /** + * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link + * #processOutputBuffer(long)}. + * + * @param positionUs The player's current position. + * @return Whether it may be possible to drain more output data. + * @throws ExoPlaybackException If an error occurs draining the output buffer. + */ + private boolean drainOutputBuffer(long positionUs) + throws ExoPlaybackException, VpxDecoderException { + // Acquire outputBuffer either from nextOutputBuffer or from the decoder. + if (outputBuffer == null) { + if (nextOutputBuffer != null) { + outputBuffer = nextOutputBuffer; + nextOutputBuffer = null; + } else { + outputBuffer = decoder.dequeueOutputBuffer(); + } + if (outputBuffer == null) { + return false; + } + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; + } + + if (nextOutputBuffer == null) { + nextOutputBuffer = decoder.dequeueOutputBuffer(); + } + + if (outputBuffer.isEndOfStream()) { + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + outputBuffer = null; + outputStreamEnded = true; + } + return false; + } + + return processOutputBuffer(positionUs); + } + + /** + * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns + * whether it may be possible to process another output buffer. + * + * @param positionUs The player's current position. + * @return Whether it may be possible to drain another output buffer. + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + private boolean processOutputBuffer(long positionUs) throws ExoPlaybackException { + if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(outputBuffer.timeUs - positionUs)) { + forceRenderFrame = false; + skipOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + return true; + } + return false; + } + + if (forceRenderFrame) { + forceRenderFrame = false; + renderOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + return true; + } + + long nextOutputBufferTimeUs = + nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() + ? nextOutputBuffer.timeUs + : C.TIME_UNSET; + + long earlyUs = outputBuffer.timeUs - positionUs; + if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) { + forceRenderFrame = true; + return false; + } else if (shouldDropOutputBuffer( + outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) { + dropOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + return true; + } + + // If we have yet to render a frame to the current output (either initially or immediately + // following a seek), render one irrespective of the state or current position. + if (!renderedFirstFrame || (getState() == STATE_STARTED && earlyUs <= 30000)) { + renderOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + } + + return false; + } + + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + return false; + } + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; + } + + private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { + if (bitmap == null + || bitmap.getWidth() != outputBuffer.width + || bitmap.getHeight() != outputBuffer.height) { + bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); + } + bitmap.copyPixelsFromBuffer(outputBuffer.data); + Canvas canvas = surface.lockCanvas(null); + if (scale) { + canvas.scale( + ((float) canvas.getWidth()) / outputBuffer.width, + ((float) canvas.getHeight()) / outputBuffer.height); + } + canvas.drawBitmap(bitmap, 0, 0, null); + surface.unlockCanvasAndPost(canvas); + } + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; From 13fbe7b2f23c038cadc10e264e288c7143ca7b68 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 9 Jan 2018 09:46:51 -0800 Subject: [PATCH 257/417] Fix cronet extension package name ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181331715 --- extensions/cronet/src/androidTest/AndroidManifest.xml | 4 ++-- extensions/cronet/src/main/AndroidManifest.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml index 6c4014873d..453cc68478 100644 --- a/extensions/cronet/src/androidTest/AndroidManifest.xml +++ b/extensions/cronet/src/androidTest/AndroidManifest.xml @@ -16,7 +16,7 @@ + package="com.google.android.exoplayer2.ext.cronet"> @@ -28,6 +28,6 @@ + android:targetPackage="com.google.android.exoplayer2.ext.cronet"/> diff --git a/extensions/cronet/src/main/AndroidManifest.xml b/extensions/cronet/src/main/AndroidManifest.xml index c81d95f104..5ba54999f4 100644 --- a/extensions/cronet/src/main/AndroidManifest.xml +++ b/extensions/cronet/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ --> + package="com.google.android.exoplayer2.ext.cronet"> From 11bae0af5a2ca1cf52ffa440b4ff564d8df7c696 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jan 2018 02:48:27 -0800 Subject: [PATCH 258/417] Add minimal logging for SCTE-35 splice commands in the Demo App ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181440439 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index fa22130eea..7dc7a3567f 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; +import com.google.android.exoplayer2.metadata.scte35.SpliceCommand; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -429,6 +430,10 @@ import java.util.Locale; EventMessage eventMessage = (EventMessage) entry; Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); + } else if (entry instanceof SpliceCommand) { + String description = + String.format("SCTE-35 splice command: type=%s.", entry.getClass().getSimpleName()); + Log.d(TAG, prefix + description); } } } From 1fc250a9f3c877bb2704096c7848f9687bc15847 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 10 Jan 2018 02:51:40 -0800 Subject: [PATCH 259/417] Make CacheUtil documentation clearer Also fixed some other Cache related javadoc. Issue: #3374 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181440687 --- .../exoplayer2/upstream/cache/Cache.java | 34 +++++++---- .../upstream/cache/CacheDataSink.java | 2 +- .../exoplayer2/upstream/cache/CacheUtil.java | 14 +++-- .../upstream/cache/CachedContent.java | 2 +- .../upstream/cache/SimpleCache.java | 6 +- .../upstream/cache/SimpleCacheSpan.java | 59 +++++++++++++++++-- .../upstream/cache/CacheAsserts.java | 22 ++++++- .../upstream/cache/CacheUtilTest.java | 2 +- .../upstream/cache/SimpleCacheTest.java | 16 ++--- .../exoplayer2/testutil/CacheAsserts.java | 22 ++++++- 10 files changed, 138 insertions(+), 41 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 76481bbdf7..171aa0878a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -127,23 +127,24 @@ public interface Cache { /** * A caller should invoke this method when they require data from a given position for a given * key. - *

      - * If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} + * + *

      If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller * may read from the cache file, but does not acquire any locks. - *

      - * If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} + * + *

      If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} * defines a hole in the cache starting at {@code position} into which the caller may write as it * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock. * Whilst the caller holds the lock it may write data into the hole. It may split data into - * multiple files. When the caller has finished writing a file it should commit it to the cache - * by calling {@link #commitFile(File)}. When the caller has finished writing, it must release - * the lock by calling {@link #releaseHoleSpan}. + * multiple files. When the caller has finished writing a file it should commit it to the cache by + * calling {@link #commitFile(File)}. When the caller has finished writing, it must release the + * lock by calling {@link #releaseHoleSpan}. * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. * @throws InterruptedException If the thread was interrupted. + * @throws CacheException If an error is encountered. */ CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; @@ -154,8 +155,10 @@ public interface Cache { * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. + * @throws CacheException If an error is encountered. */ - @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; + @Nullable + CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; /** * Obtains a cache file into which data can be written. Must only be called when holding a @@ -166,14 +169,16 @@ public interface Cache { * @param maxLength The maximum length of the data to be written. Used only to ensure that there * is enough space in the cache. * @return The file into which data should be written. + * @throws CacheException If an error is encountered. */ File startFile(String key, long position, long maxLength) throws CacheException; /** - * Commits a file into the cache. Must only be called when holding a corresponding hole - * {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} + * Commits a file into the cache. Must only be called when holding a corresponding hole {@link + * CacheSpan} obtained from {@link #startReadWrite(String, long)} * * @param file A newly written cache file. + * @throws CacheException If an error is encountered. */ void commitFile(File file) throws CacheException; @@ -189,6 +194,7 @@ public interface Cache { * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file. * * @param span The {@link CacheSpan} to remove. + * @throws CacheException If an error is encountered. */ void removeSpan(CacheSpan span) throws CacheException; @@ -210,15 +216,16 @@ public interface Cache { * @param key The cache key for the data. * @param position The starting position of the data. * @param length The maximum length of the data to be returned. - * @return the length of the cached or not cached data block length. + * @return The length of the cached or not cached data block length. */ - long getCachedBytes(String key, long position, long length); + long getCachedLength(String key, long position, long length); /** * Sets the content length for the given key. * * @param key The cache key for the data. * @param length The length of the data. + * @throws CacheException If an error is encountered. */ void setContentLength(String key, long length) throws CacheException; @@ -227,7 +234,8 @@ public interface Cache { * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. * * @param key The cache key for the data. + * @return The content length for the given key if one set, or {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. */ long getContentLength(String key); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 1af690e10f..57f5a6ad93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -37,7 +37,7 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { - /** Default buffer size. */ + /** Default buffer size in bytes. */ public static final int DEFAULT_BUFFER_SIZE = 20480; private final Cache cache; 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 2bf5cde8e0..22150f8e78 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 @@ -89,8 +89,8 @@ public final class CacheUtil { counters.alreadyCachedBytes = 0; counters.newlyCachedBytes = 0; while (left != 0) { - long blockLength = cache.getCachedBytes(key, start, - left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); + long blockLength = + cache.getCachedLength(key, start, left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { counters.alreadyCachedBytes += blockLength; } else { @@ -126,6 +126,12 @@ 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. * + *

      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. + * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. @@ -164,8 +170,8 @@ public final class CacheUtil { long start = dataSpec.absoluteStreamPosition; long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); while (left != 0) { - long blockLength = cache.getCachedBytes(key, start, - left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); + long blockLength = + cache.getCachedLength(key, start, left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { // Skip already cached data. } else { 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 fb59d23666..34884a457d 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 @@ -125,7 +125,7 @@ import java.util.TreeSet; * @param length The maximum length of the data to be returned. * @return the length of the cached or not cached data block length. */ - public long getCachedBytes(long position, long length) { + public long getCachedBytesLength(long position, long length) { SimpleCacheSpan span = getSpan(position); if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index ffac8a35f1..9d4a661343 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -385,13 +385,13 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length; + return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; } @Override - public synchronized long getCachedBytes(String key, long position, long length) { + public synchronized long getCachedLength(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length; + return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 8c5b7e26e7..e12d876ce1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -35,19 +36,50 @@ import java.util.regex.Pattern; private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile( "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL); - public static File getCacheFile(File cacheDir, int id, long position, - long lastAccessTimestamp) { + /** + * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code + * lastAccessTimestamp}. + * + * @param cacheDir The parent abstract pathname. + * @param id The cache file id. + * @param position The position of the stored data in the original stream. + * @param lastAccessTimestamp The last access timestamp. + * @return The cache file. + */ + public static File getCacheFile(File cacheDir, int id, long position, long lastAccessTimestamp) { return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX); } + /** + * Creates a lookup span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @return The span. + */ public static SimpleCacheSpan createLookup(String key, long position) { return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); } + /** + * Creates an open hole span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @return The span. + */ public static SimpleCacheSpan createOpenHole(String key, long position) { return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); } + /** + * Creates a closed hole span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}. + * @return The span. + */ public static SimpleCacheSpan createClosedHole(String key, long position, long length) { return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null); } @@ -60,6 +92,7 @@ import java.util.regex.Pattern; * @return The span, or null if the file name is not correctly formatted, or if the id is not * present in the content index. */ + @Nullable public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { @@ -81,6 +114,15 @@ import java.util.regex.Pattern; Long.parseLong(matcher.group(3)), file); } + /** + * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}. + * + * @param file The cache file. + * @param index Cached content index. + * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the + * file can not be renamed. + */ + @Nullable private static File upgradeFile(File file, CachedContentIndex index) { String key; String filename = file.getName(); @@ -106,8 +148,17 @@ import java.util.regex.Pattern; return newCacheFile; } - private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp, - File file) { + /** + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * #isCached} is false. + * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. + */ + private SimpleCacheSpan( + String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { super(key, position, length, lastAccessTimestamp, file); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java index 65850a13e7..c31cd0384e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java @@ -31,7 +31,11 @@ import java.util.ArrayList; /** Assertion methods for {@link com.google.android.exoplayer2.upstream.cache.Cache}. */ /* package */ final class CacheAsserts { - /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { ArrayList allData = fakeDataSet.getAllData(); Uri[] uris = new Uri[allData.size()]; @@ -43,6 +47,8 @@ import java.util.ArrayList; /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) throws IOException { @@ -55,6 +61,8 @@ import java.util.ArrayList; /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { @@ -67,7 +75,11 @@ import java.util.ArrayList; assertThat(cache.getCacheSpace()).isEqualTo(totalLength); } - /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { for (Uri uri : uris) { @@ -75,7 +87,11 @@ import java.util.ArrayList; } } - /** Asserts that the cache contains the given data for {@code uriString}. */ + /** + * Asserts that the cache contains the given data for {@code uriString} or not. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 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 c8231ec4ac..250e09bab4 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 @@ -72,7 +72,7 @@ public final class CacheUtilTest { } @Override - public long getCachedBytes(String key, long position, long length) { + public long getCachedLength(String key, long position, long length) { for (int i = 0; i < spansAndGaps.length; i++) { int spanOrGap = spansAndGaps[i]; if (position < spanOrGap) { 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 75a80185b9..e62676fc9d 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 @@ -218,36 +218,36 @@ public class SimpleCacheTest { } @Test - public void testGetCachedBytes() throws Exception { + public void testGetCachedLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); // No cached bytes, returns -'length' - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(-100); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(-100); // Position value doesn't affect the return value - assertThat(simpleCache.getCachedBytes(KEY_1, 20, 100)).isEqualTo(-100); + assertThat(simpleCache.getCachedLength(KEY_1, 20, 100)).isEqualTo(-100); addCache(simpleCache, KEY_1, 0, 15); // Returns the length of a single span - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(15); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(15); // Value is capped by the 'length' - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 10)).isEqualTo(10); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 10)).isEqualTo(10); addCache(simpleCache, KEY_1, 15, 35); // Returns the length of two adjacent spans - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); + 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.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50); // Returns length of hole up to the next cached span - assertThat(simpleCache.getCachedBytes(KEY_1, 55, 100)).isEqualTo(-5); + assertThat(simpleCache.getCachedLength(KEY_1, 55, 100)).isEqualTo(-5); simpleCache.releaseHoleSpan(cacheSpan); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index eb53191dc8..2174de1fd5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -36,7 +36,11 @@ import java.util.ArrayList; */ public final class CacheAsserts { - /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { ArrayList allData = fakeDataSet.getAllData(); Uri[] uris = new Uri[allData.size()]; @@ -48,6 +52,8 @@ public final class CacheAsserts { /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) throws IOException { @@ -60,6 +66,8 @@ public final class CacheAsserts { /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { @@ -72,7 +80,11 @@ public final class CacheAsserts { assertEquals(totalLength, cache.getCacheSpace()); } - /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { for (Uri uri : uris) { @@ -80,7 +92,11 @@ public final class CacheAsserts { } } - /** Asserts that the cache contains the given data for {@code uriString}. */ + /** + * Asserts that the cache contains the given data for {@code uriString}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); From 214d46d95781401eaad48eb2510200b63651a1e9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 10 Jan 2018 06:13:25 -0800 Subject: [PATCH 260/417] Set selection flags on image sample formats. Issue: #3008 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181455340 --- .../com/google/android/exoplayer2/Format.java | 40 ++++++++++++++++--- .../extractor/mkv/MatroskaExtractor.java | 12 +++++- .../extractor/ts/DvbSubtitleReader.java | 13 ++++-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 4bd23e2cb6..7799f411a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -324,11 +324,41 @@ public final class Format implements Parcelable { // Image. - public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, List initializationData, String language, DrmInitData drmInitData) { - return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, + public static Format createImageSampleFormat( + String id, + String sampleMimeType, + String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + List initializationData, + String language, + DrmInitData drmInitData) { + return new Format( + id, + null, + sampleMimeType, + codecs, + bitrate, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + null, + NO_VALUE, + null, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + selectionFlags, + language, + NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + initializationData, + drmInitData, null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 0eb7009c47..57128f45f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1867,8 +1867,16 @@ public final class MatroskaExtractor implements Extractor { || MimeTypes.APPLICATION_PGS.equals(mimeType) || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; - format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, initializationData, language, drmInitData); + format = + Format.createImageSampleFormat( + Integer.toString(trackId), + mimeType, + null, + Format.NO_VALUE, + selectionFlags, + initializationData, + language, + drmInitData); } else { throw new ParserException("Unexpected MIME type."); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index e00c63a354..0944d1810e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -58,9 +58,16 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i); idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); - output.format(Format.createImageSampleFormat(idGenerator.getFormatId(), - MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE, - Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, null)); + output.format( + Format.createImageSampleFormat( + idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, + null, + Format.NO_VALUE, + 0, + Collections.singletonList(subtitleInfo.initializationData), + subtitleInfo.language, + null)); outputs[i] = output; } } From f977ba256fdef5fd808d0ead6fdfea0a10719149 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Jan 2018 07:30:25 -0800 Subject: [PATCH 261/417] Add ad insertion discontinuity reason. This it to distinguish between actual period transitions and the transitions occuring to and from ads within one timeline period. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181606023 --- RELEASENOTES.md | 4 +++ .../android/exoplayer2/demo/EventLogger.java | 2 ++ .../exoplayer2/ExoPlayerImplInternal.java | 6 ++++- .../com/google/android/exoplayer2/Player.java | 25 ++++++++++--------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80381075b8..b0907f0a42 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons + reported in `Eventlistener.onPositionDiscontinuity` to distinguish + transitions to and from ads within one period from transitions between + periods. * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow more customization of the message. Now supports setting a message delivery playback position and/or a delivery handler. diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 7dc7a3567f..9d28aa47f0 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -528,6 +528,8 @@ import java.util.Locale; return "SEEK"; case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: return "SEEK_ADJUSTMENT"; + case Player.DISCONTINUITY_REASON_AD_INSERTION: + return "AD_INSERTION"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: 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 8fd508a2f0..2647a44dee 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 @@ -1448,11 +1448,15 @@ import java.util.Collections; // If we advance more than one period at a time, notify listeners after each update. maybeNotifyPlaybackInfoChanged(); } + int discontinuityReason = + playingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); - playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); updatePlaybackPositions(); advancedPlayingPeriod = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index b3ae4c28c6..97cd9449d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -251,31 +251,32 @@ public interface Player { */ int REPEAT_MODE_ALL = 2; - /** - * Reasons for position discontinuities. - */ + /** Reasons for position discontinuities. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) + @IntDef({ + DISCONTINUITY_REASON_PERIOD_TRANSITION, + DISCONTINUITY_REASON_SEEK, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, + DISCONTINUITY_REASON_AD_INSERTION, + DISCONTINUITY_REASON_INTERNAL + }) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may * be the same as it was before the discontinuity in case the current period is repeated. */ int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0; - /** - * Seek within the current period or to another period. - */ + /** Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; /** * Seek adjustment due to being unable to seek to the requested position or because the seek was * permitted to be inexact. */ int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; - /** - * Discontinuity introduced internally by the source. - */ - int DISCONTINUITY_REASON_INTERNAL = 3; + /** Discontinuity to or from an ad within one period in the timeline. */ + int DISCONTINUITY_REASON_AD_INSERTION = 3; + /** Discontinuity introduced internally by the source. */ + int DISCONTINUITY_REASON_INTERNAL = 4; /** * Reasons for timeline and/or manifest changes. From ad3e6ef4cdac12d1983f4998e594a70fbe28eee8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jan 2018 07:52:49 -0800 Subject: [PATCH 262/417] Add missing onLoadStarted callback to Extractor and SingleSample media period. We added the other callbacks some time ago, but didn't include onLoadStarted. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181743764 --- .../source/ExtractorMediaPeriod.java | 19 +++++++++++++++---- .../source/MediaSourceEventListener.java | 2 +- .../source/SingleSampleMediaPeriod.java | 19 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 76d9d22648..bc84546c83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -168,6 +168,7 @@ import java.util.Arrays; sampleQueues = new SampleQueue[0]; pendingResetPositionUs = C.TIME_UNSET; length = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; // Assume on-demand for MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, until prepared. actualMinLoadableRetryCount = minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA @@ -436,7 +437,7 @@ import java.util.Arrays; /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, - /* mediaStartTimeUs= */ 0, + /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, elapsedRealtimeMs, loadDurationMs, @@ -456,7 +457,7 @@ import java.util.Arrays; /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, - /* mediaStartTimeUs= */ 0, + /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, elapsedRealtimeMs, loadDurationMs, @@ -483,7 +484,7 @@ import java.util.Arrays; /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, - /* mediaStartTimeUs= */ 0, + /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, elapsedRealtimeMs, loadDurationMs, @@ -595,7 +596,17 @@ import java.util.Arrays; pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - loader.startLoading(loadable, this, actualMinLoadableRetryCount); + long elapsedRealtimeMs = loader.startLoading(loadable, this, actualMinLoadableRetryCount); + eventDispatcher.loadStarted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs); } private void configureRetry(ExtractingLoadable loadable) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 4d500f94bd..9fc2572b55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -44,7 +44,7 @@ public interface MediaSourceEventListener { * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if * the load is not for media data. * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. + * load is not for media data or the end time is unknown. * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. */ void onLoadStarted( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index e76de60b86..cc7179ae18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -133,10 +133,21 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } - loader.startLoading( - new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), - this, - minLoadableRetryCount); + long elapsedRealtimeMs = + loader.startLoading( + new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), + this, + minLoadableRetryCount); + eventDispatcher.loadStarted( + dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs); return true; } From 7a5640304601f5518060689785ad5fa27b82f25f Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jan 2018 09:26:04 -0800 Subject: [PATCH 263/417] Add missing downstreamFormatChanged to Extractor and SingleSample media source. These haven't been included in the recent changes but can be reported as soon as the first sample of each stream is read. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181753141 --- .../source/ExtractorMediaPeriod.java | 37 ++++++++++++++++--- .../source/SingleSampleMediaPeriod.java | 15 ++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index bc84546c83..c995884515 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -105,6 +105,7 @@ import java.util.Arrays; private long durationUs; private boolean[] trackEnabledStates; private boolean[] trackIsAudioVideoFlags; + private boolean[] trackFormatNotificationSent; private boolean haveAudioVideoTracks; private long length; @@ -398,8 +399,13 @@ import java.util.Arrays; if (suppressRead()) { return C.RESULT_NOTHING_READ; } - return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + int result = + sampleQueues[track].read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + maybeNotifyTrackFormat(track); + } + return result; } /* package */ int skipData(int track, long positionUs) { @@ -407,11 +413,31 @@ import java.util.Arrays; return 0; } SampleQueue sampleQueue = sampleQueues[track]; + int skipCount; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - return sampleQueue.advanceToEnd(); + skipCount = sampleQueue.advanceToEnd(); } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; + skipCount = sampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } + } + if (skipCount > 0) { + maybeNotifyTrackFormat(track); + } + return skipCount; + } + + private void maybeNotifyTrackFormat(int track) { + if (!trackFormatNotificationSent[track]) { + Format trackFormat = tracks.get(track).getFormat(0); + eventDispatcher.downstreamFormatChanged( + MimeTypes.getTrackType(trackFormat.sampleMimeType), + trackFormat, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + trackFormatNotificationSent[track] = true; } } @@ -556,6 +582,7 @@ import java.util.Arrays; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackIsAudioVideoFlags = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount]; + trackFormatNotificationSent = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { Format trackFormat = sampleQueues[i].getUpstreamFormat(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index cc7179ae18..36e5d910c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -252,6 +253,7 @@ import java.util.Arrays; private static final int STREAM_STATE_END_OF_STREAM = 2; private int streamState; + private boolean formatSent; public void reset() { if (streamState == STREAM_STATE_END_OF_STREAM) { @@ -287,6 +289,7 @@ import java.util.Arrays; buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); buffer.ensureSpaceForWrite(sampleSize); buffer.data.put(sampleData, 0, sampleSize); + sendFormat(); } else { buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); } @@ -300,11 +303,23 @@ import java.util.Arrays; public int skipData(long positionUs) { if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_END_OF_STREAM; + sendFormat(); return 1; } return 0; } + private void sendFormat() { + if (!formatSent) { + eventDispatcher.downstreamFormatChanged( + MimeTypes.getTrackType(format.sampleMimeType), + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaTimeUs= */ 0); + formatSent = true; + } + } } /* package */ static final class SourceLoadable implements Loadable { From f20c158a38ec8f4aa4775f0f6ee9a34eeda62f61 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 Jan 2018 03:10:17 -0800 Subject: [PATCH 264/417] Fix IMA sample ad tag URL Issue: #3703 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181947101 --- demos/main/src/main/assets/media.exolist.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 38a0c577ae..15183a4a8b 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", From 141f3aa836daf22e9daf514ca02dfe0a9474bfb8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Jan 2018 14:13:30 +0000 Subject: [PATCH 265/417] Simplify PGS captions + sync with internal tree --- RELEASENOTES.md | 9 +- demos/main/src/main/assets/media.exolist.json | 2 +- extensions/vp9/README.md | 1 + .../jni/generate_libvpx_android_configs.sh | 5 +- .../text/SubtitleDecoderFactory.java | 118 ++++----- .../exoplayer2/text/dvb/DvbDecoder.java | 4 +- .../exoplayer2/text/pgs/PgsBuilder.java | 232 ------------------ .../exoplayer2/text/pgs/PgsDecoder.java | 229 ++++++++++++++++- .../exoplayer2/text/pgs/PgsSubtitle.java | 53 ++-- 9 files changed, 317 insertions(+), 336 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b0907f0a42..6d4347490e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,13 +37,14 @@ HLS source to finish preparation without downloading any chunks, which can significantly reduce initial buffering time ([#3149](https://github.com/google/ExoPlayer/issues/3149)). -* DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods - with a builder. -* DefaultTrackSelector: Support disabling of individual text track selection - flags. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). +* Captions: Initial support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. * IMA extension: Add support for playing non-Extractor content MediaSources in diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 15183a4a8b..38a0c577ae 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 8dc4974430..9601829c91 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. + ``` NDK_PATH="" ``` diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh index 4aabf2379e..eab6862555 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -102,7 +102,10 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags="-isystem $ndk/sysroot/usr/include/arm-linux-androideabi -isystem $ndk/sysroot/usr/include" + ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \ + -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \ + -isystem $ndk/sysroot/usr/include \ + " rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 4720a67bba..139e403844 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -53,67 +53,69 @@ public interface SubtitleDecoderFactory { /** * Default {@link SubtitleDecoderFactory} implementation. - *

      - * The formats supported by this factory are: + * + *

      The formats supported by this factory are: + * *

        - *
      • WebVTT ({@link WebvttDecoder})
      • - *
      • WebVTT (MP4) ({@link Mp4WebvttDecoder})
      • - *
      • TTML ({@link TtmlDecoder})
      • - *
      • SubRip ({@link SubripDecoder})
      • - *
      • SSA/ASS ({@link SsaDecoder})
      • - *
      • TX3G ({@link Tx3gDecoder})
      • - *
      • Cea608 ({@link Cea608Decoder})
      • - *
      • Cea708 ({@link Cea708Decoder})
      • - *
      • DVB ({@link DvbDecoder})
      • + *
      • WebVTT ({@link WebvttDecoder}) + *
      • WebVTT (MP4) ({@link Mp4WebvttDecoder}) + *
      • TTML ({@link TtmlDecoder}) + *
      • SubRip ({@link SubripDecoder}) + *
      • SSA/ASS ({@link SsaDecoder}) + *
      • TX3G ({@link Tx3gDecoder}) + *
      • Cea608 ({@link Cea608Decoder}) + *
      • Cea708 ({@link Cea708Decoder}) + *
      • DVB ({@link DvbDecoder}) + *
      • PGS ({@link PgsDecoder}) *
      */ - SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { + SubtitleDecoderFactory DEFAULT = + new SubtitleDecoderFactory() { - @Override - public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; - return MimeTypes.TEXT_VTT.equals(mimeType) - || MimeTypes.TEXT_SSA.equals(mimeType) - || MimeTypes.APPLICATION_TTML.equals(mimeType) - || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) - || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) - || MimeTypes.APPLICATION_TX3G.equals(mimeType) - || MimeTypes.APPLICATION_CEA608.equals(mimeType) - || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) - || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) - || MimeTypes.APPLICATION_PGS.equals(mimeType); - } - - @Override - public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.TEXT_SSA: - return new SsaDecoder(format.initializationData); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel); - case MimeTypes.APPLICATION_DVBSUBS: - return new DvbDecoder(format.initializationData); - case MimeTypes.APPLICATION_PGS: - return new PgsDecoder(); - default: - throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - } - - }; + @Override + public boolean supportsFormat(Format format) { + String mimeType = format.sampleMimeType; + return MimeTypes.TEXT_VTT.equals(mimeType) + || MimeTypes.TEXT_SSA.equals(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) + || MimeTypes.APPLICATION_TX3G.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType) + || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); + } + @Override + public SubtitleDecoder createDecoder(Format format) { + switch (format.sampleMimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + return new Cea708Decoder(format.accessibilityChannel); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); + default: + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported format"); + } + } + }; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index dbdc0434a1..df5b19c052 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -19,9 +19,7 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for DVB Subtitles. - */ +/** A {@link SimpleSubtitleDecoder} for DVB subtitles. */ public final class DvbDecoder extends SimpleSubtitleDecoder { private final DvbParser parser; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java deleted file mode 100644 index e67178314d..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java +++ /dev/null @@ -1,232 +0,0 @@ -/* -* -* Sources for this implementation PGS decoding can be founder below -* -* http://exar.ch/suprip/hddvd.php -* http://forum.doom9.org/showthread.php?t=124105 -* http://www.equasys.de/colorconversion.html - */ - -package com.google.android.exoplayer2.text.pgs; - -import android.graphics.Bitmap; - -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.util.ParsableByteArray; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -class PgsBuilder { - - private static final int SECTION_PALETTE = 0x14; - private static final int SECTION_BITMAP_PICTURE = 0x15; - private static final int SECTION_IDENTIFIER = 0x16; - private static final int SECTION_END = 0x80; - - private List list = new ArrayList<>(); - private Holder holder = new Holder(); - - boolean readNextSection(ParsableByteArray buffer) { - - if (buffer.bytesLeft() < 3) - return false; - - int sectionId = buffer.readUnsignedByte(); - int sectionLength = buffer.readUnsignedShort(); - switch(sectionId) { - case SECTION_PALETTE: - holder.parsePaletteIndexes(buffer, sectionLength); - break; - case SECTION_BITMAP_PICTURE: - holder.fetchBitmapData(buffer, sectionLength); - break; - case SECTION_IDENTIFIER: - holder.fetchIdentifierData(buffer, sectionLength); - break; - case SECTION_END: - list.add(holder); - holder = new Holder(); - break; - default: - buffer.skipBytes(Math.min(sectionLength, buffer.bytesLeft())); - break; - } - return true; - } - - public Subtitle build() { - - if (list.isEmpty()) - return new PgsSubtitle(); - - Cue[] cues = new Cue[list.size()]; - long[] cueStartTimes = new long[list.size()]; - int index = 0; - for (Holder curr : list) { - cues[index] = curr.build(); - cueStartTimes[index++] = curr.start_time; - } - return new PgsSubtitle(cues, cueStartTimes); - } - - private class Holder { - - private int[] colors = null; - private ByteBuffer rle = null; - - Bitmap bitmap = null; - int plane_width = 0; - int plane_height = 0; - int bitmap_width = 0; - int bitmap_height = 0; - public int x = 0; - public int y = 0; - long start_time = 0; - - public Cue build() { - if (rle == null || !createBitmap(new ParsableByteArray(rle.array(), rle.position()))) - return null; - float left = (float) x / plane_width; - float top = (float) y / plane_height; - return new Cue(bitmap, left, Cue.ANCHOR_TYPE_START, top, Cue.ANCHOR_TYPE_START, - (float) bitmap_width / plane_width, (float) bitmap_height / plane_height); - } - - private void parsePaletteIndexes(ParsableByteArray buffer, int dataSize) { - // must be a multi of 5 for index, y, cb, cr, alpha - if (dataSize == 0 || (dataSize - 2) % 5 != 0) - return; - // skip first two bytes - buffer.skipBytes(2); - dataSize -= 2; - colors = new int[256]; - while (dataSize > 0) { - int index = buffer.readUnsignedByte(); - int color_y = buffer.readUnsignedByte() - 16; - int color_cr = buffer.readUnsignedByte() - 128; - int color_cb = buffer.readUnsignedByte() - 128; - int color_alpha = buffer.readUnsignedByte(); - dataSize -= 5; - if (index >= colors.length) - continue; - - int color_r = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 1.793 * color_cr), 0), 255); - int color_g = (int) Math.min(Math.max(Math.round(1.1644 * color_y + (-0.213 * color_cr) + (-0.533 * color_cb)), 0), 255); - int color_b = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 2.112 * color_cb), 0), 255); - //ARGB_8888 - colors[index] = (color_alpha << 24) | (color_r << 16) | (color_g << 8) | color_b; - } - } - - private void fetchBitmapData(ParsableByteArray buffer, int dataSize) { - if (dataSize <= 4) { - buffer.skipBytes(dataSize); - return; - } - // skip id field (2 bytes) - // skip version field - buffer.skipBytes(3); - dataSize -= 3; - - // check to see if this section is an appended section of the base section with - // width and height values - dataSize -= 1; // decrement first - if ((0x80 & buffer.readUnsignedByte()) > 0) { - if (dataSize < 3) { - buffer.skipBytes(dataSize); - return; - } - int full_len = buffer.readUnsignedInt24(); - dataSize -= 3; - if (full_len <= 4) { - buffer.skipBytes(dataSize); - return; - } - bitmap_width = buffer.readUnsignedShort(); - dataSize -= 2; - bitmap_height = buffer.readUnsignedShort(); - dataSize -= 2; - rle = ByteBuffer.allocate(full_len - 4); // don't include width & height - buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); - } else if (rle != null) { - int postSkip = dataSize > rle.capacity() ? dataSize - rle.capacity() : 0; - buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); - buffer.skipBytes(postSkip); - } - } - - private void fetchIdentifierData(ParsableByteArray buffer, int dataSize) { - if (dataSize < 4) { - buffer.skipBytes(dataSize); - return; - } - plane_width = buffer.readUnsignedShort(); - plane_height = buffer.readUnsignedShort(); - dataSize -= 4; - if (dataSize < 15) { - buffer.skipBytes(dataSize); - return; - } - // skip next 11 bytes - buffer.skipBytes(11); - x = buffer.readUnsignedShort(); - y = buffer.readUnsignedShort(); - dataSize -= 15; - buffer.skipBytes(dataSize); - } - - private boolean createBitmap(ParsableByteArray rle) { - if (bitmap_width == 0 || bitmap_height == 0 - || rle == null || rle.bytesLeft() == 0 - || colors == null || colors.length == 0) - return false; - int[] argb = new int[bitmap_width * bitmap_height]; - int currPixel = 0; - int nextbits, pixel_code, switchbits; - int number_of_pixels; - int line = 0; - while (rle.bytesLeft() > 0 && line < bitmap_height) { - boolean end_of_line = false; - do { - nextbits = rle.readUnsignedByte(); - if (nextbits != 0) { - pixel_code = nextbits; - number_of_pixels = 1; - } else { - switchbits = rle.readUnsignedByte(); - if ((switchbits & 0x80) == 0) { - pixel_code = 0; - if ((switchbits & 0x40) == 0) { - if (switchbits > 0) { - number_of_pixels = switchbits; - } else { - end_of_line = true; - ++line; - continue; - } - } else { - number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); - } - } else { - if ((switchbits & 0x40) == 0) { - number_of_pixels = switchbits & 0x3f; - pixel_code = rle.readUnsignedByte(); - } else { - number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); - pixel_code = rle.readUnsignedByte(); - } - } - } - Arrays.fill(argb, currPixel, currPixel + number_of_pixels, colors[pixel_code]); - currPixel += number_of_pixels; - } while (!end_of_line); - } - bitmap = Bitmap.createBitmap(argb, 0, bitmap_width, bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); - return bitmap != null; - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 04c3ecd0a3..7ad70397a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -1,26 +1,237 @@ +/* + * 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.text.pgs; +import android.graphics.Bitmap; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; -@SuppressWarnings("unused") -public class PgsDecoder extends SimpleSubtitleDecoder { +/** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ +public final class PgsDecoder extends SimpleSubtitleDecoder { + + private static final int SECTION_TYPE_PALETTE = 0x14; + private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15; + private static final int SECTION_TYPE_IDENTIFIER = 0x16; + private static final int SECTION_TYPE_END = 0x80; + + private final ParsableByteArray buffer; + private final CueBuilder cueBuilder; - @SuppressWarnings("unused") public PgsDecoder() { super("PgsDecoder"); + buffer = new ParsableByteArray(); + cueBuilder = new CueBuilder(); } @Override protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { - ParsableByteArray buffer = new ParsableByteArray(data, size); - PgsBuilder builder = new PgsBuilder(); - do { - if (!builder.readNextSection(buffer)) + buffer.reset(data, size); + cueBuilder.reset(); + ArrayList cues = new ArrayList<>(); + while (buffer.bytesLeft() >= 3) { + Cue cue = readNextSection(buffer, cueBuilder); + if (cue != null) { + cues.add(cue); + } + } + return new PgsSubtitle(Collections.unmodifiableList(cues)); + } + + private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { + int limit = buffer.limit(); + int sectionType = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + + int nextSectionPosition = buffer.getPosition() + sectionLength; + if (nextSectionPosition > limit) { + buffer.setPosition(limit); + return null; + } + + Cue cue = null; + switch (sectionType) { + case SECTION_TYPE_PALETTE: + cueBuilder.parsePaletteSection(buffer, sectionLength); break; - } while (buffer.bytesLeft() > 0); - return builder.build(); + case SECTION_TYPE_BITMAP_PICTURE: + cueBuilder.parseBitmapSection(buffer, sectionLength); + break; + case SECTION_TYPE_IDENTIFIER: + cueBuilder.parseIdentifierSection(buffer, sectionLength); + break; + case SECTION_TYPE_END: + cue = cueBuilder.build(); + cueBuilder.reset(); + break; + default: + break; + } + + buffer.setPosition(nextSectionPosition); + return cue; + } + + private static final class CueBuilder { + + private final ParsableByteArray bitmapData; + private final int[] colors; + + private boolean colorsSet; + private int planeWidth; + private int planeHeight; + private int bitmapX; + private int bitmapY; + private int bitmapWidth; + private int bitmapHeight; + + public CueBuilder() { + bitmapData = new ParsableByteArray(); + colors = new int[256]; + } + + private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) { + if ((sectionLength % 5) != 2) { + // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries. + return; + } + buffer.skipBytes(2); + + Arrays.fill(colors, 0); + int entryCount = sectionLength / 5; + for (int i = 0; i < entryCount; i++) { + int index = buffer.readUnsignedByte(); + int y = buffer.readUnsignedByte(); + int cr = buffer.readUnsignedByte(); + int cb = buffer.readUnsignedByte(); + int a = buffer.readUnsignedByte(); + int r = (int) (y + (1.40200 * (cr - 128))); + int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128))); + int b = (int) (y + (1.77200 * (cb - 128))); + colors[index] = + (a << 24) + | (Util.constrainValue(r, 0, 255) << 16) + | (Util.constrainValue(g, 0, 255) << 8) + | Util.constrainValue(b, 0, 255); + } + colorsSet = true; + } + + private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 4) { + return; + } + buffer.skipBytes(3); // Id (2 bytes), version (1 byte). + boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0; + sectionLength -= 4; + + if (isBaseSection) { + if (sectionLength < 7) { + return; + } + int totalLength = buffer.readUnsignedInt24() - 4; + if (totalLength < 4) { + return; + } + bitmapWidth = buffer.readUnsignedShort(); + bitmapHeight = buffer.readUnsignedShort(); + bitmapData.reset(totalLength - 4); + sectionLength -= 7; + } + + int position = bitmapData.getPosition(); + int limit = bitmapData.limit(); + if (position < limit && sectionLength > 0) { + int bytesToRead = Math.min(sectionLength, limit - position); + buffer.readBytes(bitmapData.data, position, bytesToRead); + bitmapData.setPosition(position + bytesToRead); + } + } + + private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 19) { + return; + } + planeWidth = buffer.readUnsignedShort(); + planeHeight = buffer.readUnsignedShort(); + buffer.skipBytes(11); + bitmapX = buffer.readUnsignedShort(); + bitmapY = buffer.readUnsignedShort(); + } + + public Cue build() { + if (planeWidth == 0 + || planeHeight == 0 + || bitmapWidth == 0 + || bitmapHeight == 0 + || bitmapData.limit() == 0 + || bitmapData.getPosition() != bitmapData.limit() + || !colorsSet) { + return null; + } + // Build the bitmapData. + bitmapData.setPosition(0); + int[] argbBitmapData = new int[bitmapWidth * bitmapHeight]; + int argbBitmapDataIndex = 0; + while (argbBitmapDataIndex < argbBitmapData.length) { + int colorIndex = bitmapData.readUnsignedByte(); + if (colorIndex != 0) { + argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex]; + } else { + int switchBits = bitmapData.readUnsignedByte(); + if (switchBits != 0) { + int runLength = + (switchBits & 0x40) == 0 + ? (switchBits & 0x3F) + : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte()); + int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()]; + Arrays.fill( + argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color); + argbBitmapDataIndex += runLength; + } + } + } + Bitmap bitmap = + Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + // Build the cue. + return new Cue( + bitmap, + (float) bitmapX / planeWidth, + Cue.ANCHOR_TYPE_START, + (float) bitmapY / planeHeight, + Cue.ANCHOR_TYPE_START, + (float) bitmapWidth / planeWidth, + (float) bitmapHeight / planeHeight); + } + + public void reset() { + planeWidth = 0; + planeHeight = 0; + bitmapX = 0; + bitmapY = 0; + bitmapWidth = 0; + bitmapHeight = 0; + bitmapData.reset(0); + colorsSet = false; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java index affb2aa15b..9f9af6b6a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java @@ -1,54 +1,51 @@ +/* + * 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.text.pgs; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; - -import java.util.Collections; import java.util.List; -public class PgsSubtitle implements Subtitle { +/** A representation of a PGS subtitle. */ +/* package */ final class PgsSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List cues; - PgsSubtitle() { - this.cues = null; - this.cueTimesUs = new long[0]; - } - - PgsSubtitle(Cue[] cues, long[] cueTimesUs) { + public PgsSubtitle(List cues) { this.cues = cues; - this.cueTimesUs = cueTimesUs; } @Override public int getNextEventTimeIndex(long timeUs) { - int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : -1; + return C.INDEX_UNSET; } @Override public int getEventTimeCount() { -return cueTimesUs.length; -} + return 1; + } @Override public long getEventTime(int index) { - Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + return 0; } @Override public List getCues(long timeUs) { - int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues == null || cues[index] == null) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. - return Collections.emptyList(); - } - else - return Collections.singletonList(cues[index]); + return cues; } } From 408bc08682328c50ba323ac74406983682464d4b Mon Sep 17 00:00:00 2001 From: eneim Date: Thu, 18 Jan 2018 09:32:46 +0900 Subject: [PATCH 266/417] Make Raw Resource Scheme to be public, accessible from outside. --- .../android/exoplayer2/upstream/RawResourceDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 0b7b85b8c3..941fa90e8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -58,7 +58,7 @@ public final class RawResourceDataSource implements DataSource { return Uri.parse(RAW_RESOURCE_SCHEME + ":///" + rawResourceId); } - private static final String RAW_RESOURCE_SCHEME = "rawresource"; + public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; private final TransferListener listener; From 88f8c768b0aca93da652d3955dda846dbc03b97f Mon Sep 17 00:00:00 2001 From: eneim Date: Thu, 18 Jan 2018 09:36:48 +0900 Subject: [PATCH 267/417] Also check if the Uri is built for raw resource data, and then create the RawResourceDataSource for it. --- .../exoplayer2/upstream/DefaultDataSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 853b40f73f..6d22f8b6c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -31,6 +31,9 @@ import java.lang.reflect.InvocationTargetException; * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a * local file URI). *
    • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). + *
    • rawresource: For fetching data from a raw resource in the applications' apk + * (e.g. rawresource:///resourceId, where rawResourceId is the integer identifier of the raw + * resource).
    • *
    • content: For fetching data from a content URI (e.g. content://authority/path/123). *
    • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension.
    • @@ -48,6 +51,7 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; + private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; private final TransferListener listener; @@ -60,6 +64,7 @@ public final class DefaultDataSource implements DataSource { private DataSource contentDataSource; private DataSource rtmpDataSource; private DataSource dataSchemeDataSource; + private DataSource rawResourceDataSource; private DataSource dataSource; @@ -134,6 +139,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getRtmpDataSource(); } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); + } else if (SCHEME_RAW.equals(scheme)) { + dataSource = getRawResourceDataSource(); } else { dataSource = baseDataSource; } @@ -213,4 +220,10 @@ public final class DefaultDataSource implements DataSource { return dataSchemeDataSource; } + private DataSource getRawResourceDataSource() { + if (rawResourceDataSource == null) { + rawResourceDataSource = new RawResourceDataSource(context, listener); + } + return rawResourceDataSource; + } } From 7b534cd9fda70290aac6f270291e5c28abb0fad4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 Jan 2018 06:22:08 -0800 Subject: [PATCH 268/417] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181962471 --- demos/main/src/main/assets/media.exolist.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 38a0c577ae..15183a4a8b 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", From ebfd5a7fe039d53108e8afcd6772f3371eef9aa7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 Jan 2018 07:04:15 -0800 Subject: [PATCH 269/417] Don't discard embedded queues beyond primary queue. ChunkSampleStream.seekToUs assumes that if we can seek within the primary sample queue, we can also seek within the embedded queues. This assumption can be violated fairly easily if discardBuffer is called with toKeyframe=true, since this can cause samples to be discarded from the embedded queues within the period for which a seek in the primary sample queue will succeed. This change fixes the issue. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181965902 --- .../exoplayer2/source/SampleMetadataQueue.java | 8 ++++++-- .../android/exoplayer2/source/SampleQueue.java | 5 +++++ .../exoplayer2/source/chunk/ChunkSampleStream.java | 13 +++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 65c443d425..54db9d7880 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -186,6 +186,11 @@ import com.google.android.exoplayer2.util.Util; return largestQueuedTimestampUs; } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ + public synchronized long getFirstTimestampUs() { + return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; + } + /** * Rewinds the read position to the first sample retained in the queue. */ @@ -487,8 +492,7 @@ import com.google.android.exoplayer2.util.Util; * Discards the specified number of samples. * * @param discardCount The number of samples to discard. - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. + * @return The corresponding offset up to which data should be discarded. */ private long discardSamples(int discardCount) { largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, 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 78b16bf377..a4feb924b8 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 @@ -226,6 +226,11 @@ public final class SampleQueue implements TrackOutput { return metadataQueue.getLargestQueuedTimestampUs(); } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ + public long getFirstTimestampUs() { + return metadataQueue.getFirstTimestampUs(); + } + /** * Rewinds the read position to the first sample in the queue. */ 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 947664720b..b0a2686ef6 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 @@ -120,11 +120,16 @@ public class ChunkSampleStream implements SampleStream, S * the specified position, rather than any sample before or at that position. */ public void discardBuffer(long positionUs, boolean toKeyframe) { + int oldFirstIndex = primarySampleQueue.getFirstIndex(); primarySampleQueue.discardTo(positionUs, toKeyframe, true); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardTo(positionUs, toKeyframe, embeddedTracksSelected[i]); + int newFirstIndex = primarySampleQueue.getFirstIndex(); + if (newFirstIndex > oldFirstIndex) { + long discardToUs = primarySampleQueue.getFirstTimestampUs(); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]); + } + discardDownstreamMediaChunks(newFirstIndex); } - discardDownstreamMediaChunks(primarySampleQueue.getFirstIndex()); } /** @@ -209,7 +214,7 @@ public class ChunkSampleStream implements SampleStream, S boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); if (seekInsideBuffer) { - // We succeeded. Discard samples and corresponding chunks prior to the seek position. + // We succeeded. Advance the embedded sample queues to the seek position. for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.rewind(); embeddedSampleQueue.advanceTo(positionUs, true, false); From cfed8791b0fb5698cf8d0b268e7746d1153cfb3a Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 15 Jan 2018 07:55:53 -0800 Subject: [PATCH 270/417] Send downStreamFormatChanged notification for embedded streams. This allows listeners to get notified of any change to the embedded tracks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181969023 --- .../source/chunk/ChunkSampleStream.java | 55 ++++++-- .../source/dash/DashMediaPeriod.java | 127 +++++++++++++----- .../source/smoothstreaming/SsMediaPeriod.java | 12 +- .../testutil/FakeAdaptiveMediaPeriod.java | 11 +- 4 files changed, 154 insertions(+), 51 deletions(-) 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 b0a2686ef6..e740e6607e 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 @@ -46,6 +46,7 @@ public class ChunkSampleStream implements SampleStream, S public final int primaryTrackType; private final int[] embeddedTrackTypes; + private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; @@ -65,9 +66,10 @@ public class ChunkSampleStream implements SampleStream, S /* package */ boolean loadingFinished; /** - * @param primaryTrackType The type of the primary track. One of the {@link C} - * {@code TRACK_TYPE_*} constants. + * @param primaryTrackType The type of the primary track. One of the {@link C} {@code + * TRACK_TYPE_*} constants. * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedTrackFormats The formats of the embedded tracks, or null. * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. @@ -76,11 +78,19 @@ public class ChunkSampleStream implements SampleStream, S * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource, - Callback> callback, Allocator allocator, long positionUs, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + public ChunkSampleStream( + int primaryTrackType, + int[] embeddedTrackTypes, + Format[] embeddedTrackFormats, + T chunkSource, + Callback> callback, + Allocator allocator, + long positionUs, + int minLoadableRetryCount, + EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; this.embeddedTrackTypes = embeddedTrackTypes; + this.embeddedTrackFormats = embeddedTrackFormats; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; @@ -555,6 +565,8 @@ public class ChunkSampleStream implements SampleStream, S private final SampleQueue sampleQueue; private final int index; + private boolean formatNotificationSent; + public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) { this.parent = parent; this.sampleQueue = sampleQueue; @@ -568,12 +580,19 @@ public class ChunkSampleStream implements SampleStream, S @Override public int skipData(long positionUs) { + int skipCount; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - return sampleQueue.advanceToEnd(); + skipCount = sampleQueue.advanceToEnd(); } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; + skipCount = sampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } } + if (skipCount > 0) { + maybeNotifyTrackFormatChanged(); + } + return skipCount; } @Override @@ -587,8 +606,13 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + int result = + sampleQueue.read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + maybeNotifyTrackFormatChanged(); + } + return result; } public void release() { @@ -596,6 +620,17 @@ public class ChunkSampleStream implements SampleStream, S embeddedTracksSelected[index] = false; } + private void maybeNotifyTrackFormatChanged() { + if (!formatNotificationSent) { + eventDispatcher.downstreamFormatChanged( + embeddedTrackTypes[index], + embeddedTrackFormats[index], + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + formatNotificationSent = true; + } + } } } 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 8a69f98653..569328c101 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 @@ -436,26 +436,33 @@ import java.util.Map; } AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); - int primaryTrackGroupIndex = trackGroupCount; - boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; - boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i]; + int primaryTrackGroupIndex = trackGroupCount++; + int eventMessageTrackGroupIndex = + primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET; + int cea608TrackGroupIndex = + primaryGroupHasCea608TrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET; - trackGroups[trackGroupCount] = new TrackGroup(formats); - trackGroupInfos[trackGroupCount++] = TrackGroupInfo.primaryTrack(firstAdaptationSet.type, - adaptationSetIndices, primaryTrackGroupIndex, hasEventMessageTrack, hasCea608Track); - if (hasEventMessageTrack) { + trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats); + trackGroupInfos[primaryTrackGroupIndex] = + TrackGroupInfo.primaryTrack( + firstAdaptationSet.type, + adaptationSetIndices, + primaryTrackGroupIndex, + eventMessageTrackGroupIndex, + cea608TrackGroupIndex); + if (eventMessageTrackGroupIndex != C.INDEX_UNSET) { Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); - trackGroups[trackGroupCount] = new TrackGroup(format); - trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, - primaryTrackGroupIndex); + trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format); + trackGroupInfos[eventMessageTrackGroupIndex] = + TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex); } - if (hasCea608Track) { + if (cea608TrackGroupIndex != C.INDEX_UNSET) { Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608", MimeTypes.APPLICATION_CEA608, 0, null); - trackGroups[trackGroupCount] = new TrackGroup(format); - trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedCea608Track( - adaptationSetIndices, primaryTrackGroupIndex); + trackGroups[cea608TrackGroupIndex] = new TrackGroup(format); + trackGroupInfos[cea608TrackGroupIndex] = + TrackGroupInfo.embeddedCea608Track(adaptationSetIndices, primaryTrackGroupIndex); } } return trackGroupCount; @@ -476,24 +483,39 @@ import java.util.Map; TrackSelection selection, long positionUs) { int embeddedTrackCount = 0; int[] embeddedTrackTypes = new int[2]; - boolean enableEventMessageTrack = trackGroupInfo.hasEmbeddedEventMessageTrack; + Format[] embeddedTrackFormats = new Format[2]; + boolean enableEventMessageTrack = + trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET; if (enableEventMessageTrack) { + embeddedTrackFormats[embeddedTrackCount] = + trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex).getFormat(0); embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; } - boolean enableCea608Track = trackGroupInfo.hasEmbeddedCea608Track; + boolean enableCea608Track = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET; if (enableCea608Track) { + embeddedTrackFormats[embeddedTrackCount] = + trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex).getFormat(0); embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; } if (embeddedTrackCount < embeddedTrackTypes.length) { + embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount); embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); - ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType, - embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, - eventDispatcher); + ChunkSampleStream stream = + new ChunkSampleStream<>( + trackGroupInfo.trackType, + embeddedTrackTypes, + embeddedTrackFormats, + chunkSource, + this, + allocator, + positionUs, + minLoadableRetryCount, + eventDispatcher); return stream; } @@ -578,43 +600,74 @@ import java.util.Map; public final int eventStreamGroupIndex; public final int primaryTrackGroupIndex; - public final boolean hasEmbeddedEventMessageTrack; - public final boolean hasEmbeddedCea608Track; + public final int embeddedEventMessageTrackGroupIndex; + public final int embeddedCea608TrackGroupIndex; - public static TrackGroupInfo primaryTrack(int trackType, int[] adaptationSetIndices, - int primaryTrackGroupIndex, boolean hasEmbeddedEventMessageTrack, - boolean hasEmbeddedCea608Track) { - return new TrackGroupInfo(trackType, CATEGORY_PRIMARY, adaptationSetIndices, - primaryTrackGroupIndex, hasEmbeddedEventMessageTrack, hasEmbeddedCea608Track, -1); + public static TrackGroupInfo primaryTrack( + int trackType, + int[] adaptationSetIndices, + int primaryTrackGroupIndex, + int embeddedEventMessageTrackGroupIndex, + int embeddedCea608TrackGroupIndex) { + return new TrackGroupInfo( + trackType, + CATEGORY_PRIMARY, + adaptationSetIndices, + primaryTrackGroupIndex, + embeddedEventMessageTrackGroupIndex, + embeddedCea608TrackGroupIndex, + -1); } public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices, int primaryTrackGroupIndex) { - return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_EMBEDDED, - adaptationSetIndices, primaryTrackGroupIndex, false, false, -1); + return new TrackGroupInfo( + C.TRACK_TYPE_METADATA, + CATEGORY_EMBEDDED, + adaptationSetIndices, + primaryTrackGroupIndex, + C.INDEX_UNSET, + C.INDEX_UNSET, + -1); } public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices, int primaryTrackGroupIndex) { - return new TrackGroupInfo(C.TRACK_TYPE_TEXT, CATEGORY_EMBEDDED, - adaptationSetIndices, primaryTrackGroupIndex, false, false, -1); + return new TrackGroupInfo( + C.TRACK_TYPE_TEXT, + CATEGORY_EMBEDDED, + adaptationSetIndices, + primaryTrackGroupIndex, + C.INDEX_UNSET, + C.INDEX_UNSET, + -1); } public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) { - return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_MANIFEST_EVENTS, - null, -1, false, false, eventStreamIndex); + return new TrackGroupInfo( + C.TRACK_TYPE_METADATA, + CATEGORY_MANIFEST_EVENTS, + null, + -1, + C.INDEX_UNSET, + C.INDEX_UNSET, + eventStreamIndex); } - private TrackGroupInfo(int trackType, @TrackGroupCategory int trackGroupCategory, - int[] adaptationSetIndices, int primaryTrackGroupIndex, - boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track, + private TrackGroupInfo( + int trackType, + @TrackGroupCategory int trackGroupCategory, + int[] adaptationSetIndices, + int primaryTrackGroupIndex, + int embeddedEventMessageTrackGroupIndex, + int embeddedCea608TrackGroupIndex, int eventStreamGroupIndex) { this.trackType = trackType; this.adaptationSetIndices = adaptationSetIndices; this.trackGroupCategory = trackGroupCategory; this.primaryTrackGroupIndex = primaryTrackGroupIndex; - this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack; - this.hasEmbeddedCea608Track = hasEmbeddedCea608Track; + this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex; + this.embeddedCea608TrackGroupIndex = embeddedCea608TrackGroupIndex; this.eventStreamGroupIndex = eventStreamGroupIndex; } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 99804ca809..a600741362 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -207,8 +207,16 @@ import java.util.ArrayList; int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup()); SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower, manifest, streamElementIndex, selection, trackEncryptionBoxes); - return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, null, - chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); + return new ChunkSampleStream<>( + manifest.streamElements[streamElementIndex].type, + null, + null, + chunkSource, + this, + allocator, + positionUs, + minLoadableRetryCount, + eventDispatcher); } private static TrackGroupArray buildTrackGroups(SsManifest manifest) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 7b9fe3db07..d32dda65f4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -132,8 +132,15 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod protected SampleStream createSampleStream(TrackSelection trackSelection) { FakeChunkSource chunkSource = chunkSourceFactory.createChunkSource(trackSelection, durationUs); return new ChunkSampleStream<>( - MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), null, - chunkSource, this, allocator, 0, 3, eventDispatcher); + MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), + null, + null, + chunkSource, + this, + allocator, + 0, + 3, + eventDispatcher); } @Override From 515fdf3bfd9aed9826bd84a3f2194ffc8481f000 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 15 Jan 2018 08:21:33 -0800 Subject: [PATCH 271/417] Allow extension of DebugTextViewHelper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181970955 --- .../exoplayer2/ui/DebugTextViewHelper.java | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index fda74db28d..6066445e9d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -27,7 +27,7 @@ import java.util.Locale; * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public final class DebugTextViewHelper extends Player.DefaultEventListener implements Runnable { +public class DebugTextViewHelper extends Player.DefaultEventListener implements Runnable { private static final int REFRESH_INTERVAL_MS = 1000; @@ -49,7 +49,7 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple * Starts periodic updates of the {@link TextView}. Must be called from the application's main * thread. */ - public void start() { + public final void start() { if (started) { return; } @@ -62,7 +62,7 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple * Stops periodic updates of the {@link TextView}. Must be called from the application's main * thread. */ - public void stop() { + public final void stop() { if (!started) { return; } @@ -74,59 +74,63 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { updateAndPost(); } @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { updateAndPost(); } // Runnable implementation. @Override - public void run() { + public final void run() { updateAndPost(); } - // Private methods. + // Protected methods. @SuppressLint("SetTextI18n") - private void updateAndPost() { - textView.setText(getPlayerStateString() + getPlayerWindowIndexString() + getVideoString() - + getAudioString()); + protected final void updateAndPost() { + textView.setText(getDebugString()); textView.removeCallbacks(this); textView.postDelayed(this, REFRESH_INTERVAL_MS); } - private String getPlayerStateString() { - String text = "playWhenReady:" + player.getPlayWhenReady() + " playbackState:"; + /** Returns the debugging information string to be shown by the target {@link TextView}. */ + protected String getDebugString() { + return getPlayerStateString() + getVideoString() + getAudioString(); + } + + /** Returns a string containing player state debugging information. */ + protected String getPlayerStateString() { + String playbackStateString; switch (player.getPlaybackState()) { case Player.STATE_BUFFERING: - text += "buffering"; + playbackStateString = "buffering"; break; case Player.STATE_ENDED: - text += "ended"; + playbackStateString = "ended"; break; case Player.STATE_IDLE: - text += "idle"; + playbackStateString = "idle"; break; case Player.STATE_READY: - text += "ready"; + playbackStateString = "ready"; break; default: - text += "unknown"; + playbackStateString = "unknown"; break; } - return text; + return String.format( + "playWhenReady:%s playbackState:%s window:%s", + player.getPlayWhenReady(), playbackStateString, player.getCurrentWindowIndex()); } - private String getPlayerWindowIndexString() { - return " window:" + player.getCurrentWindowIndex(); - } - - private String getVideoString() { + /** Returns a string containing video debugging information. */ + protected String getVideoString() { Format format = player.getVideoFormat(); if (format == null) { return ""; @@ -136,7 +140,8 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; } - private String getAudioString() { + /** Returns a string containing audio debugging information. */ + protected String getAudioString() { Format format = player.getAudioFormat(); if (format == null) { return ""; From 6bed2ffc047130faf3beb32aacc75aa251d355bc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 16 Jan 2018 00:23:47 -0800 Subject: [PATCH 272/417] Remove ndk-build from [] flac build rules Android NDK r9 in [] is deprecated (see [] Update the ExoPlayer flac extensions to use android_jni_library. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182017669 --- extensions/flac/src/main/jni/include/data_source.h | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/flac/src/main/jni/include/data_source.h b/extensions/flac/src/main/jni/include/data_source.h index 175431dd7a..88af3e1277 100644 --- a/extensions/flac/src/main/jni/include/data_source.h +++ b/extensions/flac/src/main/jni/include/data_source.h @@ -22,6 +22,7 @@ class DataSource { public: + virtual ~DataSource() {} // Returns the number of bytes read, or -1 on failure. It's not an error if // this returns zero; it just means the given offset is equal to, or // beyond, the end of the source. From 6749623cd127f15c014dcf21ade1349fe436ef60 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 16 Jan 2018 04:09:09 -0800 Subject: [PATCH 273/417] Handle DASH `emsg' events targeting player. For live streaming, there are several types of DASH `emsg' events that directly target the player. These events can signal whether the manifest is expired, or the live streaming has ended, and should be handle directly within the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182034591 --- RELEASENOTES.md | 8 +- .../source/chunk/ChunkSampleStream.java | 36 +- .../source/dash/DashChunkSource.java | 5 +- .../dash/DashManifestExpiredException.java | 21 + .../source/dash/DashMediaPeriod.java | 82 +++- .../source/dash/DashMediaSource.java | 115 ++++- .../source/dash/DefaultDashChunkSource.java | 110 ++++- .../source/dash/PlayerEmsgHandler.java | 454 ++++++++++++++++++ 8 files changed, 775 insertions(+), 56 deletions(-) create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6d4347490e..ba82f46525 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,7 +32,13 @@ seeking to the closest sync points before, either side or after specified seek positions. * Note: `SeekParameters` are not currently supported when playing HLS streams. -* DASH: Support DASH manifest EventStream elements. +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). +* DASH: + * Support in-band Emsg events targeting player with scheme id + "urn:mpeg:dash:event:2012" and scheme value of either "1", "2" or "3". + * Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can significantly reduce initial buffering time 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 e740e6607e..29a6ce29fb 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.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -41,6 +42,17 @@ import java.util.List; public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback, Loader.ReleaseCallback { + /** A callback to be notified when a sample stream has finished being released. */ + public interface ReleaseCallback { + + /** + * Called when the {@link ChunkSampleStream} has finished being released. + * + * @param chunkSampleStream The released sample stream. + */ + void onSampleStreamReleased(ChunkSampleStream chunkSampleStream); + } + private static final String TAG = "ChunkSampleStream"; public final int primaryTrackType; @@ -61,6 +73,7 @@ public class ChunkSampleStream implements SampleStream, S private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; + private ReleaseCallback releaseCallback; private long pendingResetPositionUs; /* package */ long lastSeekPositionUs; /* package */ boolean loadingFinished; @@ -247,10 +260,26 @@ public class ChunkSampleStream implements SampleStream, S /** * Releases the stream. - *

      - * This method should be called when the stream is no longer required. + * + *

      This method should be called when the stream is no longer required. Either this method or + * {@link #release(ReleaseCallback)} can be used to release this stream. */ public void release() { + release(null); + } + + /** + * Releases the stream. + * + *

      This method should be called when the stream is no longer required. Either this method or + * {@link #release()} can be used to release this stream. + * + * @param callback A callback to be called when the release ends. Will be called synchronously + * from this method if no load is in progress, or asynchronously once the load has been + * canceled otherwise. + */ + public void release(@Nullable ReleaseCallback callback) { + this.releaseCallback = callback; boolean releasedSynchronously = loader.release(this); if (!releasedSynchronously) { // Discard as much as we can synchronously. @@ -267,6 +296,9 @@ public class ChunkSampleStream implements SampleStream, S for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.reset(); } + if (releaseCallback != null) { + releaseCallback.onSampleStreamReleased(this); + } } // SampleStream implementation. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 167a8d486c..31c32e6100 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.source.dash; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.chunk.ChunkSource; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -53,7 +55,8 @@ public interface DashChunkSource extends ChunkSource { int type, long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, - boolean enableCea608Track); + boolean enableCea608Track, + @Nullable PlayerTrackEmsgHandler playerEmsgHandler); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java new file mode 100644 index 0000000000..2af847467c --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java @@ -0,0 +1,21 @@ +/* + * 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.source.dash; + +import java.io.IOException; + +/** Thrown when a live playback's manifest is expired and a new manifest could not be loaded. */ +public final class DashManifestExpiredException extends IOException {} 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 569328c101..4dab4e2279 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 @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Descriptor; @@ -47,14 +49,15 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -/** - * A DASH {@link MediaPeriod}. - */ -/* package */ final class DashMediaPeriod implements MediaPeriod, - SequenceableLoader.Callback> { +/** A DASH {@link MediaPeriod}. */ +/* package */ final class DashMediaPeriod + implements MediaPeriod, + SequenceableLoader.Callback>, + ChunkSampleStream.ReleaseCallback { /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; @@ -66,6 +69,9 @@ import java.util.Map; private final TrackGroupArray trackGroups; private final TrackGroupInfo[] trackGroupInfos; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final PlayerEmsgHandler playerEmsgHandler; + private final IdentityHashMap, PlayerTrackEmsgHandler> + trackEmsgHandlerBySampleStream; private Callback callback; private ChunkSampleStream[] sampleStreams; @@ -75,11 +81,18 @@ import java.util.Map; private int periodIndex; private List eventStreams; - public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - EventDispatcher eventDispatcher, long elapsedRealtimeOffset, - LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator, - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + public DashMediaPeriod( + int id, + DashManifest manifest, + int periodIndex, + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + long elapsedRealtimeOffset, + LoaderErrorThrower manifestLoaderErrorThrower, + Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + PlayerEmsgCallback playerEmsgCallback) { this.id = id; this.manifest = manifest; this.periodIndex = periodIndex; @@ -90,8 +103,10 @@ import java.util.Map; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator); sampleStreams = newSampleStreamArray(0); eventSampleStreams = new EventSampleStream[0]; + trackEmsgHandlerBySampleStream = new IdentityHashMap<>(); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); @@ -111,14 +126,14 @@ import java.util.Map; public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; this.periodIndex = periodIndex; - Period period = manifest.getPeriod(periodIndex); + playerEmsgHandler.updateManifest(manifest); if (sampleStreams != null) { for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.getChunkSource().updateManifest(manifest, periodIndex); } callback.onContinueLoadingRequested(this); } - eventStreams = period.eventStreams; + eventStreams = manifest.getPeriod(periodIndex).eventStreams; for (EventSampleStream eventSampleStream : eventSampleStreams) { for (EventStream eventStream : eventStreams) { if (eventStream.id().equals(eventSampleStream.eventStreamId())) { @@ -130,11 +145,24 @@ import java.util.Map; } public void release() { + playerEmsgHandler.release(); for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.release(); + sampleStream.release(this); } } + // ChunkSampleStream.ReleaseCallback implementation. + + @Override + public void onSampleStreamReleased(ChunkSampleStream stream) { + PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream); + if (trackEmsgHandler != null) { + trackEmsgHandler.release(); + } + } + + // MediaPeriod implementation. + @Override public void prepare(Callback callback, long positionUs) { this.callback = callback; @@ -181,7 +209,7 @@ import java.util.Map; @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { - stream.release(); + stream.release(this); streams[i] = null; } else { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); @@ -501,10 +529,22 @@ import java.util.Map; embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount); embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); } - DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, - selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack, - enableCea608Track); + PlayerTrackEmsgHandler trackPlayerEmsgHandler = + manifest.dynamic && enableEventMessageTrack + ? playerEmsgHandler.newPlayerTrackEmsgHandler() + : null; + DashChunkSource chunkSource = + chunkSourceFactory.createDashChunkSource( + manifestLoaderErrorThrower, + manifest, + periodIndex, + trackGroupInfo.adaptationSetIndices, + selection, + trackGroupInfo.trackType, + elapsedRealtimeOffset, + enableEventMessageTrack, + enableCea608Track, + trackPlayerEmsgHandler); ChunkSampleStream stream = new ChunkSampleStream<>( trackGroupInfo.trackType, @@ -516,6 +556,7 @@ import java.util.Map; positionUs, minLoadableRetryCount, eventDispatcher); + trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler); return stream; } @@ -581,9 +622,8 @@ import java.util.Map; private static final int CATEGORY_PRIMARY = 0; /** - * A track group whose samples are embedded within one of the primary streams. - * For example: an EMSG track has its sample embedded in `emsg' atoms in one of the primary - * streams. + * A track group whose samples are embedded within one of the primary streams. For example: an + * EMSG track has its sample embedded in emsg atoms in one of the primary streams. */ private static final int CATEGORY_EMBEDDED = 1; 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 77914d6d45..08e25f216a 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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; @@ -56,9 +57,7 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * A DASH {@link MediaSource}. - */ +/** A DASH {@link MediaSource}. */ public final class DashMediaSource implements MediaSource { static { @@ -280,6 +279,7 @@ public final class DashMediaSource implements MediaSource { private final SparseArray periodsById; private final Runnable refreshManifestRunnable; private final Runnable simulateManifestRefreshRunnable; + private final PlayerEmsgCallback playerEmsgCallback; private Listener sourceListener; private DataSource dataSource; @@ -291,7 +291,11 @@ public final class DashMediaSource implements MediaSource { private long manifestLoadEndTimestamp; private DashManifest manifest; private Handler handler; + private boolean pendingManifestLoading; private long elapsedRealtimeOffsetMs; + private long expiredManifestPublishTimeUs; + private boolean dynamicMediaPresentationEnded; + private int staleManifestReloadAttempt; private int firstPeriodId; @@ -446,6 +450,8 @@ public final class DashMediaSource implements MediaSource { eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestUriLock = new Object(); periodsById = new SparseArray<>(); + playerEmsgCallback = new DefaultPlayerEmsgCallback(); + expiredManifestPublishTimeUs = C.TIME_UNSET; if (sideloadedManifest) { Assertions.checkState(!manifest.dynamic); manifestCallback = null; @@ -507,9 +513,19 @@ public final class DashMediaSource implements MediaSource { int periodIndex = periodId.periodIndex; EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( manifest.getPeriod(periodIndex).startMs); - DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, - periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher, - elapsedRealtimeOffsetMs, loaderErrorThrower, allocator, compositeSequenceableLoaderFactory); + DashMediaPeriod mediaPeriod = + new DashMediaPeriod( + firstPeriodId + periodIndex, + manifest, + periodIndex, + chunkSourceFactory, + minLoadableRetryCount, + periodEventDispatcher, + elapsedRealtimeOffsetMs, + loaderErrorThrower, + allocator, + compositeSequenceableLoaderFactory, + playerEmsgCallback); periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } @@ -523,6 +539,7 @@ public final class DashMediaSource implements MediaSource { @Override public void releaseSource() { + pendingManifestLoading = false; dataSource = null; loaderErrorThrower = null; if (loader != null) { @@ -540,6 +557,24 @@ public final class DashMediaSource implements MediaSource { periodsById.clear(); } + // PlayerEmsgCallback callbacks. + + /* package */ void onDashManifestRefreshRequested() { + handler.removeCallbacks(simulateManifestRefreshRunnable); + startLoadingManifest(); + } + + /* package */ void onDashLiveMediaPresentationEndSignalEncountered() { + this.dynamicMediaPresentationEnded = true; + } + + /* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { + if (this.expiredManifestPublishTimeUs == C.TIME_UNSET + || this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) { + this.expiredManifestPublishTimeUs = expiredManifestPublishTimeUs; + } + } + // Loadable callbacks. /* package */ void onManifestLoadCompleted(ParsingLoadable loadable, @@ -566,9 +601,16 @@ public final class DashMediaSource implements MediaSource { return; } + if (maybeReloadStaleDynamicManifest(newManifest)) { + return; + } manifest = newManifest; manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; manifestLoadEndTimestamp = elapsedRealtimeMs; + staleManifestReloadAttempt = 0; + if (!manifest.dynamic) { + pendingManifestLoading = false; + } if (manifest.location != null) { synchronized (manifestUriLock) { // This condition checks that replaceManifestUri wasn't called between the start and end of @@ -622,11 +664,41 @@ public final class DashMediaSource implements MediaSource { // Internal methods. + /** + * Reloads a stale dynamic manifest to get a more recent version if possible. + * + * @return True if the reload is scheduled. False if we have already retried too many times. + */ + private boolean maybeReloadStaleDynamicManifest(DashManifest manifest) { + if (!isManifestStale(manifest)) { + return false; + } + String warning = + "Loaded a stale dynamic manifest " + + manifest.publishTimeMs + + " " + + dynamicMediaPresentationEnded + + " " + + expiredManifestPublishTimeUs; + Log.w(TAG, warning); + if (staleManifestReloadAttempt++ < minLoadableRetryCount) { + startLoadingManifest(); + return true; + } + return false; + } + private void startLoadingManifest() { + handler.removeCallbacks(refreshManifestRunnable); + if (loader.isLoading()) { + pendingManifestLoading = true; + return; + } Uri manifestUri; synchronized (manifestUriLock) { manifestUri = this.manifestUri; } + pendingManifestLoading = false; startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), manifestCallback, minLoadableRetryCount); } @@ -753,13 +825,21 @@ public final class DashMediaSource implements MediaSource { if (windowChangingImplicitly) { handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); } - // Schedule an explicit refresh if needed. - if (scheduleRefresh) { + if (pendingManifestLoading) { + startLoadingManifest(); + } else if (scheduleRefresh) { + // Schedule an explicit refresh if needed. scheduleManifestRefresh(); } } } + private boolean isManifestStale(DashManifest manifest) { + return manifest.dynamic + && (dynamicMediaPresentationEnded + || manifest.publishTimeMs <= expiredManifestPublishTimeUs); + } + private void scheduleManifestRefresh() { if (!manifest.dynamic) { return; @@ -948,6 +1028,24 @@ public final class DashMediaSource implements MediaSource { } + private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { + + @Override + public void onDashManifestRefreshRequested() { + DashMediaSource.this.onDashManifestRefreshRequested(); + } + + @Override + public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { + DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); + } + + @Override + public void onDashLiveMediaPresentationEndSignalEncountered() { + DashMediaSource.this.onDashLiveMediaPresentationEndSignalEncountered(); + } + } + private final class ManifestCallback implements Loader.Callback> { @Override @@ -1039,5 +1137,4 @@ public final class DashMediaSource implements MediaSource { } } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 1162762f7c..4635a08a3c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -17,12 +17,14 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor; @@ -35,6 +37,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; @@ -71,14 +74,31 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int[] adaptationSetIndices, - TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs, - boolean enableEventMessageTrack, boolean enableCea608Track) { + public DashChunkSource createDashChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, + int periodIndex, + int[] adaptationSetIndices, + TrackSelection trackSelection, + int trackType, + long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, + boolean enableCea608Track, + @Nullable PlayerTrackEmsgHandler playerEmsgHandler) { DataSource dataSource = dataSourceFactory.createDataSource(); - return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs, - maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); + return new DefaultDashChunkSource( + manifestLoaderErrorThrower, + manifest, + periodIndex, + adaptationSetIndices, + trackSelection, + trackType, + dataSource, + elapsedRealtimeOffsetMs, + maxSegmentsPerLoad, + enableEventMessageTrack, + enableCea608Track, + playerEmsgHandler); } } @@ -90,6 +110,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; private final int maxSegmentsPerLoad; + @Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler; protected final RepresentationHolder[] representationHolders; @@ -110,18 +131,28 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified * as the server's unix time minus the local elapsed time. If unknown, set to 0. - * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. - * Note that segments will only be combined if their {@link Uri}s are the same and if their - * data ranges are adjacent. + * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note + * that segments will only be combined if their {@link Uri}s are the same and if their data + * ranges are adjacent. * @param enableEventMessageTrack Whether the chunks generated by the source may output an event * message track. * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. + * @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg + * messages targeting the player. Maybe null if this is not necessary. */ - public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int[] adaptationSetIndices, - TrackSelection trackSelection, int trackType, DataSource dataSource, - long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, - boolean enableCea608Track) { + public DefaultDashChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, + int periodIndex, + int[] adaptationSetIndices, + TrackSelection trackSelection, + int trackType, + DataSource dataSource, + long elapsedRealtimeOffsetMs, + int maxSegmentsPerLoad, + boolean enableEventMessageTrack, + boolean enableCea608Track, + @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndices = adaptationSetIndices; @@ -131,15 +162,23 @@ public class DefaultDashChunkSource implements DashChunkSource { this.periodIndex = periodIndex; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; this.maxSegmentsPerLoad = maxSegmentsPerLoad; + this.playerTrackEmsgHandler = playerTrackEmsgHandler; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); liveEdgeTimeUs = C.TIME_UNSET; + List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, trackType, - representation, enableEventMessageTrack, enableCea608Track); + representationHolders[i] = + new RepresentationHolder( + periodDurationUs, + trackType, + representation, + enableEventMessageTrack, + enableCea608Track, + playerTrackEmsgHandler); } } @@ -203,6 +242,20 @@ public class DefaultDashChunkSource implements DashChunkSource { long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + long presentationPositionUs = + C.msToUs(manifest.availabilityStartTimeMs) + + C.msToUs(manifest.getPeriod(periodIndex).startMs) + + loadPositionUs; + try { + if (playerTrackEmsgHandler != null + && playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk( + presentationPositionUs)) { + return; + } + } catch (DashManifestExpiredException e) { + fatalError = e; + return; + } trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = @@ -298,6 +351,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } } } + if (playerTrackEmsgHandler != null) { + playerTrackEmsgHandler.onChunkLoadCompleted(chunk); + } } @Override @@ -305,6 +361,10 @@ public class DefaultDashChunkSource implements DashChunkSource { if (!cancelable) { return false; } + if (playerTrackEmsgHandler != null + && playerTrackEmsgHandler.maybeRefreshManifestOnLoadingError(chunk)) { + return true; + } // Workaround for missing segment at the end of the period if (!manifest.dynamic && chunk instanceof MediaChunk && e instanceof InvalidResponseCodeException @@ -426,8 +486,13 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - /* package */ RepresentationHolder(long periodDurationUs, int trackType, - Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { + /* package */ RepresentationHolder( + long periodDurationUs, + int trackType, + Representation representation, + boolean enableEventMessageTrack, + boolean enableCea608Track, + TrackOutput playerEmsgTrackOutput) { this.periodDurationUs = periodDurationUs; this.representation = representation; String containerMimeType = representation.format.containerMimeType; @@ -449,7 +514,10 @@ public class DefaultDashChunkSource implements DashChunkSource { ? Collections.singletonList( Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) : Collections.emptyList(); - extractor = new FragmentedMp4Extractor(flags, null, null, null, closedCaptionFormats); + + extractor = + new FragmentedMp4Extractor( + flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. @@ -534,7 +602,5 @@ public class DefaultDashChunkSource implements DashChunkSource { private static boolean mimeTypeIsRawText(String mimeType) { return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); } - } - } 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 new file mode 100644 index 0000000000..bdcfef24c1 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2017 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.dash; + +import static com.google.android.exoplayer2.util.Util.parseXsDateTime; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.Map; +import java.util.TreeMap; + +/** + * Handles all emsg messages from all media tracks for the player. + * + *

      This class will only respond to emsg messages which have schemeIdUri + * "urn:mpeg:dash:event:2012", and value "1"/"2"/"3". When it encounters one of these messages, it + * will handle the message according to Section 4.5.2.1 DASH -IF IOP Version 4.1: + * + *

        + *
      • If both presentation time delta and event duration are zero, it means the media + * presentation has ended. + *
      • Else, it will parse the message data from the emsg message to find the publishTime of the + * expired manifest, and mark manifest with publishTime smaller than that values to be + * expired. + *
      + * + * In both cases, the DASH media source will be notified, and a manifest reload should be triggered. + */ +public final class PlayerEmsgHandler implements Handler.Callback { + + private static final int EMSG_MEDIA_PRESENTATION_ENDED = 1; + private static final int EMSG_MANIFEST_EXPIRED = 2; + + /** Callbacks for player emsg events encountered during DASH live stream. */ + public interface PlayerEmsgCallback { + + /** Called when the current manifest should be refreshed. */ + void onDashManifestRefreshRequested(); + + /** + * Called when the manifest with the publish time has been expired. + * + * @param expiredManifestPublishTimeUs The manifest publish time that has been expired. + */ + void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs); + + /** Called when a media presentation end signal is encountered during live stream. * */ + void onDashLiveMediaPresentationEndSignalEncountered(); + } + + private final Allocator allocator; + private final PlayerEmsgCallback playerEmsgCallback; + private final EventMessageDecoder decoder; + private final Handler handler; + private final TreeMap manifestPublishTimeToExpiryTimeUs; + + private DashManifest manifest; + + private boolean dynamicMediaPresentationEnded; + private long expiredManifestPublishTimeUs; + private long lastLoadedChunkEndTimeUs; + private long lastLoadedChunkEndTimeBeforeRefreshUs; + private boolean isWaitingForManifestRefresh; + private boolean released; + private DashManifestExpiredException fatalError; + + /** + * @param manifest The initial manifest. + * @param playerEmsgCallback The callback that this event handler can invoke when handling emsg + * messages that generate DASH media source events. + * @param allocator An {@link Allocator} from which allocations can be obtained. + */ + public PlayerEmsgHandler( + DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) { + this.manifest = manifest; + this.playerEmsgCallback = playerEmsgCallback; + this.allocator = allocator; + + manifestPublishTimeToExpiryTimeUs = new TreeMap<>(); + handler = new Handler(this); + decoder = new EventMessageDecoder(); + lastLoadedChunkEndTimeUs = C.TIME_UNSET; + lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET; + } + + /** + * Updates the {@link DashManifest} that this handler works on. + * + * @param newManifest The updated manifest. + */ + public void updateManifest(DashManifest newManifest) { + if (isManifestStale(newManifest)) { + fatalError = new DashManifestExpiredException(); + } + + isWaitingForManifestRefresh = false; + expiredManifestPublishTimeUs = C.TIME_UNSET; + this.manifest = newManifest; + } + + private boolean isManifestStale(DashManifest manifest) { + return manifest.dynamic + && (dynamicMediaPresentationEnded + || manifest.publishTimeMs <= expiredManifestPublishTimeUs); + } + + /* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) + throws DashManifestExpiredException { + if (fatalError != null) { + throw fatalError; + } + if (!manifest.dynamic) { + return false; + } + if (isWaitingForManifestRefresh) { + return true; + } + boolean manifestRefreshNeeded = false; + if (dynamicMediaPresentationEnded) { + // The manifest we have is dynamic, but we know a non-dynamic one representing the final state + // should be available. + manifestRefreshNeeded = true; + } else { + // Find the smallest publishTime (greater than or equal to the current manifest's publish + // time) that has a corresponding expiry time. + Map.Entry expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs); + if (expiredEntry != null) { + long expiredPointUs = expiredEntry.getValue(); + if (expiredPointUs < presentationPositionUs) { + expiredManifestPublishTimeUs = expiredEntry.getKey(); + notifyManifestPublishTimeExpired(); + manifestRefreshNeeded = true; + } + } + } + if (manifestRefreshNeeded) { + maybeNotifyDashManifestRefreshNeeded(); + } + return manifestRefreshNeeded; + } + + /** + * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that + * signals end-of-stream or Manifest expiry, which results in load error. In this case, we should + * notify the Dash media source to refresh its manifest. + * + * @param chunk The chunk whose load encountered the error. + * @return True if manifest refresh has been requested, false otherwise. + */ + /* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { + if (!manifest.dynamic) { + return false; + } + if (isWaitingForManifestRefresh) { + return true; + } + boolean isAfterForwardSeek = + lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs; + if (isAfterForwardSeek) { + // if we are after a forward seek, and the playback is dynamic with embedded emsg stream, + // there's a chance that we have seek over the emsg messages, in which case we should ask + // media source for a refresh. + maybeNotifyDashManifestRefreshNeeded(); + return true; + } + return false; + } + + /** + * Called when the a new chunk in the current media stream has been loaded. + * + * @param chunk The chunk whose load has been completed. + */ + /* package */ void onChunkLoadCompleted(Chunk chunk) { + if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) { + lastLoadedChunkEndTimeUs = chunk.endTimeUs; + } + } + + /** + * Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the + * player. + */ + public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) { + return "urn:mpeg:dash:event:2012".equals(schemeIdUri) + && ("1".equals(value) || "2".equals(value) || "3".equals(value)); + } + + /** Returns a {@link TrackOutput} that emsg messages could be written to. */ + public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { + return new PlayerTrackEmsgHandler(new SampleQueue(allocator)); + } + + /** Release this emsg handler. It should not be reused after this call. */ + public void release() { + released = true; + handler.removeCallbacksAndMessages(null); + } + + @Override + public boolean handleMessage(Message message) { + if (released) { + return true; + } + switch (message.what) { + case (EMSG_MEDIA_PRESENTATION_ENDED): + handleMediaPresentationEndedMessageEncountered(); + return true; + case (EMSG_MANIFEST_EXPIRED): + ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj; + handleManifestExpiredMessage( + messageObj.eventTimeUs, messageObj.manifestPublishTimeMsInEmsg); + return true; + default: + // Do nothing. + } + return false; + } + + // Internal methods. + + private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) { + if (!manifestPublishTimeToExpiryTimeUs.containsKey(manifestPublishTimeMsInEmsg)) { + manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); + } else { + long previousExpiryTimeUs = + manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg); + if (previousExpiryTimeUs > eventTimeUs) { + manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); + } + } + } + + private void handleMediaPresentationEndedMessageEncountered() { + dynamicMediaPresentationEnded = true; + notifySourceMediaPresentationEnded(); + } + + private Map.Entry ceilingExpiryEntryForPublishTime(long publishTimeMs) { + if (manifestPublishTimeToExpiryTimeUs.isEmpty()) { + return null; + } + return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); + } + + private void notifyManifestPublishTimeExpired() { + playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); + } + + private void notifySourceMediaPresentationEnded() { + playerEmsgCallback.onDashLiveMediaPresentationEndSignalEncountered(); + } + + /** Requests DASH media manifest to be refreshed if necessary. */ + private void maybeNotifyDashManifestRefreshNeeded() { + if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET + && lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) { + // Already requested manifest refresh. + return; + } + isWaitingForManifestRefresh = true; + lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs; + playerEmsgCallback.onDashManifestRefreshRequested(); + } + + private static long getManifestPublishTimeMsInEmsg(EventMessage eventMessage) { + try { + return parseXsDateTime(new String(eventMessage.messageData)); + } catch (ParserException ignored) { + // if we can't parse this event, ignore + return C.TIME_UNSET; + } + } + + private static boolean isMessageSignalingMediaPresentationEnded(EventMessage eventMessage) { + // According to section 4.5.2.1 DASH-IF IOP, if both presentation time delta and event duration + // are zero, the media presentation is ended. + return eventMessage.presentationTimeUs == 0 && eventMessage.durationMs == 0; + } + + /** Handles emsg messages for a specific track for the player. */ + public final class PlayerTrackEmsgHandler implements TrackOutput { + + private final SampleQueue sampleQueue; + private final FormatHolder formatHolder; + private final MetadataInputBuffer buffer; + + /* package */ PlayerTrackEmsgHandler(SampleQueue sampleQueue) { + this.sampleQueue = sampleQueue; + + formatHolder = new FormatHolder(); + buffer = new MetadataInputBuffer(); + } + + @Override + public void format(Format format) { + sampleQueue.format(format); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return sampleQueue.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + sampleQueue.sampleData(data, length); + } + + @Override + public void sampleMetadata( + long timeUs, int flags, int size, int offset, CryptoData encryptionData) { + sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData); + parseAndDiscardSamples(); + } + + /** + * For live streaming, check if the DASH manifest is expired before the next segment start time. + * If it is, the DASH media source will be notified to refresh the manifest. + * + * @param presentationPositionUs The next load position in presentation time. + * @return True if manifest refresh has been requested, false otherwise. + * @throws DashManifestExpiredException If the current DASH manifest is expired, but a new + * manifest could not be loaded. + */ + public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) + throws DashManifestExpiredException { + return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk( + presentationPositionUs); + } + + /** + * Called when the a new chunk in the current media stream has been loaded. + * + * @param chunk The chunk whose load has been completed. + */ + public void onChunkLoadCompleted(Chunk chunk) { + PlayerEmsgHandler.this.onChunkLoadCompleted(chunk); + } + + /** + * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages + * that signals end-of-stream or Manifest expiry, which results in load error. In this case, we + * should notify the Dash media source to refresh its manifest. + * + * @param chunk The chunk whose load encountered the error. + * @return True if manifest refresh has been requested, false otherwise. + */ + public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { + return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk); + } + + /** Release this track emsg handler. It should not be reused after this call. */ + public void release() { + sampleQueue.reset(); + } + + // Internal methods. + + private void parseAndDiscardSamples() { + while (sampleQueue.hasNextSample()) { + MetadataInputBuffer inputBuffer = dequeueSample(); + if (inputBuffer == null) { + continue; + } + long eventTimeUs = inputBuffer.timeUs; + Metadata metadata = decoder.decode(inputBuffer); + EventMessage eventMessage = (EventMessage) metadata.get(0); + if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { + parsePlayerEmsgEvent(eventTimeUs, eventMessage); + } + } + sampleQueue.discardToRead(); + } + + @Nullable + private MetadataInputBuffer dequeueSample() { + buffer.clear(); + int result = sampleQueue.read(formatHolder, buffer, false, false, 0); + if (result == C.RESULT_BUFFER_READ) { + buffer.flip(); + return buffer; + } + return null; + } + + private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) { + long manifestPublishTimeMsInEmsg = getManifestPublishTimeMsInEmsg(eventMessage); + if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) { + return; + } + + if (isMessageSignalingMediaPresentationEnded(eventMessage)) { + onMediaPresentationEndedMessageEncountered(); + } else { + onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg); + } + } + + private void onMediaPresentationEndedMessageEncountered() { + handler.sendMessage(handler.obtainMessage(EMSG_MEDIA_PRESENTATION_ENDED)); + } + + private void onManifestExpiredMessageEncountered( + long eventTimeUs, long manifestPublishTimeMsInEmsg) { + ManifestExpiryEventInfo manifestExpiryEventInfo = + new ManifestExpiryEventInfo(eventTimeUs, manifestPublishTimeMsInEmsg); + handler.sendMessage(handler.obtainMessage(EMSG_MANIFEST_EXPIRED, manifestExpiryEventInfo)); + } + } + + /** Holds information related to a manifest expiry event. */ + private static final class ManifestExpiryEventInfo { + + public final long eventTimeUs; + public final long manifestPublishTimeMsInEmsg; + + public ManifestExpiryEventInfo(long eventTimeUs, long manifestPublishTimeMsInEmsg) { + this.eventTimeUs = eventTimeUs; + this.manifestPublishTimeMsInEmsg = manifestPublishTimeMsInEmsg; + } + } +} From 965bc4f6771c1fc9d848188b9e11807bf90f23ba Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 16 Jan 2018 07:23:59 -0800 Subject: [PATCH 274/417] Add JobDispatcherScheduler This is a Scheduler implementation which uses Firebase JobDispatcher. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182051350 --- extensions/leanback/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index 715e2e56d7..d8952ca2b8 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -30,7 +30,7 @@ dependencies { } ext { - javadocTitle = 'Leanback extension for Exoplayer library' + javadocTitle = 'Leanback extension' } apply from: '../../javadoc_library.gradle' From a4114f59b6897a28dd10cff2f11311dc04fc9363 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 16 Jan 2018 07:59:51 -0800 Subject: [PATCH 275/417] Seek at chunk level when seeking to chunk start positions This avoids issues that can arise due to slight discrepancies between chunk start times (obtained from the manifest of segment index) and the timestamps of the samples contained within those chunks. Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182054959 --- .../source/SampleMetadataQueue.java | 16 +++++ .../exoplayer2/source/SampleQueue.java | 12 ++++ .../source/chunk/ChunkSampleStream.java | 63 +++++++++++++++---- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 54db9d7880..e5b950cf2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -296,6 +296,22 @@ import com.google.android.exoplayer2.util.Util; return skipCount; } + /** + * Attempts to set the read position to the specified sample index. + * + * @param sampleIndex The sample index. + * @return Whether the read position was set successfully. False is returned if the specified + * index is smaller than the index of the first sample in the queue, or larger than the index + * of the next sample that will be written. + */ + public synchronized boolean setReadPosition(int sampleIndex) { + if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) { + readPosition = sampleIndex - absoluteFirstIndex; + return true; + } + return false; + } + /** * Discards up to but not including the sample immediately before or at the specified time. * 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 a4feb924b8..d9090baf3b 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 @@ -293,6 +293,18 @@ public final class SampleQueue implements TrackOutput { return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); } + /** + * Attempts to set the read position to the specified sample index. + * + * @param sampleIndex The sample index. + * @return Whether the read position was set successfully. False is returned if the specified + * index is smaller than the index of the first sample in the queue, or larger than the index + * of the next sample that will be written. + */ + public boolean setReadPosition(int sampleIndex) { + return metadataQueue.setReadPosition(sampleIndex); + } + /** * Attempts to read from the queue. * 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 29a6ce29fb..e0c5d35996 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 @@ -75,7 +75,8 @@ public class ChunkSampleStream implements SampleStream, S private Format primaryDownstreamTrackFormat; private ReleaseCallback releaseCallback; private long pendingResetPositionUs; - /* package */ long lastSeekPositionUs; + private long lastSeekPositionUs; + /* package */ long decodeOnlyUntilPositionUs; /* package */ boolean loadingFinished; /** @@ -219,9 +220,6 @@ public class ChunkSampleStream implements SampleStream, S * @return The adjusted seek position, in microseconds. */ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - // TODO: Using this method to adjust a seek position and then passing the adjusted position to - // seekToUs does not handle small discrepancies between the chunk boundary timestamps obtained - // from the chunk source and the timestamps of the samples in the chunks. return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); } @@ -233,9 +231,43 @@ public class ChunkSampleStream implements SampleStream, S public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; primarySampleQueue.rewind(); - // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, - positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); + + // See if we can seek within the primary sample queue. + boolean seekInsideBuffer; + if (isPendingReset()) { + seekInsideBuffer = false; + } else { + // Detect whether the seek is to the start of a chunk that's at least partially buffered. + BaseMediaChunk seekToMediaChunk = null; + for (int i = 0; i < mediaChunks.size(); i++) { + BaseMediaChunk mediaChunk = mediaChunks.get(i); + long mediaChunkStartTimeUs = mediaChunk.startTimeUs; + if (mediaChunkStartTimeUs == positionUs) { + seekToMediaChunk = mediaChunk; + break; + } else if (mediaChunkStartTimeUs > positionUs) { + // We're not going to find a chunk with a matching start time. + break; + } + } + if (seekToMediaChunk != null) { + // When seeking to the start of a chunk we use the index of the first sample in the chunk + // rather than the seek position. This ensures we seek to the keyframe at the start of the + // chunk even if the sample timestamps are slightly offset from the chunk start times. + seekInsideBuffer = + primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0)); + decodeOnlyUntilPositionUs = Long.MIN_VALUE; + } else { + seekInsideBuffer = + primarySampleQueue.advanceTo( + positionUs, + /* toKeyframe= */ true, + /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()) + != SampleQueue.ADVANCE_FAILED; + decodeOnlyUntilPositionUs = lastSeekPositionUs; + } + } + if (seekInsideBuffer) { // We succeeded. Advance the embedded sample queues to the seek position. for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { @@ -322,8 +354,9 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + int result = + primarySampleQueue.read( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); if (result == C.RESULT_BUFFER_READ) { maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), 1); } @@ -421,9 +454,10 @@ public class ChunkSampleStream implements SampleStream, S return false; } + boolean pendingReset = isPendingReset(); MediaChunk previousChunk; long loadPositionUs; - if (isPendingReset()) { + if (pendingReset) { previousChunk = null; loadPositionUs = pendingResetPositionUs; } else { @@ -446,8 +480,13 @@ public class ChunkSampleStream implements SampleStream, S } if (isMediaChunk(loadable)) { - pendingResetPositionUs = C.TIME_UNSET; BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; + if (pendingReset) { + boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; + // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. + decodeOnlyUntilPositionUs = resetToMediaChunk ? Long.MIN_VALUE : pendingResetPositionUs; + pendingResetPositionUs = C.TIME_UNSET; + } mediaChunk.init(mediaChunkOutput); mediaChunks.add(mediaChunk); } @@ -640,7 +679,7 @@ public class ChunkSampleStream implements SampleStream, S } int result = sampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); if (result == C.RESULT_BUFFER_READ) { maybeNotifyTrackFormatChanged(); } From be304486e043616a36ee13eba75dab6431ed6930 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 16 Jan 2018 09:11:07 -0800 Subject: [PATCH 276/417] Fix HLS' mime type propagation Issue:#3653 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182064250 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/Format.java | 11 +++++++++-- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ba82f46525..f086bbf515 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* HLS: Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). * SimpleExoPlayerView: Automatically apply video rotation if `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 7799f411a9..6ef57537f1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -474,8 +474,15 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, - @C.SelectionFlags int selectionFlags, String language) { + public Format copyWithContainerInfo( + String id, + String sampleMimeType, + String codecs, + int bitrate, + int width, + int height, + @C.SelectionFlags int selectionFlags, + String language) { return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, 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 eba4596b7f..0dbadcd8e0 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 @@ -882,8 +882,10 @@ import java.util.Arrays; int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); + String mimeType = MimeTypes.getMediaMimeType(codecs); return sampleFormat.copyWithContainerInfo( playlistFormat.id, + mimeType, codecs, bitrate, playlistFormat.width, From 3919843db27c78dc04ec498087182058393cced0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 16 Jan 2018 09:56:12 -0800 Subject: [PATCH 277/417] Force single audio and video sample queues This solves the problem of having dense tracks' ids change. For example, if the available variants offer both HEVC and AVC video tracks, all video samples will map to the same sample queue even if IDs don't match. Issue:#3653 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182070486 --- .../exoplayer2/source/hls/HlsMediaChunk.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 69 ++++++++++++++++--- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index c4e54d4bd3..50c1200fae 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -156,7 +156,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { this.output = output; - output.init(uid, shouldSpliceIn); + output.init(uid, shouldSpliceIn, reusingExtractor); if (!reusingExtractor) { extractor.init(output); } 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 0dbadcd8e0..e4c71e43c5 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 @@ -93,6 +93,10 @@ import java.util.Arrays; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; + private boolean audioSampleQueueMappingDone; + private int audioSampleQueueIndex; + private boolean videoSampleQueueMappingDone; + private int videoSampleQueueIndex; private boolean sampleQueuesBuilt; private boolean prepared; private int enabledTrackGroupCount; @@ -143,6 +147,8 @@ import java.util.Arrays; loader = new Loader("Loader:HlsSampleStreamWrapper"); nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); sampleQueueTrackIds = new int[0]; + audioSampleQueueIndex = C.INDEX_UNSET; + videoSampleQueueIndex = C.INDEX_UNSET; sampleQueues = new SampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; @@ -616,8 +622,14 @@ import java.util.Arrays; * @param chunkUid The chunk's uid. * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any * samples already queued to the wrapper. + * @param reusingExtractor Whether the extractor for the chunk has already been used for preceding + * chunks. */ - public void init(int chunkUid, boolean shouldSpliceIn) { + public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) { + if (!reusingExtractor) { + audioSampleQueueMappingDone = false; + videoSampleQueueMappingDone = false; + } for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); } @@ -633,14 +645,43 @@ import java.util.Arrays; @Override public TrackOutput track(int id, int type) { int trackCount = sampleQueues.length; - for (int i = 0; i < trackCount; i++) { - if (sampleQueueTrackIds[i] == id) { - return sampleQueues[i]; + + // Audio and video tracks are handled manually to ignore ids. + if (type == C.TRACK_TYPE_AUDIO) { + if (audioSampleQueueIndex != C.INDEX_UNSET) { + if (audioSampleQueueMappingDone) { + return sampleQueueTrackIds[audioSampleQueueIndex] == id + ? sampleQueues[audioSampleQueueIndex] + : createDummyTrackOutput(id, type); + } + audioSampleQueueMappingDone = true; + sampleQueueTrackIds[audioSampleQueueIndex] = id; + return sampleQueues[audioSampleQueueIndex]; + } else if (tracksEnded) { + return createDummyTrackOutput(id, type); + } + } else if (type == C.TRACK_TYPE_VIDEO) { + if (videoSampleQueueIndex != C.INDEX_UNSET) { + if (videoSampleQueueMappingDone) { + return sampleQueueTrackIds[videoSampleQueueIndex] == id + ? sampleQueues[videoSampleQueueIndex] + : createDummyTrackOutput(id, type); + } + videoSampleQueueMappingDone = true; + sampleQueueTrackIds[videoSampleQueueIndex] = id; + return sampleQueues[videoSampleQueueIndex]; + } else if (tracksEnded) { + return createDummyTrackOutput(id, type); + } + } else /* sparse track */ { + for (int i = 0; i < trackCount; i++) { + if (sampleQueueTrackIds[i] == id) { + return sampleQueues[i]; + } + } + if (tracksEnded) { + return createDummyTrackOutput(id, type); } - } - if (tracksEnded) { - Log.w(TAG, "Unmapped track with id " + id + " of type " + type); - return new DummyTrackOutput(); } SampleQueue trackOutput = new SampleQueue(allocator); trackOutput.setSampleOffsetUs(sampleOffsetUs); @@ -653,6 +694,13 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; + if (type == C.TRACK_TYPE_AUDIO) { + audioSampleQueueMappingDone = true; + audioSampleQueueIndex = trackCount; + } else if (type == C.TRACK_TYPE_VIDEO) { + videoSampleQueueMappingDone = true; + videoSampleQueueIndex = trackCount; + } sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1); return trackOutput; } @@ -913,4 +961,9 @@ import java.util.Arrays; } return true; } + + private static DummyTrackOutput createDummyTrackOutput(int id, int type) { + Log.w(TAG, "Unmapped track with id " + id + " of type " + type); + return new DummyTrackOutput(); + } } From 0708aa87ba87a54fb5c1e85a6d2c5359439b1f66 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jan 2018 02:11:55 -0800 Subject: [PATCH 278/417] Fix stray calculation in PGS decoder ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182183184 --- .../java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 7ad70397a0..6d60da7d81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -148,7 +148,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { if (sectionLength < 7) { return; } - int totalLength = buffer.readUnsignedInt24() - 4; + int totalLength = buffer.readUnsignedInt24(); if (totalLength < 4) { return; } From 0697fb3955d8a30edb935cc428d515e2fd9d48cb Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jan 2018 05:59:50 -0800 Subject: [PATCH 279/417] Fail on HLS+TS loss of sync Issue:#3632 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182202289 --- RELEASENOTES.md | 10 ++++++---- .../exoplayer2/extractor/ts/TsExtractor.java | 13 +++++++++++-- .../source/hls/HlsSampleStreamWrapper.java | 3 ++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f086bbf515..715e09c977 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,13 +41,15 @@ * Support in-band Emsg events targeting player with scheme id "urn:mpeg:dash:event:2012" and scheme value of either "1", "2" or "3". * Support DASH manifest EventStream elements. -* HLS: Add opt-in support for chunkless preparation in HLS. This allows an - HLS source to finish preparation without downloading any chunks, which can - significantly reduce initial buffering time - ([#3149](https://github.com/google/ExoPlayer/issues/3149)). * DefaultTrackSelector: * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Support disabling of individual text track selection flags. +* HLS: + * Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). + * Fail on loss of sync with Transport Stream. * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). 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 13e669da23..50931e2d90 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 @@ -20,6 +20,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -122,6 +123,7 @@ public final class TsExtractor implements Extractor { private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; + private int bytesSinceLastSync; public TsExtractor() { this(0); @@ -163,7 +165,7 @@ public final class TsExtractor implements Extractor { timestampAdjusters = new ArrayList<>(); timestampAdjusters.add(timestampAdjuster); } - tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); + tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0); trackIds = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); @@ -206,6 +208,7 @@ public final class TsExtractor implements Extractor { continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. resetPayloadReaders(); + bytesSinceLastSync = 0; } @Override @@ -238,8 +241,9 @@ public final class TsExtractor implements Extractor { } // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. - final int limit = tsPacketBuffer.limit(); + int limit = tsPacketBuffer.limit(); int position = tsPacketBuffer.getPosition(); + int searchStart = position; while (position < limit && data[position] != TS_SYNC_BYTE) { position++; } @@ -247,8 +251,13 @@ public final class TsExtractor implements Extractor { int endOfPacket = position + TS_PACKET_SIZE; if (endOfPacket > limit) { + bytesSinceLastSync += position - searchStart; + if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) { + throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream."); + } return RESULT_CONTINUE; } + bytesSinceLastSync = 0; int tsPacketHeader = tsPacketBuffer.readInt(); if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator 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 e4c71e43c5..4a529aef18 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 @@ -20,6 +20,7 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -610,7 +611,7 @@ import java.util.Arrays; } return Loader.DONT_RETRY; } else { - return Loader.RETRY; + return error instanceof ParserException ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } } From 78c6b39ae82843ad8dfdbfe75bab91d254f431f2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jan 2018 06:40:23 -0800 Subject: [PATCH 280/417] Fix HLS media playlist only playback This was broken by [] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182206548 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 3 +++ 1 file changed, 3 insertions(+) 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 4a529aef18..508f2f0f2f 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 @@ -932,6 +932,9 @@ import java.util.Arrays; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); String mimeType = MimeTypes.getMediaMimeType(codecs); + if (mimeType == null) { + mimeType = sampleFormat.sampleMimeType; + } return sampleFormat.copyWithContainerInfo( playlistFormat.id, mimeType, From 65597e0db8b1e525d077a61170f5adc5d4c47cc4 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jan 2018 13:33:39 -0800 Subject: [PATCH 281/417] DashMediaSource variable name cleanup ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182261649 --- .../source/dash/DashMediaSource.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) 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 08e25f216a..28b3de357d 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 @@ -285,17 +285,18 @@ public final class DashMediaSource implements MediaSource { private DataSource dataSource; private Loader loader; private LoaderErrorThrower loaderErrorThrower; + private Handler handler; private Uri manifestUri; - private long manifestLoadStartTimestamp; - private long manifestLoadEndTimestamp; private DashManifest manifest; - private Handler handler; - private boolean pendingManifestLoading; + private boolean manifestLoadPending; + private long manifestLoadStartTimestampMs; + private long manifestLoadEndTimestampMs; private long elapsedRealtimeOffsetMs; + + private int staleManifestReloadAttempt; private long expiredManifestPublishTimeUs; private boolean dynamicMediaPresentationEnded; - private int staleManifestReloadAttempt; private int firstPeriodId; @@ -539,15 +540,15 @@ public final class DashMediaSource implements MediaSource { @Override public void releaseSource() { - pendingManifestLoading = false; + manifestLoadPending = false; dataSource = null; loaderErrorThrower = null; if (loader != null) { loader.release(); loader = null; } - manifestLoadStartTimestamp = 0; - manifestLoadEndTimestamp = 0; + manifestLoadStartTimestampMs = 0; + manifestLoadEndTimestampMs = 0; manifest = null; if (handler != null) { handler.removeCallbacksAndMessages(null); @@ -605,11 +606,11 @@ public final class DashMediaSource implements MediaSource { return; } manifest = newManifest; - manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; - manifestLoadEndTimestamp = elapsedRealtimeMs; + manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; + manifestLoadEndTimestampMs = elapsedRealtimeMs; staleManifestReloadAttempt = 0; if (!manifest.dynamic) { - pendingManifestLoading = false; + manifestLoadPending = false; } if (manifest.location != null) { synchronized (manifestUriLock) { @@ -691,14 +692,14 @@ public final class DashMediaSource implements MediaSource { private void startLoadingManifest() { handler.removeCallbacks(refreshManifestRunnable); if (loader.isLoading()) { - pendingManifestLoading = true; + manifestLoadPending = true; return; } Uri manifestUri; synchronized (manifestUriLock) { manifestUri = this.manifestUri; } - pendingManifestLoading = false; + manifestLoadPending = false; startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), manifestCallback, minLoadableRetryCount); } @@ -722,8 +723,8 @@ public final class DashMediaSource implements MediaSource { private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) { try { - long utcTimestamp = Util.parseXsDateTime(timingElement.value); - onUtcTimestampResolved(utcTimestamp - manifestLoadEndTimestamp); + long utcTimestampMs = Util.parseXsDateTime(timingElement.value); + onUtcTimestampResolved(utcTimestampMs - manifestLoadEndTimestampMs); } catch (ParserException e) { onUtcTimestampResolutionError(e); } @@ -825,7 +826,7 @@ public final class DashMediaSource implements MediaSource { if (windowChangingImplicitly) { handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); } - if (pendingManifestLoading) { + if (manifestLoadPending) { startLoadingManifest(); } else if (scheduleRefresh) { // Schedule an explicit refresh if needed. @@ -852,7 +853,7 @@ public final class DashMediaSource implements MediaSource { // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ minUpdatePeriodMs = 5000; } - long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriodMs; + long nextLoadTimestamp = manifestLoadStartTimestampMs + minUpdatePeriodMs; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad); } From 605aeb3a42c66c01069071c6d1fe6bba3ee39084 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 18 Jan 2018 06:46:31 -0800 Subject: [PATCH 282/417] Make id3 context usage robust against container format changes Issue:#3622 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182363243 --- RELEASENOTES.md | 6 ++++-- .../android/exoplayer2/source/hls/HlsMediaChunk.java | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 715e09c977..ed6dedd0c3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,8 +2,6 @@ ### dev-v2 (not yet released) ### -* HLS: Fix mime type propagation - ([#3653](https://github.com/google/ExoPlayer/issues/3653)). * SimpleExoPlayerView: Automatically apply video rotation if `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). @@ -50,6 +48,10 @@ significantly reduce initial buffering time ([#3149](https://github.com/google/ExoPlayer/issues/3149)). * Fail on loss of sync with Transport Stream. + * Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). + * Fix ID3 context reuse across segment format changes + ([#3622](https://github.com/google/ExoPlayer/issues/3622)). * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 50c1200fae..5457f33867 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -137,9 +137,13 @@ import java.util.concurrent.atomic.AtomicInteger; reusingExtractor = extractor == previousExtractor; initLoadCompleted = reusingExtractor && initDataSpec != null; if (isPackedAudioExtractor) { - id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder(); - id3Data = previousChunk != null ? previousChunk.id3Data - : new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + if (previousChunk != null && previousChunk.id3Data != null) { + id3Decoder = previousChunk.id3Decoder; + id3Data = previousChunk.id3Data; + } else { + id3Decoder = new Id3Decoder(); + id3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } } else { id3Decoder = null; id3Data = null; From c577d9d35191b11b46ec215be4812b96accd06a0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jan 2018 06:59:30 -0800 Subject: [PATCH 283/417] Let SimpleExoPlayerView/LeanbackPlayerAdapter bind with any Player Also sanitize naming (PlayerView/PlayerControlView). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182364487 --- RELEASENOTES.md | 8 + .../exoplayer2/castdemo/MainActivity.java | 21 +- .../exoplayer2/castdemo/PlayerManager.java | 52 +- .../src/main/res/layout/main_activity.xml | 4 +- .../exoplayer2/imademo/MainActivity.java | 4 +- .../exoplayer2/imademo/PlayerManager.java | 8 +- .../ima/src/main/res/layout/main_activity.xml | 2 +- .../exoplayer2/demo/PlayerActivity.java | 28 +- .../src/main/res/layout/player_activity.xml | 2 +- extensions/cast/README.md | 5 +- .../exoplayer2/ext/cast/CastPlayer.java | 10 + .../ext/leanback/LeanbackPlayerAdapter.java | 39 +- .../android/exoplayer2/ExoPlayerImpl.java | 10 + .../com/google/android/exoplayer2/Player.java | 138 +++ .../android/exoplayer2/SimpleExoPlayer.java | 177 +-- .../exoplayer2/video/VideoListener.java | 45 + .../exoplayer2/ui/PlaybackControlView.java | 1078 +--------------- .../exoplayer2/ui/PlayerControlView.java | 1101 +++++++++++++++++ .../android/exoplayer2/ui/PlayerView.java | 1057 ++++++++++++++++ .../exoplayer2/ui/SimpleExoPlayerView.java | 1011 +-------------- .../res/layout/exo_player_control_view.xml | 18 + .../src/main/res/layout/exo_player_view.xml | 18 + library/ui/src/main/res/values/attrs.xml | 6 +- .../exoplayer2/testutil/StubExoPlayer.java | 10 + 24 files changed, 2575 insertions(+), 2277 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java create mode 100644 library/ui/src/main/res/layout/exo_player_control_view.xml create mode 100644 library/ui/src/main/res/layout/exo_player_view.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed6dedd0c3..1a54a44058 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). * Player interface: + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. @@ -17,6 +21,10 @@ more customization of the message. Now supports setting a message delivery playback position and/or a delivery handler. ([#2189](https://github.com/google/ExoPlayer/issues/2189)). +* UI components: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index d34888352f..07781c091e 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; @@ -50,8 +50,8 @@ import com.google.android.gms.cast.framework.CastContext; public class MainActivity extends AppCompatActivity implements OnClickListener, PlayerManager.QueuePositionListener { - private SimpleExoPlayerView simpleExoPlayerView; - private PlaybackControlView castControlView; + private PlayerView localPlayerView; + private PlayerControlView castControlView; private PlayerManager playerManager; private MediaQueueAdapter listAdapter; private CastContext castContext; @@ -66,8 +66,8 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, setContentView(R.layout.main_activity); - simpleExoPlayerView = findViewById(R.id.player_view); - simpleExoPlayerView.requestFocus(); + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); castControlView = findViewById(R.id.cast_control_view); @@ -93,8 +93,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onResume() { super.onResume(); - playerManager = PlayerManager.createPlayerManager(this, simpleExoPlayerView, castControlView, - this, castContext); + playerManager = + PlayerManager.createPlayerManager( + /* queuePositionListener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); } @Override diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 548482f61f..ac488ff3fd 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -40,8 +40,8 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaInfo; @@ -73,12 +73,12 @@ import java.util.ArrayList; private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); - private final SimpleExoPlayerView exoPlayerView; - private final PlaybackControlView castControlView; + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; private final ArrayList mediaQueue; - private final QueuePositionListener listener; + private final QueuePositionListener queuePositionListener; private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; private boolean castMediaQueueCreationPending; @@ -86,25 +86,33 @@ import java.util.ArrayList; private Player currentPlayer; /** - * @param listener A {@link QueuePositionListener} for queue position changes. - * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. - * @param castControlView The {@link PlaybackControlView} to control remote playback. + * @param queuePositionListener A {@link QueuePositionListener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. * @param context A {@link Context}. * @param castContext The {@link CastContext}. */ - public static PlayerManager createPlayerManager(QueuePositionListener listener, - SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, Context context, + public static PlayerManager createPlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, CastContext castContext) { - PlayerManager playerManager = new PlayerManager(listener, exoPlayerView, castControlView, - context, castContext); + PlayerManager playerManager = + new PlayerManager( + queuePositionListener, localPlayerView, castControlView, context, castContext); playerManager.init(); return playerManager; } - private PlayerManager(QueuePositionListener listener, SimpleExoPlayerView exoPlayerView, - PlaybackControlView castControlView, Context context, CastContext castContext) { - this.listener = listener; - this.exoPlayerView = exoPlayerView; + private PlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.queuePositionListener = queuePositionListener; + this.localPlayerView = localPlayerView; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; @@ -113,7 +121,7 @@ import java.util.ArrayList; RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); exoPlayer.addListener(this); - exoPlayerView.setPlayer(exoPlayer); + localPlayerView.setPlayer(exoPlayer); castPlayer = new CastPlayer(castContext); castPlayer.addListener(this); @@ -242,7 +250,7 @@ import java.util.ArrayList; */ public boolean dispatchKeyEvent(KeyEvent event) { if (currentPlayer == exoPlayer) { - return exoPlayerView.dispatchKeyEvent(event); + return localPlayerView.dispatchKeyEvent(event); } else /* currentPlayer == castPlayer */ { return castControlView.dispatchKeyEvent(event); } @@ -256,7 +264,7 @@ import java.util.ArrayList; mediaQueue.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); - exoPlayerView.setPlayer(null); + localPlayerView.setPlayer(null); exoPlayer.release(); } @@ -309,10 +317,10 @@ import java.util.ArrayList; // View management. if (currentPlayer == exoPlayer) { - exoPlayerView.setVisibility(View.VISIBLE); + localPlayerView.setVisibility(View.VISIBLE); castControlView.hide(); } else /* currentPlayer == castPlayer */ { - exoPlayerView.setVisibility(View.GONE); + localPlayerView.setVisibility(View.GONE); castControlView.show(); } @@ -380,7 +388,7 @@ import java.util.ArrayList; if (this.currentItemIndex != currentItemIndex) { int oldIndex = this.currentItemIndex; this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); + queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); } } diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 1cce287b28..01e48cdea7 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> - - - - diff --git a/extensions/cast/README.md b/extensions/cast/README.md index 73f7041729..8666690661 100644 --- a/extensions/cast/README.md +++ b/extensions/cast/README.md @@ -27,7 +27,4 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## Create a `CastPlayer` and use it to integrate Cast into your app using -ExoPlayer's common Player interface. You can try the Cast Extension to see how a -[PlaybackControlView][] can be used to control playback in a remote receiver app. - -[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html +ExoPlayer's common `Player` interface. diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 1f39fe0023..e545dfd352 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -280,6 +280,16 @@ public final class CastPlayer implements Player { // Player implementation. + @Override + public VideoComponent getVideoComponent() { + return null; + } + + @Override + public TextComponent getTextComponent() { + return null; + } + @Override public void addListener(EventListener listener) { listeners.add(listener); diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index c9ed54398e..cbb950093c 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -33,13 +33,11 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.video.VideoListener; -/** - * Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}. - */ +/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */ public final class LeanbackPlayerAdapter extends PlayerAdapter { static { @@ -47,7 +45,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } private final Context context; - private final SimpleExoPlayer player; + private final Player player; private final Handler handler; private final ComponentListener componentListener; private final Runnable updateProgressRunnable; @@ -60,14 +58,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { /** * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the - * {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when - * it's no longer required. + * {@link Player} instance. The caller remains responsible for releasing the player when it's no + * longer required. * * @param context The current context (activity). * @param player Instance of your exoplayer that needs to be configured. * @param updatePeriodMs The delay between player control updates, in milliseconds. */ - public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) { + public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) { this.context = context; this.player = player; handler = new Handler(); @@ -115,13 +113,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } notifyStateChanged(); player.addListener(componentListener); - player.addVideoListener(componentListener); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.addVideoListener(componentListener); + } } @Override public void onDetachedFromHost() { player.removeListener(componentListener); - player.removeVideoListener(componentListener); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.removeVideoListener(componentListener); + } if (surfaceHolderGlueHost != null) { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; @@ -196,7 +200,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { /* package */ void setVideoSurface(Surface surface) { hasSurface = surface != null; - player.setVideoSurface(surface); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); + } maybeNotifyPreparedStateChanged(getCallback()); } @@ -219,8 +226,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - private final class ComponentListener extends Player.DefaultEventListener implements - SimpleExoPlayer.VideoListener, SurfaceHolder.Callback { + private final class ComponentListener extends Player.DefaultEventListener + implements SurfaceHolder.Callback, VideoListener { // SurfaceHolder.Callback implementation. @@ -274,11 +281,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } - // SimpleExoplayerView.Callback implementation. + // VideoListener implementation. @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b5f6e623eb..83bbdd1157 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -124,6 +124,16 @@ import java.util.concurrent.CopyOnWriteArraySet; internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } + @Override + public VideoComponent getVideoComponent() { + return null; + } + + @Override + public TextComponent getTextComponent() { + return null; + } + @Override public Looper getPlaybackLooper() { return internalPlayer.getPlaybackLooper(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 97cd9449d3..443ff8a2ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -18,8 +18,14 @@ package com.google.android.exoplayer2; import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.video.VideoListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -44,6 +50,130 @@ import java.lang.annotation.RetentionPolicy; */ public interface Player { + /** The video component of a {@link Player}. */ + interface VideoComponent { + + /** + * Sets the video scaling mode. + * + * @param videoScalingMode The video scaling mode. + */ + void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode); + + /** Returns the video scaling mode. */ + @C.VideoScalingMode + int getVideoScalingMode(); + + /** + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + void addVideoListener(VideoListener listener); + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + void removeVideoListener(VideoListener listener); + + /** + * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} + * currently set on the player. + */ + void clearVideoSurface(); + + /** + * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for + * tracking the lifecycle of the surface, and must clear the surface by calling {@code + * setVideoSurface(null)} if the surface is destroyed. + * + *

      If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link + * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link + * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather + * than this method, since passing the holder allows the player to track the lifecycle of the + * surface automatically. + * + * @param surface The {@link Surface}. + */ + void setVideoSurface(Surface surface); + + /** + * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surface The surface to clear. + */ + void clearVideoSurface(Surface surface); + + /** + * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be + * rendered. The player will track the lifecycle of the surface automatically. + * + * @param surfaceHolder The surface holder. + */ + void setVideoSurfaceHolder(SurfaceHolder surfaceHolder); + + /** + * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being + * rendered if it matches the one passed. Else does nothing. + * + * @param surfaceHolder The surface holder to clear. + */ + void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder); + + /** + * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param surfaceView The surface view. + */ + void setVideoSurfaceView(SurfaceView surfaceView); + + /** + * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one + * passed. Else does nothing. + * + * @param surfaceView The texture view to clear. + */ + void clearVideoSurfaceView(SurfaceView surfaceView); + + /** + * Sets the {@link TextureView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param textureView The texture view. + */ + void setVideoTextureView(TextureView textureView); + + /** + * Clears the {@link TextureView} onto which video is being rendered if it matches the one + * passed. Else does nothing. + * + * @param textureView The texture view to clear. + */ + void clearVideoTextureView(TextureView textureView); + } + + /** The text component of a {@link Player}. */ + interface TextComponent { + + /** + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + void addTextOutput(TextOutput listener); + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + void removeTextOutput(TextOutput listener); + } + /** * Listener of changes in player state. */ @@ -298,6 +428,14 @@ public interface Player { */ int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** Returns the component of this player for video output, or null if video is not supported. */ + @Nullable + VideoComponent getVideoComponent(); + + /** Returns the component of this player for text output, or null if text is not supported. */ + @Nullable + TextComponent getTextComponent(); + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ec53e5a964..98ef35d62c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -50,39 +50,11 @@ import java.util.concurrent.CopyOnWriteArraySet; * be obtained from {@link ExoPlayerFactory}. */ @TargetApi(16) -public class SimpleExoPlayer implements ExoPlayer { +public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent { - /** - * A listener for video rendering information from a {@link SimpleExoPlayer}. - */ - public interface VideoListener { - - /** - * Called each time there's a change in the size of the video being rendered. - * - * @param width The video width in pixels. - * @param height The video height in pixels. - * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise - * rotation in degrees that the application should apply for the video for it to be rendered - * in the correct orientation. This value will always be zero on API levels 21 and above, - * since the renderer will apply all necessary rotations internally. On earlier API levels - * this is not possible. Applications that use {@link android.view.TextureView} can apply - * the rotation by calling {@link android.view.TextureView#setTransform}. Applications that - * do not expect to encounter rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic - * content. - */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); - - /** - * Called when a frame is rendered for the first time since setting the surface, and when a - * frame is rendered for the first time since a video track was selected. - */ - void onRenderedFirstFrame(); - - } + /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ + @Deprecated + public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {} private static final String TAG = "SimpleExoPlayer"; @@ -90,7 +62,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; - private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet + videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; @@ -154,14 +127,25 @@ public class SimpleExoPlayer implements ExoPlayer { player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); } + @Override + public VideoComponent getVideoComponent() { + return this; + } + + @Override + public TextComponent getTextComponent() { + return this; + } + /** * Sets the video scaling mode. - *

      - * Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is - * enabled and if the output surface is owned by a {@link android.view.SurfaceView}. + * + *

      Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} + * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}. * * @param videoScalingMode The video scaling mode. */ + @Override public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; for (Renderer renderer : renderers) { @@ -175,57 +159,30 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Returns the video scaling mode. - */ + @Override public @C.VideoScalingMode int getVideoScalingMode() { return videoScalingMode; } - /** - * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} - * currently set on the player. - */ + @Override public void clearVideoSurface() { setVideoSurface(null); } - /** - * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for - * tracking the lifecycle of the surface, and must clear the surface by calling - * {@code setVideoSurface(null)} if the surface is destroyed. - *

      - * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link SurfaceHolder} - * then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, - * {@link #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} - * rather than this method, since passing the holder allows the player to track the lifecycle of - * the surface automatically. - * - * @param surface The {@link Surface}. - */ + @Override public void setVideoSurface(Surface surface) { removeSurfaceCallbacks(); setVideoSurfaceInternal(surface, false); } - /** - * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surface The surface to clear. - */ + @Override public void clearVideoSurface(Surface surface) { if (surface != null && surface == this.surface) { setVideoSurface(null); } } - /** - * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be - * rendered. The player will track the lifecycle of the surface automatically. - * - * @param surfaceHolder The surface holder. - */ + @Override public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { removeSurfaceCallbacks(); this.surfaceHolder = surfaceHolder; @@ -238,44 +195,24 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being - * rendered if it matches the one passed. Else does nothing. - * - * @param surfaceHolder The surface holder to clear. - */ + @Override public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { setVideoSurfaceHolder(null); } } - /** - * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param surfaceView The surface view. - */ + @Override public void setVideoSurfaceView(SurfaceView surfaceView) { setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surfaceView The texture view to clear. - */ + @Override public void clearVideoSurfaceView(SurfaceView surfaceView) { clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Sets the {@link TextureView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param textureView The texture view. - */ + @Override public void setVideoTextureView(TextureView textureView) { removeSurfaceCallbacks(); this.textureView = textureView; @@ -292,12 +229,7 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param textureView The texture view to clear. - */ + @Override public void clearVideoTextureView(TextureView textureView) { if (textureView != null && textureView == this.textureView) { setVideoTextureView(null); @@ -446,21 +378,13 @@ public class SimpleExoPlayer implements ExoPlayer { return audioDecoderCounters; } - /** - * Adds a listener to receive video events. - * - * @param listener The listener to register. - */ - public void addVideoListener(VideoListener listener) { + @Override + public void addVideoListener(com.google.android.exoplayer2.video.VideoListener listener) { videoListeners.add(listener); } - /** - * Removes a listener of video events. - * - * @param listener The listener to unregister. - */ - public void removeVideoListener(VideoListener listener) { + @Override + public void removeVideoListener(com.google.android.exoplayer2.video.VideoListener listener) { videoListeners.remove(listener); } @@ -468,7 +392,7 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive video events, removing all existing listeners. * * @param listener The listener. - * @deprecated Use {@link #addVideoListener(VideoListener)}. + * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated public void setVideoListener(VideoListener listener) { @@ -479,30 +403,23 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Equivalent to {@link #removeVideoListener(VideoListener)}. + * Equivalent to {@link #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. * * @param listener The listener to clear. - * @deprecated Use {@link #removeVideoListener(VideoListener)}. + * @deprecated Use {@link + * #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated public void clearVideoListener(VideoListener listener) { removeVideoListener(listener); } - /** - * Registers an output to receive text events. - * - * @param listener The output to register. - */ + @Override public void addTextOutput(TextOutput listener) { textOutputs.add(listener); } - /** - * Removes a text output. - * - * @param listener The output to remove. - */ + @Override public void removeTextOutput(TextOutput listener) { textOutputs.remove(listener); } @@ -532,20 +449,10 @@ public class SimpleExoPlayer implements ExoPlayer { removeTextOutput(output); } - /** - * Registers an output to receive metadata events. - * - * @param listener The output to register. - */ public void addMetadataOutput(MetadataOutput listener) { metadataOutputs.add(listener); } - /** - * Removes a metadata output. - * - * @param listener The output to remove. - */ public void removeMetadataOutput(MetadataOutput listener) { metadataOutputs.remove(listener); } @@ -978,7 +885,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - for (VideoListener videoListener : videoListeners) { + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -991,7 +898,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onRenderedFirstFrame(Surface surface) { if (SimpleExoPlayer.this.surface == surface) { - for (VideoListener videoListener : videoListeners) { + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { videoListener.onRenderedFirstFrame(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java new file mode 100644 index 0000000000..ab09e0bbc2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java @@ -0,0 +1,45 @@ +/* + * 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.video; + +/** A listener for metadata corresponding to video being rendered. */ +public interface VideoListener { + + /** + * Called each time there's a change in the size of the video being rendered. + * + * @param width The video width in pixels. + * @param height The video height in pixels. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. On earlier API levels + * this is not possible. Applications that use {@link android.view.TextureView} can apply the + * rotation by calling {@link android.view.TextureView#setTransform}. Applications that do not + * expect to encounter rotated videos can safely ignore this parameter. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * content. + */ + void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); + + /** + * Called when a frame is rendered for the first time since setting the surface, and when a frame + * is rendered for the first time since a video track was selected. + */ + void onRenderedFirstFrame(); +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index fefbb0797a..da03d28cba 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,171 +15,24 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.SuppressLint; import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.os.SystemClock; -import android.support.annotation.Nullable; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; -import java.util.Formatter; -import java.util.Locale; -/** - * A view for controlling {@link Player} instances. - * - *

      A PlaybackControlView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. - * - *

      Attributes

      - * - * The following attributes can be set on a PlaybackControlView when used in a layout XML file: - * - *

      - * - *

        - *
      • {@code show_timeout} - The time between the last user interaction and the controls - * being automatically hidden, in milliseconds. Use zero if the controls should not - * automatically timeout. - *
          - *
        • Corresponding method: {@link #setShowTimeoutMs(int)} - *
        • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} - *
        - *
      • {@code rewind_increment} - The duration of the rewind applied when the user taps the - * rewind button, in milliseconds. Use zero to disable the rewind button. - *
          - *
        • Corresponding method: {@link #setRewindIncrementMs(int)} - *
        • Default: {@link #DEFAULT_REWIND_MS} - *
        - *
      • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. - *
          - *
        • Corresponding method: {@link #setFastForwardIncrementMs(int)} - *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} - *
        - *
      • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat - * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, - * or {@code one|all}. - *
          - *
        • Corresponding method: {@link #setRepeatToggleModes(int)} - *
        • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES} - *
        - *
      • {@code show_shuffle_button} - Whether the shuffle button is shown. - *
          - *
        • Corresponding method: {@link #setShowShuffleButton(boolean)} - *
        • Default: false - *
        - *
      • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See - * below for more details. - *
          - *
        • Corresponding method: None - *
        • Default: {@code R.id.exo_playback_control_view} - *
        - *
      - * - *

      Overriding the layout file

      - * - * To customize the layout of PlaybackControlView throughout your app, or just for certain - * configurations, you can define {@code exo_playback_control_view.xml} layout files in your - * application {@code res/layout*} directories. These layouts will override the one provided by the - * ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and - * binds its children by looking for the following ids: - * - *

      - * - *

        - *
      • {@code exo_play} - The play button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_pause} - The pause button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_ffwd} - The fast forward button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_rew} - The rewind button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_prev} - The previous track button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_next} - The next track button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_repeat_toggle} - The repeat toggle button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_shuffle} - The shuffle button. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_position} - Text view displaying the current playback position. - *
          - *
        • Type: {@link TextView} - *
        - *
      • {@code exo_duration} - Text view displaying the current media duration. - *
          - *
        • Type: {@link TextView} - *
        - *
      • {@code exo_progress} - Time bar that's updated during playback and allows seeking. - *
          - *
        • Type: {@link TimeBar} - *
        - *
      - * - *

      All child views are optional and so can be omitted if not required, however where defined they - * must be of the expected type. - * - *

      Specifying a custom layout file

      - * - * Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of - * PlaybackControlView throughout your application. It's also possible to customize the layout for a - * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} - * attribute on a PlaybackControlView. This will cause the specified layout to be inflated instead - * of {@code exo_playback_control_view.xml} for only the instance on which the attribute is set. - */ -public class PlaybackControlView extends FrameLayout { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); - } +/** @deprecated Use {@link PlayerControlView}. */ +@Deprecated +public class PlaybackControlView extends PlayerControlView { /** @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. */ @Deprecated public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} - /** Listener to be notified about changes of the visibility of the UI control. */ - public interface VisibilityListener { - - /** - * Called when the visibility changes. - * - * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. - */ - void onVisibilityChange(int visibility); - } + /** + * @deprecated Use {@link com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener}. + */ + @Deprecated + public interface VisibilityListener + extends com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener {} private static final class DefaultControlDispatcher extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} @@ -188,927 +41,34 @@ public class PlaybackControlView extends FrameLayout { public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); /** The default fast forward increment, in milliseconds. */ - public static final int DEFAULT_FAST_FORWARD_MS = 15000; + public static final int DEFAULT_FAST_FORWARD_MS = PlayerControlView.DEFAULT_FAST_FORWARD_MS; /** The default rewind increment, in milliseconds. */ - public static final int DEFAULT_REWIND_MS = 5000; + public static final int DEFAULT_REWIND_MS = PlayerControlView.DEFAULT_REWIND_MS; /** The default show timeout, in milliseconds. */ - public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + public static final int DEFAULT_SHOW_TIMEOUT_MS = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; /** The default repeat toggle modes. */ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = - RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + PlayerControlView.DEFAULT_REPEAT_TOGGLE_MODES; /** The maximum number of windows that can be shown in a multi-window time bar. */ - public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; - - private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; - - private final ComponentListener componentListener; - private final View previousButton; - private final View nextButton; - private final View playButton; - private final View pauseButton; - private final View fastForwardButton; - private final View rewindButton; - private final ImageView repeatToggleButton; - private final View shuffleButton; - private final TextView durationView; - private final TextView positionView; - private final TimeBar timeBar; - private final StringBuilder formatBuilder; - private final Formatter formatter; - private final Timeline.Period period; - private final Timeline.Window window; - - private final Drawable repeatOffButtonDrawable; - private final Drawable repeatOneButtonDrawable; - private final Drawable repeatAllButtonDrawable; - private final String repeatOffButtonContentDescription; - private final String repeatOneButtonContentDescription; - private final String repeatAllButtonContentDescription; - - private Player player; - private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; - private VisibilityListener visibilityListener; - - private boolean isAttachedToWindow; - private boolean showMultiWindowTimeBar; - private boolean multiWindowTimeBar; - private boolean scrubbing; - private int rewindMs; - private int fastForwardMs; - private int showTimeoutMs; - private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; - private boolean showShuffleButton; - private long hideAtMs; - private long[] adGroupTimesMs; - private boolean[] playedAdGroups; - private long[] extraAdGroupTimesMs; - private boolean[] extraPlayedAdGroups; - - private final Runnable updateProgressAction = - new Runnable() { - @Override - public void run() { - updateProgress(); - } - }; - - private final Runnable hideAction = - new Runnable() { - @Override - public void run() { - hide(); - } - }; + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = + PlayerControlView.MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR; public PlaybackControlView(Context context) { - this(context, null); + super(context); } public PlaybackControlView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, attrs); + super(context, attrs, defStyleAttr); } public PlaybackControlView( Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { - super(context, attrs, defStyleAttr); - int controllerLayoutId = R.layout.exo_playback_control_view; - rewindMs = DEFAULT_REWIND_MS; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; - showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; - repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; - showShuffleButton = false; - if (playbackAttrs != null) { - TypedArray a = - context - .getTheme() - .obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); - try { - rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); - fastForwardMs = - a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); - showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); - controllerLayoutId = - a.getResourceId( - R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); - repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); - showShuffleButton = - a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, showShuffleButton); - } finally { - a.recycle(); - } - } - period = new Timeline.Period(); - window = new Timeline.Window(); - formatBuilder = new StringBuilder(); - formatter = new Formatter(formatBuilder, Locale.getDefault()); - adGroupTimesMs = new long[0]; - playedAdGroups = new boolean[0]; - extraAdGroupTimesMs = new long[0]; - extraPlayedAdGroups = new boolean[0]; - componentListener = new ComponentListener(); - controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); - - LayoutInflater.from(context).inflate(controllerLayoutId, this); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - durationView = findViewById(R.id.exo_duration); - positionView = findViewById(R.id.exo_position); - timeBar = findViewById(R.id.exo_progress); - if (timeBar != null) { - timeBar.addListener(componentListener); - } - playButton = findViewById(R.id.exo_play); - if (playButton != null) { - playButton.setOnClickListener(componentListener); - } - pauseButton = findViewById(R.id.exo_pause); - if (pauseButton != null) { - pauseButton.setOnClickListener(componentListener); - } - previousButton = findViewById(R.id.exo_prev); - if (previousButton != null) { - previousButton.setOnClickListener(componentListener); - } - nextButton = findViewById(R.id.exo_next); - if (nextButton != null) { - nextButton.setOnClickListener(componentListener); - } - rewindButton = findViewById(R.id.exo_rew); - if (rewindButton != null) { - rewindButton.setOnClickListener(componentListener); - } - fastForwardButton = findViewById(R.id.exo_ffwd); - if (fastForwardButton != null) { - fastForwardButton.setOnClickListener(componentListener); - } - repeatToggleButton = findViewById(R.id.exo_repeat_toggle); - if (repeatToggleButton != null) { - repeatToggleButton.setOnClickListener(componentListener); - } - shuffleButton = findViewById(R.id.exo_shuffle); - if (shuffleButton != null) { - shuffleButton.setOnClickListener(componentListener); - } - Resources resources = context.getResources(); - repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); - repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); - repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); - repeatOffButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_off_description); - repeatOneButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_one_description); - repeatAllButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_all_description); + super(context, attrs, defStyleAttr, playbackAttrs); } - @SuppressWarnings("ResourceType") - private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( - TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); - } - - /** - * Returns the {@link Player} currently being controlled by this view, or null if no player is - * set. - */ - public Player getPlayer() { - return player; - } - - /** - * Sets the {@link Player} to control. - * - * @param player The {@link Player} to control. - */ - public void setPlayer(Player player) { - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - } - this.player = player; - if (player != null) { - player.addListener(componentListener); - } - updateAll(); - } - - /** - * Sets whether the time bar should show all windows, as opposed to just the current one. If the - * timeline has a period with unknown duration or more than {@link - * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single - * window. - * - * @param showMultiWindowTimeBar Whether the time bar should show all windows. - */ - public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - this.showMultiWindowTimeBar = showMultiWindowTimeBar; - updateTimeBarMode(); - } - - /** - * Sets the millisecond positions of extra ad markers relative to the start of the window (or - * timeline, if in multi-window mode) and whether each extra ad has been played or not. The - * markers are shown in addition to any ad markers for ads in the player's timeline. - * - * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or - * {@code null} to show no extra ad markers. - * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad - * markers. - */ - public void setExtraAdGroupMarkers( - @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { - if (extraAdGroupTimesMs == null) { - this.extraAdGroupTimesMs = new long[0]; - this.extraPlayedAdGroups = new boolean[0]; - } else { - Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); - this.extraAdGroupTimesMs = extraAdGroupTimesMs; - this.extraPlayedAdGroups = extraPlayedAdGroups; - } - updateProgress(); - } - - /** - * Sets the {@link VisibilityListener}. - * - * @param listener The listener to be notified about visibility changes. - */ - public void setVisibilityListener(VisibilityListener listener) { - this.visibilityListener = listener; - } - - /** - * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. - * - * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null - * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. - */ - public void setControlDispatcher( - @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { - this.controlDispatcher = - controlDispatcher == null - ? new com.google.android.exoplayer2.DefaultControlDispatcher() - : controlDispatcher; - } - - /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. - */ - public void setRewindIncrementMs(int rewindMs) { - this.rewindMs = rewindMs; - updateNavigation(); - } - - /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. - */ - public void setFastForwardIncrementMs(int fastForwardMs) { - this.fastForwardMs = fastForwardMs; - updateNavigation(); - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input. - * - * @return The duration in milliseconds. A non-positive value indicates that the controls will - * remain visible indefinitely. - */ - public int getShowTimeoutMs() { - return showTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input. - * - * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls - * to remain visible indefinitely. - */ - public void setShowTimeoutMs(int showTimeoutMs) { - this.showTimeoutMs = showTimeoutMs; - // showTimeoutMs is changed, so call hideAfterTimeout to reset the timeout. - if (isVisible()) { - hideAfterTimeout(); - } - } - - /** - * Returns which repeat toggle modes are enabled. - * - * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. - */ - public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { - return repeatToggleModes; - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - this.repeatToggleModes = repeatToggleModes; - if (player != null) { - @Player.RepeatMode int currentMode = player.getRepeatMode(); - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE - && currentMode != Player.REPEAT_MODE_OFF) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE - && currentMode == Player.REPEAT_MODE_ALL) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL - && currentMode == Player.REPEAT_MODE_ONE) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); - } - } - } - - /** Returns whether the shuffle button is shown. */ - public boolean getShowShuffleButton() { - return showShuffleButton; - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - this.showShuffleButton = showShuffleButton; - updateShuffleButton(); - } - - /** - * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will - * be automatically hidden after this duration of time has elapsed without user input. - */ - public void show() { - if (!isVisible()) { - setVisibility(VISIBLE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); - } - updateAll(); - requestPlayPauseFocus(); - } - // Call hideAfterTimeout even if already visible to reset the timeout. - hideAfterTimeout(); - } - - /** Hides the controller. */ - public void hide() { - if (isVisible()) { - setVisibility(GONE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); - } - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); - hideAtMs = C.TIME_UNSET; - } - } - - /** Returns whether the controller is currently visible. */ - public boolean isVisible() { - return getVisibility() == VISIBLE; - } - - private void hideAfterTimeout() { - removeCallbacks(hideAction); - if (showTimeoutMs > 0) { - hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; - if (isAttachedToWindow) { - postDelayed(hideAction, showTimeoutMs); - } - } else { - hideAtMs = C.TIME_UNSET; - } - } - - private void updateAll() { - updatePlayPauseButton(); - updateNavigation(); - updateRepeatModeButton(); - updateShuffleButton(); - updateProgress(); - } - - private void updatePlayPauseButton() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - boolean requestPlayPauseFocus = false; - boolean playing = player != null && player.getPlayWhenReady(); - if (playButton != null) { - requestPlayPauseFocus |= playing && playButton.isFocused(); - playButton.setVisibility(playing ? View.GONE : View.VISIBLE); - } - if (pauseButton != null) { - requestPlayPauseFocus |= !playing && pauseButton.isFocused(); - pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); - } - if (requestPlayPauseFocus) { - requestPlayPauseFocus(); - } - } - - private void updateNavigation() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - Timeline timeline = player != null ? player.getCurrentTimeline() : null; - boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); - boolean isSeekable = false; - boolean enablePrevious = false; - boolean enableNext = false; - if (haveNonEmptyTimeline && !player.isPlayingAd()) { - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - isSeekable = window.isSeekable; - enablePrevious = - isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; - enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; - } - setButtonEnabled(enablePrevious, previousButton); - setButtonEnabled(enableNext, nextButton); - setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); - setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); - if (timeBar != null) { - timeBar.setEnabled(isSeekable); - } - } - - private void updateRepeatModeButton() { - if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { - return; - } - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { - repeatToggleButton.setVisibility(View.GONE); - return; - } - if (player == null) { - setButtonEnabled(false, repeatToggleButton); - return; - } - setButtonEnabled(true, repeatToggleButton); - switch (player.getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); - repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); - break; - case Player.REPEAT_MODE_ONE: - repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); - repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); - break; - case Player.REPEAT_MODE_ALL: - repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); - repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); - break; - default: - // Never happens. - } - repeatToggleButton.setVisibility(View.VISIBLE); - } - - private void updateShuffleButton() { - if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { - return; - } - if (!showShuffleButton) { - shuffleButton.setVisibility(View.GONE); - } else if (player == null) { - setButtonEnabled(false, shuffleButton); - } else { - shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); - shuffleButton.setEnabled(true); - shuffleButton.setVisibility(View.VISIBLE); - } - } - - private void updateTimeBarMode() { - if (player == null) { - return; - } - multiWindowTimeBar = - showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); - } - - private void updateProgress() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - - long position = 0; - long bufferedPosition = 0; - long duration = 0; - if (player != null) { - long currentWindowTimeBarOffsetUs = 0; - long durationUs = 0; - int adGroupCount = 0; - Timeline timeline = player.getCurrentTimeline(); - if (!timeline.isEmpty()) { - int currentWindowIndex = player.getCurrentWindowIndex(); - int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; - int lastWindowIndex = - multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; - for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { - if (i == currentWindowIndex) { - currentWindowTimeBarOffsetUs = durationUs; - } - timeline.getWindow(i, window); - if (window.durationUs == C.TIME_UNSET) { - Assertions.checkState(!multiWindowTimeBar); - break; - } - for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - timeline.getPeriod(j, period); - int periodAdGroupCount = period.getAdGroupCount(); - for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { - long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); - if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { - if (period.durationUs == C.TIME_UNSET) { - // Don't show ad markers for postrolls in periods with unknown duration. - continue; - } - adGroupTimeInPeriodUs = period.durationUs; - } - long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { - if (adGroupCount == adGroupTimesMs.length) { - int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); - playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); - } - adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); - playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); - adGroupCount++; - } - } - } - durationUs += window.durationUs; - } - } - duration = C.usToMs(durationUs); - position = C.usToMs(currentWindowTimeBarOffsetUs); - bufferedPosition = position; - if (player.isPlayingAd()) { - position += player.getContentPosition(); - bufferedPosition = position; - } else { - position += player.getCurrentPosition(); - bufferedPosition += player.getBufferedPosition(); - } - if (timeBar != null) { - int extraAdGroupCount = extraAdGroupTimesMs.length; - int totalAdGroupCount = adGroupCount + extraAdGroupCount; - if (totalAdGroupCount > adGroupTimesMs.length) { - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); - playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); - } - System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); - System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); - } - } - if (durationView != null) { - durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); - } - if (positionView != null && !scrubbing) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - if (timeBar != null) { - timeBar.setPosition(position); - timeBar.setBufferedPosition(bufferedPosition); - timeBar.setDuration(duration); - } - - // Cancel any pending updates and schedule a new one if necessary. - removeCallbacks(updateProgressAction); - int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); - if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { - long delayMs; - if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { - float playbackSpeed = player.getPlaybackParameters().speed; - if (playbackSpeed <= 0.1f) { - delayMs = 1000; - } else if (playbackSpeed <= 5f) { - long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); - long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); - if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { - mediaTimeDelayMs += mediaTimeUpdatePeriodMs; - } - delayMs = - playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); - } else { - delayMs = 200; - } - } else { - delayMs = 1000; - } - postDelayed(updateProgressAction, delayMs); - } - } - - private void requestPlayPauseFocus() { - boolean playing = player != null && player.getPlayWhenReady(); - if (!playing && playButton != null) { - playButton.requestFocus(); - } else if (playing && pauseButton != null) { - pauseButton.requestFocus(); - } - } - - private void setButtonEnabled(boolean enabled, View view) { - if (view == null) { - return; - } - view.setEnabled(enabled); - view.setAlpha(enabled ? 1f : 0.3f); - view.setVisibility(VISIBLE); - } - - private void previous() { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET - && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { - seekTo(previousWindowIndex, C.TIME_UNSET); - } else { - seekTo(0); - } - } - - private void next() { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - seekTo(nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { - seekTo(windowIndex, C.TIME_UNSET); - } - } - - private void rewind() { - if (rewindMs <= 0) { - return; - } - seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); - } - - private void fastForward() { - if (fastForwardMs <= 0) { - return; - } - long durationMs = player.getDuration(); - long seekPositionMs = player.getCurrentPosition() + fastForwardMs; - if (durationMs != C.TIME_UNSET) { - seekPositionMs = Math.min(seekPositionMs, durationMs); - } - seekTo(seekPositionMs); - } - - private void seekTo(long positionMs) { - seekTo(player.getCurrentWindowIndex(), positionMs); - } - - private void seekTo(int windowIndex, long positionMs) { - boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); - if (!dispatched) { - // The seek wasn't dispatched. If the progress bar was dragged by the user to perform the - // seek then it'll now be in the wrong position. Trigger a progress update to snap it back. - updateProgress(); - } - } - - private void seekToTimeBarPosition(long positionMs) { - int windowIndex; - Timeline timeline = player.getCurrentTimeline(); - if (multiWindowTimeBar && !timeline.isEmpty()) { - int windowCount = timeline.getWindowCount(); - windowIndex = 0; - while (true) { - long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); - if (positionMs < windowDurationMs) { - break; - } else if (windowIndex == windowCount - 1) { - // Seeking past the end of the last window should seek to the end of the timeline. - positionMs = windowDurationMs; - break; - } - positionMs -= windowDurationMs; - windowIndex++; - } - } else { - windowIndex = player.getCurrentWindowIndex(); - } - seekTo(windowIndex, positionMs); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - isAttachedToWindow = true; - if (hideAtMs != C.TIME_UNSET) { - long delayMs = hideAtMs - SystemClock.uptimeMillis(); - if (delayMs <= 0) { - hide(); - } else { - postDelayed(hideAction, delayMs); - } - } - updateAll(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - isAttachedToWindow = false; - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - if (player == null || !isHandledMediaKey(keyCode)) { - return false; - } - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - fastForward(); - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { - rewind(); - } else if (event.getRepeatCount() == 0) { - switch (keyCode) { - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - next(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(); - break; - default: - break; - } - } - } - return true; - } - - @SuppressLint("InlinedApi") - private static boolean isHandledMediaKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT - || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; - } - - /** - * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. - * - * @param timeline The {@link Timeline} to check. - * @param window A scratch {@link Timeline.Window} instance. - * @return Whether the specified timeline can be shown on a multi-window time bar. - */ - private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { - if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { - return false; - } - int windowCount = timeline.getWindowCount(); - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { - return false; - } - } - return true; - } - - private final class ComponentListener extends Player.DefaultEventListener - implements TimeBar.OnScrubListener, OnClickListener { - - @Override - public void onScrubStart(TimeBar timeBar, long position) { - removeCallbacks(hideAction); - scrubbing = true; - } - - @Override - public void onScrubMove(TimeBar timeBar, long position) { - if (positionView != null) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - } - - @Override - public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { - scrubbing = false; - if (!canceled && player != null) { - seekToTimeBarPosition(position); - } - hideAfterTimeout(); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - updatePlayPauseButton(); - updateProgress(); - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - updateRepeatModeButton(); - updateNavigation(); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - updateShuffleButton(); - updateNavigation(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - updateNavigation(); - updateProgress(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { - updateNavigation(); - updateTimeBarMode(); - updateProgress(); - } - - @Override - public void onClick(View view) { - if (player != null) { - if (nextButton == view) { - next(); - } else if (previousButton == view) { - previous(); - } else if (fastForwardButton == view) { - fastForward(); - } else if (rewindButton == view) { - rewind(); - } else if (playButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, true); - } else if (pauseButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, false); - } else if (repeatToggleButton == view) { - controlDispatcher.dispatchSetRepeatMode( - player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); - } else if (shuffleButton == view) { - controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); - } - } - hideAfterTimeout(); - } - } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java new file mode 100644 index 0000000000..20c3ef02dc --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -0,0 +1,1101 @@ +/* + * Copyright (C) 2016 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.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.Formatter; +import java.util.Locale; + +/** + * A view for controlling {@link Player} instances. + * + *

      A PlayerControlView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. + * + *

      Attributes

      + * + * The following attributes can be set on a PlayerControlView when used in a layout XML file: + * + *
        + *
      • {@code show_timeout} - The time between the last user interaction and the controls + * being automatically hidden, in milliseconds. Use zero if the controls should not + * automatically timeout. + *
          + *
        • Corresponding method: {@link #setShowTimeoutMs(int)} + *
        • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} + *
        + *
      • {@code rewind_increment} - The duration of the rewind applied when the user taps the + * rewind button, in milliseconds. Use zero to disable the rewind button. + *
          + *
        • Corresponding method: {@link #setRewindIncrementMs(int)} + *
        • Default: {@link #DEFAULT_REWIND_MS} + *
        + *
      • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. + *
          + *
        • Corresponding method: {@link #setFastForwardIncrementMs(int)} + *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} + *
        + *
      • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, + * or {@code one|all}. + *
          + *
        • Corresponding method: {@link #setRepeatToggleModes(int)} + *
        • Default: {@link PlayerControlView#DEFAULT_REPEAT_TOGGLE_MODES} + *
        + *
      • {@code show_shuffle_button} - Whether the shuffle button is shown. + *
          + *
        • Corresponding method: {@link #setShowShuffleButton(boolean)} + *
        • Default: false + *
        + *
      • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See + * below for more details. + *
          + *
        • Corresponding method: None + *
        • Default: {@code R.id.exo_player_control_view} + *
        + *
      + * + *

      Overriding the layout file

      + * + * To customize the layout of PlayerControlView throughout your app, or just for certain + * configurations, you can define {@code exo_player_control_view.xml} layout files in your + * application {@code res/layout*} directories. These layouts will override the one provided by the + * ExoPlayer library, and will be inflated for use by PlayerControlView. The view identifies and + * binds its children by looking for the following ids: + * + *

      + * + *

        + *
      • {@code exo_play} - The play button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_pause} - The pause button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_ffwd} - The fast forward button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_rew} - The rewind button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_prev} - The previous track button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_next} - The next track button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_repeat_toggle} - The repeat toggle button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_shuffle} - The shuffle button. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_position} - Text view displaying the current playback position. + *
          + *
        • Type: {@link TextView} + *
        + *
      • {@code exo_duration} - Text view displaying the current media duration. + *
          + *
        • Type: {@link TextView} + *
        + *
      • {@code exo_progress} - Time bar that's updated during playback and allows seeking. + *
          + *
        • Type: {@link TimeBar} + *
        + *
      + * + *

      All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

      Specifying a custom layout file

      + * + * Defining your own {@code exo_player_control_view.xml} is useful to customize the layout of + * PlayerControlView throughout your application. It's also possible to customize the layout for a + * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} + * attribute on a PlayerControlView. This will cause the specified layout to be inflated instead of + * {@code exo_player_control_view.xml} for only the instance on which the attribute is set. + */ +public class PlayerControlView extends FrameLayout { + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); + } + + /** Listener to be notified about changes of the visibility of the UI control. */ + public interface VisibilityListener { + + /** + * Called when the visibility changes. + * + * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. + */ + void onVisibilityChange(int visibility); + } + + /** The default fast forward increment, in milliseconds. */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** The default rewind increment, in milliseconds. */ + public static final int DEFAULT_REWIND_MS = 5000; + /** The default show timeout, in milliseconds. */ + public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + /** The default repeat toggle modes. */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + + /** The maximum number of windows that can be shown in a multi-window time bar. */ + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; + + private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; + + private final ComponentListener componentListener; + private final View previousButton; + private final View nextButton; + private final View playButton; + private final View pauseButton; + private final View fastForwardButton; + private final View rewindButton; + private final ImageView repeatToggleButton; + private final View shuffleButton; + private final TextView durationView; + private final TextView positionView; + private final TimeBar timeBar; + private final StringBuilder formatBuilder; + private final Formatter formatter; + private final Timeline.Period period; + private final Timeline.Window window; + + private final Drawable repeatOffButtonDrawable; + private final Drawable repeatOneButtonDrawable; + private final Drawable repeatAllButtonDrawable; + private final String repeatOffButtonContentDescription; + private final String repeatOneButtonContentDescription; + private final String repeatAllButtonContentDescription; + + private Player player; + private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; + private VisibilityListener visibilityListener; + + private boolean isAttachedToWindow; + private boolean showMultiWindowTimeBar; + private boolean multiWindowTimeBar; + private boolean scrubbing; + private int rewindMs; + private int fastForwardMs; + private int showTimeoutMs; + private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; + private boolean showShuffleButton; + private long hideAtMs; + private long[] adGroupTimesMs; + private boolean[] playedAdGroups; + private long[] extraAdGroupTimesMs; + private boolean[] extraPlayedAdGroups; + + private final Runnable updateProgressAction = + new Runnable() { + @Override + public void run() { + updateProgress(); + } + }; + + private final Runnable hideAction = + new Runnable() { + @Override + public void run() { + hide(); + } + }; + + public PlayerControlView(Context context) { + this(context, null); + } + + public PlayerControlView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, attrs); + } + + public PlayerControlView( + Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); + int controllerLayoutId = R.layout.exo_player_control_view; + rewindMs = DEFAULT_REWIND_MS; + fastForwardMs = DEFAULT_FAST_FORWARD_MS; + showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; + repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; + showShuffleButton = false; + if (playbackAttrs != null) { + TypedArray a = + context + .getTheme() + .obtainStyledAttributes(playbackAttrs, R.styleable.PlayerControlView, 0, 0); + try { + rewindMs = a.getInt(R.styleable.PlayerControlView_rewind_increment, rewindMs); + fastForwardMs = + a.getInt(R.styleable.PlayerControlView_fastforward_increment, fastForwardMs); + showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs); + controllerLayoutId = + a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId); + repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); + showShuffleButton = + a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton); + } finally { + a.recycle(); + } + } + period = new Timeline.Period(); + window = new Timeline.Window(); + formatBuilder = new StringBuilder(); + formatter = new Formatter(formatBuilder, Locale.getDefault()); + adGroupTimesMs = new long[0]; + playedAdGroups = new boolean[0]; + extraAdGroupTimesMs = new long[0]; + extraPlayedAdGroups = new boolean[0]; + componentListener = new ComponentListener(); + controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); + + LayoutInflater.from(context).inflate(controllerLayoutId, this); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + durationView = findViewById(R.id.exo_duration); + positionView = findViewById(R.id.exo_position); + timeBar = findViewById(R.id.exo_progress); + if (timeBar != null) { + timeBar.addListener(componentListener); + } + playButton = findViewById(R.id.exo_play); + if (playButton != null) { + playButton.setOnClickListener(componentListener); + } + pauseButton = findViewById(R.id.exo_pause); + if (pauseButton != null) { + pauseButton.setOnClickListener(componentListener); + } + previousButton = findViewById(R.id.exo_prev); + if (previousButton != null) { + previousButton.setOnClickListener(componentListener); + } + nextButton = findViewById(R.id.exo_next); + if (nextButton != null) { + nextButton.setOnClickListener(componentListener); + } + rewindButton = findViewById(R.id.exo_rew); + if (rewindButton != null) { + rewindButton.setOnClickListener(componentListener); + } + fastForwardButton = findViewById(R.id.exo_ffwd); + if (fastForwardButton != null) { + fastForwardButton.setOnClickListener(componentListener); + } + repeatToggleButton = findViewById(R.id.exo_repeat_toggle); + if (repeatToggleButton != null) { + repeatToggleButton.setOnClickListener(componentListener); + } + shuffleButton = findViewById(R.id.exo_shuffle); + if (shuffleButton != null) { + shuffleButton.setOnClickListener(componentListener); + } + Resources resources = context.getResources(); + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); + repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); + repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + repeatOffButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_all_description); + } + + @SuppressWarnings("ResourceType") + private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( + TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, repeatToggleModes); + } + + /** + * Returns the {@link Player} currently being controlled by this view, or null if no player is + * set. + */ + public Player getPlayer() { + return player; + } + + /** + * Sets the {@link Player} to control. + * + * @param player The {@link Player} to control. + */ + public void setPlayer(Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + } + this.player = player; + if (player != null) { + player.addListener(componentListener); + } + updateAll(); + } + + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. If the + * timeline has a period with unknown duration or more than {@link + * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single + * window. + * + * @param showMultiWindowTimeBar Whether the time bar should show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + this.showMultiWindowTimeBar = showMultiWindowTimeBar; + updateTimeBarMode(); + } + + /** + * Sets the millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers( + @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { + if (extraAdGroupTimesMs == null) { + this.extraAdGroupTimesMs = new long[0]; + this.extraPlayedAdGroups = new boolean[0]; + } else { + Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); + this.extraAdGroupTimesMs = extraAdGroupTimesMs; + this.extraPlayedAdGroups = extraPlayedAdGroups; + } + updateProgress(); + } + + /** + * Sets the {@link VisibilityListener}. + * + * @param listener The listener to be notified about visibility changes. + */ + public void setVisibilityListener(VisibilityListener listener) { + this.visibilityListener = listener; + } + + /** + * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. + * + * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null + * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. + */ + public void setControlDispatcher( + @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { + this.controlDispatcher = + controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() + : controlDispatcher; + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. + */ + public void setRewindIncrementMs(int rewindMs) { + this.rewindMs = rewindMs; + updateNavigation(); + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. + */ + public void setFastForwardIncrementMs(int fastForwardMs) { + this.fastForwardMs = fastForwardMs; + updateNavigation(); + } + + /** + * Returns the playback controls timeout. The playback controls are automatically hidden after + * this duration of time has elapsed without user input. + * + * @return The duration in milliseconds. A non-positive value indicates that the controls will + * remain visible indefinitely. + */ + public int getShowTimeoutMs() { + return showTimeoutMs; + } + + /** + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * duration of time has elapsed without user input. + * + * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls + * to remain visible indefinitely. + */ + public void setShowTimeoutMs(int showTimeoutMs) { + this.showTimeoutMs = showTimeoutMs; + if (isVisible()) { + // Reset the timeout. + hideAfterTimeout(); + } + } + + /** + * Returns which repeat toggle modes are enabled. + * + * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. + */ + public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { + return repeatToggleModes; + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + this.repeatToggleModes = repeatToggleModes; + if (player != null) { + @Player.RepeatMode int currentMode = player.getRepeatMode(); + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE + && currentMode != Player.REPEAT_MODE_OFF) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE + && currentMode == Player.REPEAT_MODE_ALL) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL + && currentMode == Player.REPEAT_MODE_ONE) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); + } + } + } + + /** Returns whether the shuffle button is shown. */ + public boolean getShowShuffleButton() { + return showShuffleButton; + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + this.showShuffleButton = showShuffleButton; + updateShuffleButton(); + } + + /** + * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will + * be automatically hidden after this duration of time has elapsed without user input. + */ + public void show() { + if (!isVisible()) { + setVisibility(VISIBLE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + updateAll(); + requestPlayPauseFocus(); + } + // Call hideAfterTimeout even if already visible to reset the timeout. + hideAfterTimeout(); + } + + /** Hides the controller. */ + public void hide() { + if (isVisible()) { + setVisibility(GONE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + hideAtMs = C.TIME_UNSET; + } + } + + /** Returns whether the controller is currently visible. */ + public boolean isVisible() { + return getVisibility() == VISIBLE; + } + + private void hideAfterTimeout() { + removeCallbacks(hideAction); + if (showTimeoutMs > 0) { + hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; + if (isAttachedToWindow) { + postDelayed(hideAction, showTimeoutMs); + } + } else { + hideAtMs = C.TIME_UNSET; + } + } + + private void updateAll() { + updatePlayPauseButton(); + updateNavigation(); + updateRepeatModeButton(); + updateShuffleButton(); + updateProgress(); + } + + private void updatePlayPauseButton() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + boolean requestPlayPauseFocus = false; + boolean playing = player != null && player.getPlayWhenReady(); + if (playButton != null) { + requestPlayPauseFocus |= playing && playButton.isFocused(); + playButton.setVisibility(playing ? View.GONE : View.VISIBLE); + } + if (pauseButton != null) { + requestPlayPauseFocus |= !playing && pauseButton.isFocused(); + pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); + } + if (requestPlayPauseFocus) { + requestPlayPauseFocus(); + } + } + + private void updateNavigation() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + Timeline timeline = player != null ? player.getCurrentTimeline() : null; + boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); + boolean isSeekable = false; + boolean enablePrevious = false; + boolean enableNext = false; + if (haveNonEmptyTimeline && !player.isPlayingAd()) { + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + isSeekable = window.isSeekable; + enablePrevious = + isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; + } + setButtonEnabled(enablePrevious, previousButton); + setButtonEnabled(enableNext, nextButton); + setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); + setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); + if (timeBar != null) { + timeBar.setEnabled(isSeekable); + } + } + + private void updateRepeatModeButton() { + if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { + return; + } + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { + repeatToggleButton.setVisibility(View.GONE); + return; + } + if (player == null) { + setButtonEnabled(false, repeatToggleButton); + return; + } + setButtonEnabled(true, repeatToggleButton); + switch (player.getRepeatMode()) { + case Player.REPEAT_MODE_OFF: + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); + break; + case Player.REPEAT_MODE_ONE: + repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); + repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); + break; + case Player.REPEAT_MODE_ALL: + repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); + repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); + break; + default: + // Never happens. + } + repeatToggleButton.setVisibility(View.VISIBLE); + } + + private void updateShuffleButton() { + if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { + return; + } + if (!showShuffleButton) { + shuffleButton.setVisibility(View.GONE); + } else if (player == null) { + setButtonEnabled(false, shuffleButton); + } else { + shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); + shuffleButton.setEnabled(true); + shuffleButton.setVisibility(View.VISIBLE); + } + } + + private void updateTimeBarMode() { + if (player == null) { + return; + } + multiWindowTimeBar = + showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); + } + + private void updateProgress() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + + long position = 0; + long bufferedPosition = 0; + long duration = 0; + if (player != null) { + long currentWindowTimeBarOffsetUs = 0; + long durationUs = 0; + int adGroupCount = 0; + Timeline timeline = player.getCurrentTimeline(); + if (!timeline.isEmpty()) { + int currentWindowIndex = player.getCurrentWindowIndex(); + int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; + int lastWindowIndex = + multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; + for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { + if (i == currentWindowIndex) { + currentWindowTimeBarOffsetUs = durationUs; + } + timeline.getWindow(i, window); + if (window.durationUs == C.TIME_UNSET) { + Assertions.checkState(!multiWindowTimeBar); + break; + } + for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { + timeline.getPeriod(j, period); + int periodAdGroupCount = period.getAdGroupCount(); + for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { + long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); + if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { + if (period.durationUs == C.TIME_UNSET) { + // Don't show ad markers for postrolls in periods with unknown duration. + continue; + } + adGroupTimeInPeriodUs = period.durationUs; + } + long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); + if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupCount == adGroupTimesMs.length) { + int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); + playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); + } + adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); + playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); + adGroupCount++; + } + } + } + durationUs += window.durationUs; + } + } + duration = C.usToMs(durationUs); + position = C.usToMs(currentWindowTimeBarOffsetUs); + bufferedPosition = position; + if (player.isPlayingAd()) { + position += player.getContentPosition(); + bufferedPosition = position; + } else { + position += player.getCurrentPosition(); + bufferedPosition += player.getBufferedPosition(); + } + if (timeBar != null) { + int extraAdGroupCount = extraAdGroupTimesMs.length; + int totalAdGroupCount = adGroupCount + extraAdGroupCount; + if (totalAdGroupCount > adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); + playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); + } + System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); + System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); + } + } + if (durationView != null) { + durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); + } + if (positionView != null && !scrubbing) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); + } + if (timeBar != null) { + timeBar.setPosition(position); + timeBar.setBufferedPosition(bufferedPosition); + timeBar.setDuration(duration); + } + + // Cancel any pending updates and schedule a new one if necessary. + removeCallbacks(updateProgressAction); + int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); + if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { + long delayMs; + if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { + float playbackSpeed = player.getPlaybackParameters().speed; + if (playbackSpeed <= 0.1f) { + delayMs = 1000; + } else if (playbackSpeed <= 5f) { + long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); + long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); + if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { + mediaTimeDelayMs += mediaTimeUpdatePeriodMs; + } + delayMs = + playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); + } else { + delayMs = 200; + } + } else { + delayMs = 1000; + } + postDelayed(updateProgressAction, delayMs); + } + } + + private void requestPlayPauseFocus() { + boolean playing = player != null && player.getPlayWhenReady(); + if (!playing && playButton != null) { + playButton.requestFocus(); + } else if (playing && pauseButton != null) { + pauseButton.requestFocus(); + } + } + + private void setButtonEnabled(boolean enabled, View view) { + if (view == null) { + return; + } + view.setEnabled(enabled); + view.setAlpha(enabled ? 1f : 0.3f); + view.setVisibility(VISIBLE); + } + + private void previous() { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + int previousWindowIndex = player.getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + seekTo(previousWindowIndex, C.TIME_UNSET); + } else { + seekTo(0); + } + } + + private void next() { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = player.getCurrentWindowIndex(); + int nextWindowIndex = player.getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + seekTo(nextWindowIndex, C.TIME_UNSET); + } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { + seekTo(windowIndex, C.TIME_UNSET); + } + } + + private void rewind() { + if (rewindMs <= 0) { + return; + } + seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); + } + + private void fastForward() { + if (fastForwardMs <= 0) { + return; + } + long durationMs = player.getDuration(); + long seekPositionMs = player.getCurrentPosition() + fastForwardMs; + if (durationMs != C.TIME_UNSET) { + seekPositionMs = Math.min(seekPositionMs, durationMs); + } + seekTo(seekPositionMs); + } + + private void seekTo(long positionMs) { + seekTo(player.getCurrentWindowIndex(), positionMs); + } + + private void seekTo(int windowIndex, long positionMs) { + boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); + if (!dispatched) { + // The seek wasn't dispatched. If the progress bar was dragged by the user to perform the + // seek then it'll now be in the wrong position. Trigger a progress update to snap it back. + updateProgress(); + } + } + + private void seekToTimeBarPosition(long positionMs) { + int windowIndex; + Timeline timeline = player.getCurrentTimeline(); + if (multiWindowTimeBar && !timeline.isEmpty()) { + int windowCount = timeline.getWindowCount(); + windowIndex = 0; + while (true) { + long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); + if (positionMs < windowDurationMs) { + break; + } else if (windowIndex == windowCount - 1) { + // Seeking past the end of the last window should seek to the end of the timeline. + positionMs = windowDurationMs; + break; + } + positionMs -= windowDurationMs; + windowIndex++; + } + } else { + windowIndex = player.getCurrentWindowIndex(); + } + seekTo(windowIndex, positionMs); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + isAttachedToWindow = true; + if (hideAtMs != C.TIME_UNSET) { + long delayMs = hideAtMs - SystemClock.uptimeMillis(); + if (delayMs <= 0) { + hide(); + } else { + postDelayed(hideAction, delayMs); + } + } + updateAll(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + isAttachedToWindow = false; + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (player == null || !isHandledMediaKey(keyCode)) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + fastForward(); + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + rewind(); + } else if (event.getRepeatCount() == 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + controlDispatcher.dispatchSetPlayWhenReady(player, true); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, false); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + next(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + previous(); + break; + default: + break; + } + } + } + return true; + } + + @SuppressLint("InlinedApi") + private static boolean isHandledMediaKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD + || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } + + /** + * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. + * + * @param timeline The {@link Timeline} to check. + * @param window A scratch {@link Timeline.Window} instance. + * @return Whether the specified timeline can be shown on a multi-window time bar. + */ + private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { + if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { + return false; + } + int windowCount = timeline.getWindowCount(); + for (int i = 0; i < windowCount; i++) { + if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { + return false; + } + } + return true; + } + + private final class ComponentListener extends Player.DefaultEventListener + implements TimeBar.OnScrubListener, OnClickListener { + + @Override + public void onScrubStart(TimeBar timeBar, long position) { + removeCallbacks(hideAction); + scrubbing = true; + } + + @Override + public void onScrubMove(TimeBar timeBar, long position) { + if (positionView != null) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); + } + } + + @Override + public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { + scrubbing = false; + if (!canceled && player != null) { + seekToTimeBarPosition(position); + } + hideAfterTimeout(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updatePlayPauseButton(); + updateProgress(); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + updateRepeatModeButton(); + updateNavigation(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + updateShuffleButton(); + updateNavigation(); + } + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + updateNavigation(); + updateProgress(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + updateNavigation(); + updateTimeBarMode(); + updateProgress(); + } + + @Override + public void onClick(View view) { + if (player != null) { + if (nextButton == view) { + next(); + } else if (previousButton == view) { + previous(); + } else if (fastForwardButton == view) { + fastForward(); + } else if (rewindButton == view) { + rewind(); + } else if (playButton == view) { + controlDispatcher.dispatchSetPlayWhenReady(player, true); + } else if (pauseButton == view) { + controlDispatcher.dispatchSetPlayWhenReady(player, false); + } else if (repeatToggleButton == view) { + controlDispatcher.dispatchSetRepeatMode( + player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); + } else if (shuffleButton == view) { + controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); + } + } + hideAfterTimeout(); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java new file mode 100644 index 0000000000..66c197ecb7 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -0,0 +1,1057 @@ +/* + * Copyright (C) 2016 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.ui; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoListener; +import java.util.List; + +/** + * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art + * during playback, and displays playback controls using a {@link PlayerControlView}. + * + *

      A PlayerView can be customized by setting attributes (or calling corresponding methods), + * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

      Attributes

      + * + * The following attributes can be set on a PlayerView when used in a layout XML file: + * + *
        + *
      • {@code use_artwork} - Whether artwork is used if available in audio streams. + *
          + *
        • Corresponding method: {@link #setUseArtwork(boolean)} + *
        • Default: {@code true} + *
        + *
      • {@code default_artwork} - Default artwork to use if no artwork available in audio + * streams. + *
          + *
        • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
        • Default: {@code null} + *
        + *
      • {@code use_controller} - Whether the playback controls can be shown. + *
          + *
        • Corresponding method: {@link #setUseController(boolean)} + *
        • Default: {@code true} + *
        + *
      • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. + *
          + *
        • Corresponding method: {@link #setControllerHideOnTouch(boolean)} + *
        • Default: {@code true} + *
        + *
      • {@code auto_show} - Whether the playback controls are automatically shown when + * playback starts, pauses, ends, or fails. If set to false, the playback controls can be + * manually operated with {@link #showController()} and {@link #hideController()}. + *
          + *
        • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
        • Default: {@code true} + *
        + *
      • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
          + *
        • Corresponding method: {@link #setControllerHideDuringAds(boolean)} + *
        • Default: {@code true} + *
        + *
      • {@code resize_mode} - Controls how video and album art is resized within the view. + * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. + *
          + *
        • Corresponding method: {@link #setResizeMode(int)} + *
        • Default: {@code fit} + *
        + *
      • {@code surface_type} - The type of surface view used for video playbacks. Valid + * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} + * is recommended for audio only applications, since creating the surface can be expensive. + * Using {@code surface_view} is recommended for video applications. + *
          + *
        • Corresponding method: None + *
        • Default: {@code surface_view} + *
        + *
      • {@code shutter_background_color} - The background color of the {@code exo_shutter} + * view. + *
          + *
        • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
        • Default: {@code unset} + *
        + *
      • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below + * for more details. + *
          + *
        • Corresponding method: None + *
        • Default: {@code R.id.exo_player_view} + *
        + *
      • {@code controller_layout_id} - Specifies the id of the layout resource to be + * inflated by the child {@link PlayerControlView}. See below for more details. + *
          + *
        • Corresponding method: None + *
        • Default: {@code R.id.exo_player_control_view} + *
        + *
      • All attributes that can be set on a {@link PlayerControlView} can also be set on a + * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the + * layout is overridden to specify a custom {@code exo_controller} (see below). + *
      + * + *

      Overriding the layout file

      + * + * To customize the layout of PlayerView throughout your app, or just for certain configurations, + * you can define {@code exo_player_view.xml} layout files in your application {@code res/layout*} + * directories. These layouts will override the one provided by the ExoPlayer library, and will be + * inflated for use by PlayerView. The view identifies and binds its children by looking for the + * following ids: + * + *

      + * + *

        + *
      • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video + * or album art of the media being played, and the configured {@code resize_mode}. The video + * surface view is inflated into this frame as its first child. + *
          + *
        • Type: {@link AspectRatioFrameLayout} + *
        + *
      • {@code exo_shutter} - A view that's made visible when video should be hidden. This + * view is typically an opaque view that covers the video surface view, thereby obscuring it + * when visible. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_subtitles} - Displays subtitles. + *
          + *
        • Type: {@link SubtitleView} + *
        + *
      • {@code exo_artwork} - Displays album art. + *
          + *
        • Type: {@link ImageView} + *
        + *
      • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated + * {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists. + *
          + *
        • Type: {@link View} + *
        + *
      • {@code exo_controller} - An already inflated {@link PlayerControlView}. Allows use + * of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. + *
          + *
        • Type: {@link PlayerControlView} + *
        + *
      • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which + * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. + *
          + *
        • Type: {@link FrameLayout} + *
        + *
      + * + *

      All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

      Specifying a custom layout file

      + * + * Defining your own {@code exo_player_view.xml} is useful to customize the layout of PlayerView + * throughout your application. It's also possible to customize the layout for a single instance in + * a layout file. This is achieved by setting the {@code player_layout_id} attribute on a + * PlayerView. This will cause the specified layout to be inflated instead of {@code + * exo_player_view.xml} for only the instance on which the attribute is set. + */ +public class PlayerView extends FrameLayout { + + private static final int SURFACE_TYPE_NONE = 0; + private static final int SURFACE_TYPE_SURFACE_VIEW = 1; + private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; + + private final AspectRatioFrameLayout contentFrame; + private final View shutterView; + private final View surfaceView; + private final ImageView artworkView; + private final SubtitleView subtitleView; + private final PlayerControlView controller; + private final ComponentListener componentListener; + private final FrameLayout overlayFrameLayout; + + private Player player; + private boolean useController; + private boolean useArtwork; + private Bitmap defaultArtwork; + private int controllerShowTimeoutMs; + private boolean controllerAutoShow; + private boolean controllerHideDuringAds; + private boolean controllerHideOnTouch; + private int textureViewRotation; + + public PlayerView(Context context) { + this(context, null); + } + + public PlayerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + if (isInEditMode()) { + contentFrame = null; + shutterView = null; + surfaceView = null; + artworkView = null; + subtitleView = null; + controller = null; + componentListener = null; + overlayFrameLayout = null; + ImageView logo = new ImageView(context); + if (Util.SDK_INT >= 23) { + configureEditModeLogoV23(getResources(), logo); + } else { + configureEditModeLogo(getResources(), logo); + } + addView(logo); + return; + } + + boolean shutterColorSet = false; + int shutterColor = 0; + int playerLayoutId = R.layout.exo_player_view; + boolean useArtwork = true; + int defaultArtworkId = 0; + boolean useController = true; + int surfaceType = SURFACE_TYPE_SURFACE_VIEW; + int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; + boolean controllerHideOnTouch = true; + boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); + try { + shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color); + shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor); + playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId); + useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork); + defaultArtworkId = + a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId); + useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController); + surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType); + resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode); + controllerShowTimeoutMs = + a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs); + controllerHideOnTouch = + a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); + } finally { + a.recycle(); + } + } + + LayoutInflater.from(context).inflate(playerLayoutId, this); + componentListener = new ComponentListener(); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + // Content frame. + contentFrame = findViewById(R.id.exo_content_frame); + if (contentFrame != null) { + setResizeModeRaw(contentFrame, resizeMode); + } + + // Shutter view. + shutterView = findViewById(R.id.exo_shutter); + if (shutterView != null && shutterColorSet) { + shutterView.setBackgroundColor(shutterColor); + } + + // Create a surface view and insert it into the content frame, if there is one. + if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = + surfaceType == SURFACE_TYPE_TEXTURE_VIEW + ? new TextureView(context) + : new SurfaceView(context); + surfaceView.setLayoutParams(params); + contentFrame.addView(surfaceView, 0); + } else { + surfaceView = null; + } + + // Overlay frame layout. + overlayFrameLayout = findViewById(R.id.exo_overlay); + + // Artwork view. + artworkView = findViewById(R.id.exo_artwork); + this.useArtwork = useArtwork && artworkView != null; + if (defaultArtworkId != 0) { + defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); + } + + // Subtitle view. + subtitleView = findViewById(R.id.exo_subtitles); + if (subtitleView != null) { + subtitleView.setUserDefaultStyle(); + subtitleView.setUserDefaultTextSize(); + } + + // Playback control view. + PlayerControlView customController = findViewById(R.id.exo_controller); + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { + // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); + } else { + this.controller = null; + } + this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; + this.controllerHideOnTouch = controllerHideOnTouch; + this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; + this.useController = useController && controller != null; + hideController(); + } + + /** + * Switches the view targeted by a given {@link Player}. + * + * @param player The player whose target view is being switched. + * @param oldPlayerView The old view to detach from the player. + * @param newPlayerView The new view to attach to the player. + */ + public static void switchTargetView( + @NonNull Player player, + @Nullable PlayerView oldPlayerView, + @Nullable PlayerView newPlayerView) { + if (oldPlayerView == newPlayerView) { + return; + } + // We attach the new view before detaching the old one because this ordering allows the player + // to swap directly from one surface to another, without transitioning through a state where no + // surface is attached. This is significantly more efficient and achieves a more seamless + // transition when using platform provided video decoders. + if (newPlayerView != null) { + newPlayerView.setPlayer(player); + } + if (oldPlayerView != null) { + oldPlayerView.setPlayer(null); + } + } + + /** Returns the player currently set on this view, or null if no player is set. */ + public Player getPlayer() { + return player; + } + + /** + * Set the {@link Player} to use. + * + *

      To transition a {@link Player} from targeting one view to another, it's recommended to use + * {@link #switchTargetView(Player, PlayerView, PlayerView)} rather than this method. If you do + * wish to use this method directly, be sure to attach the player to the new view before + * calling {@code setPlayer(null)} to detach it from the old one. This ordering is significantly + * more efficient and may allow for more seamless transitions. + * + * @param player The {@link Player} to use. + */ + public void setPlayer(Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + Player.VideoComponent oldVideoComponent = this.player.getVideoComponent(); + if (oldVideoComponent != null) { + oldVideoComponent.removeVideoListener(componentListener); + if (surfaceView instanceof TextureView) { + oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); + } + } + Player.TextComponent oldTextComponent = this.player.getTextComponent(); + if (oldTextComponent != null) { + oldTextComponent.removeTextOutput(componentListener); + } + } + this.player = player; + if (useController) { + controller.setPlayer(player); + } + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + if (subtitleView != null) { + subtitleView.setCues(null); + } + if (player != null) { + Player.VideoComponent newVideoComponent = player.getVideoComponent(); + if (newVideoComponent != null) { + if (surfaceView instanceof TextureView) { + newVideoComponent.setVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); + } + newVideoComponent.addVideoListener(componentListener); + } + Player.TextComponent newTextComponent = player.getTextComponent(); + if (newTextComponent != null) { + newTextComponent.addTextOutput(componentListener); + } + player.addListener(componentListener); + maybeShowController(false); + updateForCurrentTrackSelections(); + } else { + hideController(); + hideArtwork(); + } + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160. + surfaceView.setVisibility(visibility); + } + } + + /** + * Sets the resize mode. + * + * @param resizeMode The resize mode. + */ + public void setResizeMode(@ResizeMode int resizeMode) { + Assertions.checkState(contentFrame != null); + contentFrame.setResizeMode(resizeMode); + } + + /** Returns whether artwork is displayed if present in the media. */ + public boolean getUseArtwork() { + return useArtwork; + } + + /** + * Sets whether artwork is displayed if present in the media. + * + * @param useArtwork Whether artwork is displayed. + */ + public void setUseArtwork(boolean useArtwork) { + Assertions.checkState(!useArtwork || artworkView != null); + if (this.useArtwork != useArtwork) { + this.useArtwork = useArtwork; + updateForCurrentTrackSelections(); + } + } + + /** Returns the default artwork to display. */ + public Bitmap getDefaultArtwork() { + return defaultArtwork; + } + + /** + * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is + * present in the media. + * + * @param defaultArtwork the default artwork to display. + */ + public void setDefaultArtwork(Bitmap defaultArtwork) { + if (this.defaultArtwork != defaultArtwork) { + this.defaultArtwork = defaultArtwork; + updateForCurrentTrackSelections(); + } + } + + /** Returns whether the playback controls can be shown. */ + public boolean getUseController() { + return useController; + } + + /** + * Sets whether the playback controls can be shown. If set to {@code false} the playback controls + * are never visible and are disconnected from the player. + * + * @param useController Whether the playback controls can be shown. + */ + public void setUseController(boolean useController) { + Assertions.checkState(!useController || controller != null); + if (this.useController == useController) { + return; + } + this.useController = useController; + if (useController) { + controller.setPlayer(player); + } else if (controller != null) { + controller.hide(); + controller.setPlayer(null); + } + } + + /** + * Sets the background color of the {@code exo_shutter} view. + * + * @param color The background color. + */ + public void setShutterBackgroundColor(int color) { + if (shutterView != null) { + shutterView.setBackgroundColor(color); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (player != null && player.isPlayingAd()) { + // Focus any overlay UI now, in case it's provided by a WebView whose contents may update + // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using + // IMA [Internal: b/62371030]. + overlayFrameLayout.requestFocus(); + return super.dispatchKeyEvent(event); + } + boolean isDpadWhenControlHidden = + isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); + maybeShowController(true); + return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. Does nothing if playback controls are disabled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + return useController && controller.dispatchMediaKeyEvent(event); + } + + /** + * Shows the playback controls. Does nothing if playback controls are disabled. + * + *

      The playback controls are automatically hidden during playback after {{@link + * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, + * is paused, has ended or failed. + */ + public void showController() { + showController(shouldShowControllerIndefinitely()); + } + + /** Hides the playback controls. Does nothing if playback controls are disabled. */ + public void hideController() { + if (controller != null) { + controller.hide(); + } + } + + /** + * Returns the playback controls timeout. The playback controls are automatically hidden after + * this duration of time has elapsed without user input and with playback or buffering in + * progress. + * + * @return The timeout in milliseconds. A non-positive value will cause the controller to remain + * visible indefinitely. + */ + public int getControllerShowTimeoutMs() { + return controllerShowTimeoutMs; + } + + /** + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * duration of time has elapsed without user input and with playback or buffering in progress. + * + * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the + * controller to remain visible indefinitely. + */ + public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { + Assertions.checkState(controller != null); + this.controllerShowTimeoutMs = controllerShowTimeoutMs; + if (controller.isVisible()) { + // Update the controller's timeout if necessary. + showController(); + } + } + + /** Returns whether the playback controls are hidden by touch events. */ + public boolean getControllerHideOnTouch() { + return controllerHideOnTouch; + } + + /** + * Sets whether the playback controls are hidden by touch events. + * + * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. + */ + public void setControllerHideOnTouch(boolean controllerHideOnTouch) { + Assertions.checkState(controller != null); + this.controllerHideOnTouch = controllerHideOnTouch; + } + + /** + * Returns whether the playback controls are automatically shown when playback starts, pauses, + * ends, or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + */ + public boolean getControllerAutoShow() { + return controllerAutoShow; + } + + /** + * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, + * or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + * + * @param controllerAutoShow Whether the playback controls are allowed to show automatically. + */ + public void setControllerAutoShow(boolean controllerAutoShow) { + this.controllerAutoShow = controllerAutoShow; + } + + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + + /** + * Set the {@link PlayerControlView.VisibilityListener}. + * + * @param listener The listener to be notified about visibility changes. + */ + public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) { + Assertions.checkState(controller != null); + controller.setVisibilityListener(listener); + } + + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link + * DefaultControlDispatcher}. + */ + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + Assertions.checkState(controller != null); + controller.setControlDispatcher(controlDispatcher); + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. + */ + public void setRewindIncrementMs(int rewindMs) { + Assertions.checkState(controller != null); + controller.setRewindIncrementMs(rewindMs); + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. + */ + public void setFastForwardIncrementMs(int fastForwardMs) { + Assertions.checkState(controller != null); + controller.setFastForwardIncrementMs(fastForwardMs); + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + Assertions.checkState(controller != null); + controller.setRepeatToggleModes(repeatToggleModes); + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + Assertions.checkState(controller != null); + controller.setShowShuffleButton(showShuffleButton); + } + + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. + * + * @param showMultiWindowTimeBar Whether to show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + Assertions.checkState(controller != null); + controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); + } + + /** + * Gets the view onto which video is rendered. This is a: + * + *

        + *
      • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code + * surface_view}. + *
      • {@link TextureView} if {@code surface_type} is {@code texture_view}. + *
      • {@code null} if {@code surface_type} is {@code none}. + *
      + * + * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. + */ + public View getVideoSurfaceView() { + return surfaceView; + } + + /** + * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of + * the player. + * + * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and + * the overlay is not present. + */ + public FrameLayout getOverlayFrameLayout() { + return overlayFrameLayout; + } + + /** + * Gets the {@link SubtitleView}. + * + * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the + * subtitle view is not present. + */ + public SubtitleView getSubtitleView() { + return subtitleView; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + if (!controller.isVisible()) { + maybeShowController(true); + } else if (controllerHideOnTouch) { + controller.hide(); + } + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + if (!useController || player == null) { + return false; + } + maybeShowController(true); + return true; + } + + /** Shows the playback controls, but only if forced or shown indefinitely. */ + private void maybeShowController(boolean isForced) { + if (isPlayingAd() && controllerHideDuringAds) { + return; + } + if (useController) { + boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; + boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); + if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { + showController(shouldShowIndefinitely); + } + } + } + + private boolean shouldShowControllerIndefinitely() { + if (player == null) { + return true; + } + int playbackState = player.getPlaybackState(); + return controllerAutoShow + && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED + || !player.getPlayWhenReady()); + } + + private void showController(boolean showIndefinitely) { + if (!useController) { + return; + } + controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); + controller.show(); + } + + private boolean isPlayingAd() { + return player != null && player.isPlayingAd() && player.getPlayWhenReady(); + } + + private void updateForCurrentTrackSelections() { + if (player == null) { + return; + } + TrackSelectionArray selections = player.getCurrentTrackSelections(); + for (int i = 0; i < selections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; + } + } + // Video disabled so the shutter must be closed. + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + // Display artwork if enabled and available, else hide it. + if (useArtwork) { + for (int i = 0; i < selections.length; i++) { + TrackSelection selection = selections.get(i); + if (selection != null) { + for (int j = 0; j < selection.length(); j++) { + Metadata metadata = selection.getFormat(j).metadata; + if (metadata != null && setArtworkFromMetadata(metadata)) { + return; + } + } + } + } + if (setArtworkFromBitmap(defaultArtwork)) { + return; + } + } + // Artwork disabled or unavailable. + hideArtwork(); + } + + private boolean setArtworkFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof ApicFrame) { + byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); + return setArtworkFromBitmap(bitmap); + } + } + return false; + } + + private boolean setArtworkFromBitmap(Bitmap bitmap) { + if (bitmap != null) { + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + if (bitmapWidth > 0 && bitmapHeight > 0) { + if (contentFrame != null) { + contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); + } + artworkView.setImageBitmap(bitmap); + artworkView.setVisibility(VISIBLE); + return true; + } + } + return false; + } + + private void hideArtwork() { + if (artworkView != null) { + artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. + artworkView.setVisibility(INVISIBLE); + } + } + + @TargetApi(23) + private static void configureEditModeLogoV23(Resources resources, ImageView logo) { + logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); + logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); + } + + @SuppressWarnings("deprecation") + private static void configureEditModeLogo(Resources resources, ImageView logo) { + logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); + logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); + } + + @SuppressWarnings("ResourceType") + private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { + aspectRatioFrame.setResizeMode(resizeMode); + } + + /** Applies a texture rotation to a {@link TextureView}. */ + private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + float textureViewWidth = textureView.getWidth(); + float textureViewHeight = textureView.getHeight(); + if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { + textureView.setTransform(null); + } else { + Matrix transformMatrix = new Matrix(); + float pivotX = textureViewWidth / 2; + float pivotY = textureViewHeight / 2; + transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); + + // After rotation, scale the rotated texture to fit the TextureView size. + RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); + RectF rotatedTextureRect = new RectF(); + transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); + transformMatrix.postScale( + textureViewWidth / rotatedTextureRect.width(), + textureViewHeight / rotatedTextureRect.height(), + pivotX, + pivotY); + textureView.setTransform(transformMatrix); + } + } + + @SuppressLint("InlinedApi") + private boolean isDpadKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; + } + + private final class ComponentListener extends Player.DefaultEventListener + implements TextOutput, VideoListener, OnLayoutChangeListener { + + // TextOutput implementation + + @Override + public void onCues(List cues) { + if (subtitleView != null) { + subtitleView.onCues(cues); + } + } + + // VideoListener implementation + + @Override + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (contentFrame == null) { + return; + } + float videoAspectRatio = + (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; + + if (surfaceView instanceof TextureView) { + // Try to apply rotation transformation when our surface is a TextureView. + if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { + // We will apply a rotation 90/270 degree to the output texture of the TextureView. + // In this case, the output video's width and height will be swapped. + videoAspectRatio = 1 / videoAspectRatio; + } + if (textureViewRotation != 0) { + surfaceView.removeOnLayoutChangeListener(this); + } + textureViewRotation = unappliedRotationDegrees; + if (textureViewRotation != 0) { + // The texture view's dimensions might be changed after layout step. + // So add an OnLayoutChangeListener to apply rotation after layout step. + surfaceView.addOnLayoutChangeListener(this); + } + applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); + } + + contentFrame.setAspectRatio(videoAspectRatio); + } + + @Override + public void onRenderedFirstFrame() { + if (shutterView != null) { + shutterView.setVisibility(INVISIBLE); + } + } + + @Override + public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { + updateForCurrentTrackSelections(); + } + + // Player.EventListener implementation + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } else { + maybeShowController(false); + } + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } + } + + // OnLayoutChangeListener implementation + + @Override + public void onLayoutChange( + View view, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + applyTextureViewRotation((TextureView) view, textureViewRotation); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 6e69a31fd9..b8098b6fa7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -15,360 +15,28 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Matrix; -import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ControlDispatcher; -import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.ApicFrame; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextOutput; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.RepeatModeUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.List; -/** - * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and - * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - * - *

      A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. - * - *

      Attributes

      - * - * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: - * - *

      - * - *

        - *
      • {@code use_artwork} - Whether artwork is used if available in audio streams. - *
          - *
        • Corresponding method: {@link #setUseArtwork(boolean)} - *
        • Default: {@code true} - *
        - *
      • {@code default_artwork} - Default artwork to use if no artwork available in audio - * streams. - *
          - *
        • Corresponding method: {@link #setDefaultArtwork(Bitmap)} - *
        • Default: {@code null} - *
        - *
      • {@code use_controller} - Whether the playback controls can be shown. - *
          - *
        • Corresponding method: {@link #setUseController(boolean)} - *
        • Default: {@code true} - *
        - *
      • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. - *
          - *
        • Corresponding method: {@link #setControllerHideOnTouch(boolean)} - *
        • Default: {@code true} - *
        - *
      • {@code auto_show} - Whether the playback controls are automatically shown when - * playback starts, pauses, ends, or fails. If set to false, the playback controls can be - * manually operated with {@link #showController()} and {@link #hideController()}. - *
          - *
        • Corresponding method: {@link #setControllerAutoShow(boolean)} - *
        • Default: {@code true} - *
        - *
      • {@code hide_during_ads} - Whether the playback controls are hidden during ads. - * Controls are always shown during ads if they are enabled and the player is paused. - *
          - *
        • Corresponding method: {@link #setControllerHideDuringAds(boolean)} - *
        • Default: {@code true} - *
        - *
      • {@code resize_mode} - Controls how video and album art is resized within the view. - * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. - *
          - *
        • Corresponding method: {@link #setResizeMode(int)} - *
        • Default: {@code fit} - *
        - *
      • {@code surface_type} - The type of surface view used for video playbacks. Valid - * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} - * is recommended for audio only applications, since creating the surface can be expensive. - * Using {@code surface_view} is recommended for video applications. - *
          - *
        • Corresponding method: None - *
        • Default: {@code surface_view} - *
        - *
      • {@code shutter_background_color} - The background color of the {@code exo_shutter} - * view. - *
          - *
        • Corresponding method: {@link #setShutterBackgroundColor(int)} - *
        • Default: {@code unset} - *
        - *
      • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below - * for more details. - *
          - *
        • Corresponding method: None - *
        • Default: {@code R.id.exo_simple_player_view} - *
        - *
      • {@code controller_layout_id} - Specifies the id of the layout resource to be - * inflated by the child {@link PlaybackControlView}. See below for more details. - *
          - *
        • Corresponding method: None - *
        • Default: {@code R.id.exo_playback_control_view} - *
        - *
      • All attributes that can be set on a {@link PlaybackControlView} can also be set on a - * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} - * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
      - * - *

      Overriding the layout file

      - * - * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain - * configurations, you can define {@code exo_simple_player_view.xml} layout files in your - * application {@code res/layout*} directories. These layouts will override the one provided by the - * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and - * binds its children by looking for the following ids: - * - *

      - * - *

        - *
      • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video - * or album art of the media being played, and the configured {@code resize_mode}. The video - * surface view is inflated into this frame as its first child. - *
          - *
        • Type: {@link AspectRatioFrameLayout} - *
        - *
      • {@code exo_shutter} - A view that's made visible when video should be hidden. This - * view is typically an opaque view that covers the video surface view, thereby obscuring it - * when visible. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_subtitles} - Displays subtitles. - *
          - *
        • Type: {@link SubtitleView} - *
        - *
      • {@code exo_artwork} - Displays album art. - *
          - *
        • Type: {@link ImageView} - *
        - *
      • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated - * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. - *
          - *
        • Type: {@link View} - *
        - *
      • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code - * rewind_increment} will not be automatically propagated through to this instance. If a view - * exists with this id, any {@code exo_controller_placeholder} view will be ignored. - *
          - *
        • Type: {@link PlaybackControlView} - *
        - *
      • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which - * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. - *
          - *
        • Type: {@link FrameLayout} - *
        - *
      - * - *

      All child views are optional and so can be omitted if not required, however where defined they - * must be of the expected type. - * - *

      Specifying a custom layout file

      - * - * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of - * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a - * single instance in a layout file. This is achieved by setting the {@code player_layout_id} - * attribute on a SimpleExoPlayerView. This will cause the specified layout to be inflated instead - * of {@code exo_simple_player_view.xml} for only the instance on which the attribute is set. - */ +/** @deprecated Use {@link PlayerView}. */ +@Deprecated @TargetApi(16) -public final class SimpleExoPlayerView extends FrameLayout { - - private static final int SURFACE_TYPE_NONE = 0; - private static final int SURFACE_TYPE_SURFACE_VIEW = 1; - private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; - - private final AspectRatioFrameLayout contentFrame; - private final View shutterView; - private final View surfaceView; - private final ImageView artworkView; - private final SubtitleView subtitleView; - private final PlaybackControlView controller; - private final ComponentListener componentListener; - private final FrameLayout overlayFrameLayout; - - private SimpleExoPlayer player; - private boolean useController; - private boolean useArtwork; - private Bitmap defaultArtwork; - private int controllerShowTimeoutMs; - private boolean controllerAutoShow; - private boolean controllerHideDuringAds; - private boolean controllerHideOnTouch; - private int textureViewRotation; +public final class SimpleExoPlayerView extends PlayerView { public SimpleExoPlayerView(Context context) { - this(context, null); + super(context); } public SimpleExoPlayerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); } public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - - if (isInEditMode()) { - contentFrame = null; - shutterView = null; - surfaceView = null; - artworkView = null; - subtitleView = null; - controller = null; - componentListener = null; - overlayFrameLayout = null; - ImageView logo = new ImageView(context); - if (Util.SDK_INT >= 23) { - configureEditModeLogoV23(getResources(), logo); - } else { - configureEditModeLogo(getResources(), logo); - } - addView(logo); - return; - } - - boolean shutterColorSet = false; - int shutterColor = 0; - int playerLayoutId = R.layout.exo_simple_player_view; - boolean useArtwork = true; - int defaultArtworkId = 0; - boolean useController = true; - int surfaceType = SURFACE_TYPE_SURFACE_VIEW; - int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; - boolean controllerHideOnTouch = true; - boolean controllerAutoShow = true; - boolean controllerHideDuringAds = true; - if (attrs != null) { - TypedArray a = - context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); - try { - shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); - shutterColor = - a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); - playerLayoutId = - a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); - useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); - defaultArtworkId = - a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, defaultArtworkId); - useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); - surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); - resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); - controllerShowTimeoutMs = - a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, controllerShowTimeoutMs); - controllerHideOnTouch = - a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, controllerHideOnTouch); - controllerAutoShow = - a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); - controllerHideDuringAds = - a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); - } finally { - a.recycle(); - } - } - - LayoutInflater.from(context).inflate(playerLayoutId, this); - componentListener = new ComponentListener(); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - // Content frame. - contentFrame = findViewById(R.id.exo_content_frame); - if (contentFrame != null) { - setResizeModeRaw(contentFrame, resizeMode); - } - - // Shutter view. - shutterView = findViewById(R.id.exo_shutter); - if (shutterView != null && shutterColorSet) { - shutterView.setBackgroundColor(shutterColor); - } - - // Create a surface view and insert it into the content frame, if there is one. - if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { - ViewGroup.LayoutParams params = - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = - surfaceType == SURFACE_TYPE_TEXTURE_VIEW - ? new TextureView(context) - : new SurfaceView(context); - surfaceView.setLayoutParams(params); - contentFrame.addView(surfaceView, 0); - } else { - surfaceView = null; - } - - // Overlay frame layout. - overlayFrameLayout = findViewById(R.id.exo_overlay); - - // Artwork view. - artworkView = findViewById(R.id.exo_artwork); - this.useArtwork = useArtwork && artworkView != null; - if (defaultArtworkId != 0) { - defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); - } - - // Subtitle view. - subtitleView = findViewById(R.id.exo_subtitles); - if (subtitleView != null) { - subtitleView.setUserDefaultStyle(); - subtitleView.setUserDefaultTextSize(); - } - - // Playback control view. - PlaybackControlView customController = findViewById(R.id.exo_controller); - View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (customController != null) { - this.controller = customController; - } else if (controllerPlaceholder != null) { - // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are - // transferred, but standard FrameLayout attributes (e.g. background) are not. - this.controller = new PlaybackControlView(context, null, 0, attrs); - controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); - ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); - int controllerIndex = parent.indexOfChild(controllerPlaceholder); - parent.removeView(controllerPlaceholder); - parent.addView(controller, controllerIndex); - } else { - this.controller = null; - } - this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; - this.controllerHideOnTouch = controllerHideOnTouch; - this.controllerAutoShow = controllerAutoShow; - this.controllerHideDuringAds = controllerHideDuringAds; - this.useController = useController && controller != null; - hideController(); } /** @@ -382,674 +50,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @NonNull SimpleExoPlayer player, @Nullable SimpleExoPlayerView oldPlayerView, @Nullable SimpleExoPlayerView newPlayerView) { - if (oldPlayerView == newPlayerView) { - return; - } - // We attach the new view before detaching the old one because this ordering allows the player - // to swap directly from one surface to another, without transitioning through a state where no - // surface is attached. This is significantly more efficient and achieves a more seamless - // transition when using platform provided video decoders. - if (newPlayerView != null) { - newPlayerView.setPlayer(player); - } - if (oldPlayerView != null) { - oldPlayerView.setPlayer(null); - } + PlayerView.switchTargetView(player, oldPlayerView, newPlayerView); } - /** Returns the player currently set on this view, or null if no player is set. */ - public SimpleExoPlayer getPlayer() { - return player; - } - - /** - * Set the {@link SimpleExoPlayer} to use. - * - *

      To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended - * to use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} - * rather than this method. If you do wish to use this method directly, be sure to attach the - * player to the new view before calling {@code setPlayer(null)} to detach it from the - * old one. This ordering is significantly more efficient and may allow for more seamless - * transitions. - * - * @param player The {@link SimpleExoPlayer} to use. - */ - public void setPlayer(SimpleExoPlayer player) { - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - this.player.removeTextOutput(componentListener); - this.player.removeVideoListener(componentListener); - if (surfaceView instanceof TextureView) { - this.player.clearVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SurfaceView) { - this.player.clearVideoSurfaceView((SurfaceView) surfaceView); - } - } - this.player = player; - if (useController) { - controller.setPlayer(player); - } - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } - if (subtitleView != null) { - subtitleView.setCues(null); - } - if (player != null) { - if (surfaceView instanceof TextureView) { - player.setVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SurfaceView) { - player.setVideoSurfaceView((SurfaceView) surfaceView); - } - player.addVideoListener(componentListener); - player.addTextOutput(componentListener); - player.addListener(componentListener); - maybeShowController(false); - updateForCurrentTrackSelections(); - } else { - hideController(); - hideArtwork(); - } - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - if (surfaceView instanceof SurfaceView) { - // Work around https://github.com/google/ExoPlayer/issues/3160. - surfaceView.setVisibility(visibility); - } - } - - /** - * Sets the resize mode. - * - * @param resizeMode The resize mode. - */ - public void setResizeMode(@ResizeMode int resizeMode) { - Assertions.checkState(contentFrame != null); - contentFrame.setResizeMode(resizeMode); - } - - /** Returns whether artwork is displayed if present in the media. */ - public boolean getUseArtwork() { - return useArtwork; - } - - /** - * Sets whether artwork is displayed if present in the media. - * - * @param useArtwork Whether artwork is displayed. - */ - public void setUseArtwork(boolean useArtwork) { - Assertions.checkState(!useArtwork || artworkView != null); - if (this.useArtwork != useArtwork) { - this.useArtwork = useArtwork; - updateForCurrentTrackSelections(); - } - } - - /** Returns the default artwork to display. */ - public Bitmap getDefaultArtwork() { - return defaultArtwork; - } - - /** - * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is - * present in the media. - * - * @param defaultArtwork the default artwork to display. - */ - public void setDefaultArtwork(Bitmap defaultArtwork) { - if (this.defaultArtwork != defaultArtwork) { - this.defaultArtwork = defaultArtwork; - updateForCurrentTrackSelections(); - } - } - - /** Returns whether the playback controls can be shown. */ - public boolean getUseController() { - return useController; - } - - /** - * Sets whether the playback controls can be shown. If set to {@code false} the playback controls - * are never visible and are disconnected from the player. - * - * @param useController Whether the playback controls can be shown. - */ - public void setUseController(boolean useController) { - Assertions.checkState(!useController || controller != null); - if (this.useController == useController) { - return; - } - this.useController = useController; - if (useController) { - controller.setPlayer(player); - } else if (controller != null) { - controller.hide(); - controller.setPlayer(null); - } - } - - /** - * Sets the background color of the {@code exo_shutter} view. - * - * @param color The background color. - */ - public void setShutterBackgroundColor(int color) { - if (shutterView != null) { - shutterView.setBackgroundColor(color); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (player != null && player.isPlayingAd()) { - // Focus any overlay UI now, in case it's provided by a WebView whose contents may update - // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using - // IMA [Internal: b/62371030]. - overlayFrameLayout.requestFocus(); - return super.dispatchKeyEvent(event); - } - boolean isDpadWhenControlHidden = - isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - maybeShowController(true); - return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. Does nothing if playback controls are disabled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - return useController && controller.dispatchMediaKeyEvent(event); - } - - /** - * Shows the playback controls. Does nothing if playback controls are disabled. - * - *

      The playback controls are automatically hidden during playback after {{@link - * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, - * is paused, has ended or failed. - */ - public void showController() { - showController(shouldShowControllerIndefinitely()); - } - - /** Hides the playback controls. Does nothing if playback controls are disabled. */ - public void hideController() { - if (controller != null) { - controller.hide(); - } - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input and with playback or buffering in - * progress. - * - * @return The timeout in milliseconds. A non-positive value will cause the controller to remain - * visible indefinitely. - */ - public int getControllerShowTimeoutMs() { - return controllerShowTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input and with playback or buffering in progress. - * - * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the - * controller to remain visible indefinitely. - */ - public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { - Assertions.checkState(controller != null); - this.controllerShowTimeoutMs = controllerShowTimeoutMs; - // If controller is already visible, call showController to update the controller's timeout - // if necessary. - if (controller.isVisible()) { - showController(); - } - } - - /** Returns whether the playback controls are hidden by touch events. */ - public boolean getControllerHideOnTouch() { - return controllerHideOnTouch; - } - - /** - * Sets whether the playback controls are hidden by touch events. - * - * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. - */ - public void setControllerHideOnTouch(boolean controllerHideOnTouch) { - Assertions.checkState(controller != null); - this.controllerHideOnTouch = controllerHideOnTouch; - } - - /** - * Returns whether the playback controls are automatically shown when playback starts, pauses, - * ends, or fails. If set to false, the playback controls can be manually operated with {@link - * #showController()} and {@link #hideController()}. - */ - public boolean getControllerAutoShow() { - return controllerAutoShow; - } - - /** - * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, - * or fails. If set to false, the playback controls can be manually operated with {@link - * #showController()} and {@link #hideController()}. - * - * @param controllerAutoShow Whether the playback controls are allowed to show automatically. - */ - public void setControllerAutoShow(boolean controllerAutoShow) { - this.controllerAutoShow = controllerAutoShow; - } - - /** - * Sets whether the playback controls are hidden when ads are playing. Controls are always shown - * during ads if they are enabled and the player is paused. - * - * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. - */ - public void setControllerHideDuringAds(boolean controllerHideDuringAds) { - this.controllerHideDuringAds = controllerHideDuringAds; - } - - /** - * Set the {@link PlaybackControlView.VisibilityListener}. - * - * @param listener The listener to be notified about visibility changes. - */ - public void setControllerVisibilityListener(PlaybackControlView.VisibilityListener listener) { - Assertions.checkState(controller != null); - controller.setVisibilityListener(listener); - } - - /** - * Sets the {@link ControlDispatcher}. - * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link - * DefaultControlDispatcher}. - */ - public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { - Assertions.checkState(controller != null); - controller.setControlDispatcher(controlDispatcher); - } - - /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. - */ - public void setRewindIncrementMs(int rewindMs) { - Assertions.checkState(controller != null); - controller.setRewindIncrementMs(rewindMs); - } - - /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. - */ - public void setFastForwardIncrementMs(int fastForwardMs) { - Assertions.checkState(controller != null); - controller.setFastForwardIncrementMs(fastForwardMs); - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - Assertions.checkState(controller != null); - controller.setRepeatToggleModes(repeatToggleModes); - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - Assertions.checkState(controller != null); - controller.setShowShuffleButton(showShuffleButton); - } - - /** - * Sets whether the time bar should show all windows, as opposed to just the current one. - * - * @param showMultiWindowTimeBar Whether to show all windows. - */ - public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - Assertions.checkState(controller != null); - controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); - } - - /** - * Gets the view onto which video is rendered. This is a: - * - *

        - *
      • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code - * surface_view}. - *
      • {@link TextureView} if {@code surface_type} is {@code texture_view}. - *
      • {@code null} if {@code surface_type} is {@code none}. - *
      - * - * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. - */ - public View getVideoSurfaceView() { - return surfaceView; - } - - /** - * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of - * the player. - * - * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and - * the overlay is not present. - */ - public FrameLayout getOverlayFrameLayout() { - return overlayFrameLayout; - } - - /** - * Gets the {@link SubtitleView}. - * - * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the - * subtitle view is not present. - */ - public SubtitleView getSubtitleView() { - return subtitleView; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { - return false; - } - if (!controller.isVisible()) { - maybeShowController(true); - } else if (controllerHideOnTouch) { - controller.hide(); - } - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent ev) { - if (!useController || player == null) { - return false; - } - maybeShowController(true); - return true; - } - - /** Shows the playback controls, but only if forced or shown indefinitely. */ - private void maybeShowController(boolean isForced) { - if (isPlayingAd() && controllerHideDuringAds) { - return; - } - if (useController) { - boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; - boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); - if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { - showController(shouldShowIndefinitely); - } - } - } - - private boolean shouldShowControllerIndefinitely() { - if (player == null) { - return true; - } - int playbackState = player.getPlaybackState(); - return controllerAutoShow - && (playbackState == Player.STATE_IDLE - || playbackState == Player.STATE_ENDED - || !player.getPlayWhenReady()); - } - - private void showController(boolean showIndefinitely) { - if (!useController) { - return; - } - controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); - controller.show(); - } - - private boolean isPlayingAd() { - return player != null && player.isPlayingAd() && player.getPlayWhenReady(); - } - - private void updateForCurrentTrackSelections() { - if (player == null) { - return; - } - TrackSelectionArray selections = player.getCurrentTrackSelections(); - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; - } - } - // Video disabled so the shutter must be closed. - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } - // Display artwork if enabled and available, else hide it. - if (useArtwork) { - for (int i = 0; i < selections.length; i++) { - TrackSelection selection = selections.get(i); - if (selection != null) { - for (int j = 0; j < selection.length(); j++) { - Metadata metadata = selection.getFormat(j).metadata; - if (metadata != null && setArtworkFromMetadata(metadata)) { - return; - } - } - } - } - if (setArtworkFromBitmap(defaultArtwork)) { - return; - } - } - // Artwork disabled or unavailable. - hideArtwork(); - } - - private boolean setArtworkFromMetadata(Metadata metadata) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry metadataEntry = metadata.get(i); - if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; - Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setArtworkFromBitmap(bitmap); - } - } - return false; - } - - private boolean setArtworkFromBitmap(Bitmap bitmap) { - if (bitmap != null) { - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - if (bitmapWidth > 0 && bitmapHeight > 0) { - if (contentFrame != null) { - contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); - } - artworkView.setImageBitmap(bitmap); - artworkView.setVisibility(VISIBLE); - return true; - } - } - return false; - } - - private void hideArtwork() { - if (artworkView != null) { - artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. - artworkView.setVisibility(INVISIBLE); - } - } - - @TargetApi(23) - private static void configureEditModeLogoV23(Resources resources, ImageView logo) { - logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); - logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); - } - - @SuppressWarnings("deprecation") - private static void configureEditModeLogo(Resources resources, ImageView logo) { - logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); - logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); - } - - @SuppressWarnings("ResourceType") - private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { - aspectRatioFrame.setResizeMode(resizeMode); - } - - /** Applies a texture rotation to a {@link TextureView}. */ - private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { - float textureViewWidth = textureView.getWidth(); - float textureViewHeight = textureView.getHeight(); - if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { - textureView.setTransform(null); - } else { - Matrix transformMatrix = new Matrix(); - float pivotX = textureViewWidth / 2; - float pivotY = textureViewHeight / 2; - transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); - - // After rotation, scale the rotated texture to fit the TextureView size. - RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); - RectF rotatedTextureRect = new RectF(); - transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); - transformMatrix.postScale( - textureViewWidth / rotatedTextureRect.width(), - textureViewHeight / rotatedTextureRect.height(), - pivotX, - pivotY); - textureView.setTransform(transformMatrix); - } - } - - @SuppressLint("InlinedApi") - private boolean isDpadKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_DPAD_UP - || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; - } - - private final class ComponentListener extends Player.DefaultEventListener - implements TextOutput, SimpleExoPlayer.VideoListener, OnLayoutChangeListener { - - // TextOutput implementation - - @Override - public void onCues(List cues) { - if (subtitleView != null) { - subtitleView.onCues(cues); - } - } - - // SimpleExoPlayer.VideoInfoListener implementation - - @Override - public void onVideoSizeChanged( - int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame == null) { - return; - } - float videoAspectRatio = - (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; - - if (surfaceView instanceof TextureView) { - // Try to apply rotation transformation when our surface is a TextureView. - if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { - // We will apply a rotation 90/270 degree to the output texture of the TextureView. - // In this case, the output video's width and height will be swapped. - videoAspectRatio = 1 / videoAspectRatio; - } - if (textureViewRotation != 0) { - surfaceView.removeOnLayoutChangeListener(this); - } - textureViewRotation = unappliedRotationDegrees; - if (textureViewRotation != 0) { - // The texture view's dimensions might be changed after layout step. - // So add an OnLayoutChangeListener to apply rotation after layout step. - surfaceView.addOnLayoutChangeListener(this); - } - applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); - } - - contentFrame.setAspectRatio(videoAspectRatio); - } - - @Override - public void onRenderedFirstFrame() { - if (shutterView != null) { - shutterView.setVisibility(INVISIBLE); - } - } - - @Override - public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - updateForCurrentTrackSelections(); - } - - // Player.EventListener implementation - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } else { - maybeShowController(false); - } - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } - } - - // OnLayoutChangeListener implementation - - @Override - public void onLayoutChange( - View view, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - applyTextureViewRotation((TextureView) view, textureViewRotation); - } - } } diff --git a/library/ui/src/main/res/layout/exo_player_control_view.xml b/library/ui/src/main/res/layout/exo_player_control_view.xml new file mode 100644 index 0000000000..fd221e5d84 --- /dev/null +++ b/library/ui/src/main/res/layout/exo_player_control_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/library/ui/src/main/res/layout/exo_player_view.xml b/library/ui/src/main/res/layout/exo_player_view.xml new file mode 100644 index 0000000000..dc6dda1667 --- /dev/null +++ b/library/ui/src/main/res/layout/exo_player_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index b6ed4b17af..24fa8a2091 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -41,7 +41,7 @@
      - + @@ -52,7 +52,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 7164fa13ab..40d5b6c3f9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -32,6 +32,16 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; */ public abstract class StubExoPlayer implements ExoPlayer { + @Override + public VideoComponent getVideoComponent() { + throw new UnsupportedOperationException(); + } + + @Override + public TextComponent getTextComponent() { + throw new UnsupportedOperationException(); + } + @Override public Looper getPlaybackLooper() { throw new UnsupportedOperationException(); From 029c95832cea5a4ac81b1afb75b764595e7338d2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jan 2018 08:20:35 -0800 Subject: [PATCH 284/417] Add queue abstraction to ExoPlayerImplInternal. This gets rid of the manual tracking of this queue with reading, playing, and loading period holders. Still keeping these names for queue access methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182378944 --- .../exoplayer2/ExoPlayerImplInternal.java | 420 +++++++++++------- 1 file changed, 269 insertions(+), 151 deletions(-) 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 2647a44dee..b8667ce6d1 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 @@ -118,6 +118,7 @@ import java.util.Collections; private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList customMessageInfos; private final Clock clock; + private final MediaPeriodHolderQueue queue; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -136,10 +137,6 @@ import java.util.Collections; private long rendererPositionUs; private int nextCustomMessageInfoIndex; - private MediaPeriodHolder loadingPeriodHolder; - private MediaPeriodHolder readingPeriodHolder; - private MediaPeriodHolder playingPeriodHolder; - public ExoPlayerImplInternal( Renderer[] renderers, TrackSelector trackSelector, @@ -161,6 +158,7 @@ import java.util.Collections; this.eventHandler = eventHandler; this.player = player; this.clock = clock; + this.queue = new MediaPeriodHolderQueue(); backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -444,8 +442,7 @@ import java.util.Collections; private void validateExistingPeriodHolders() throws ExoPlaybackException { // Find the last existing period holder that matches the new period order. - MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null - ? playingPeriodHolder : loadingPeriodHolder; + MediaPeriodHolder lastValidPeriodHolder = queue.getFrontPeriod(); if (lastValidPeriodHolder == null) { return; } @@ -465,30 +462,19 @@ import java.util.Collections; } // Release any period holders that don't match the new period order. - int loadingPeriodHolderIndex = loadingPeriodHolder.index; - int readingPeriodHolderIndex = - readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET; - if (lastValidPeriodHolder.next != null) { - releasePeriodHoldersFrom(lastValidPeriodHolder.next); - lastValidPeriodHolder.next = null; - } + boolean readingPeriodRemoved = queue.removeAfter(lastValidPeriodHolder); // Update the period info for the last holder, as it may now be the last period in the timeline. lastValidPeriodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); - // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. - boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index; - if (!seenLoadingPeriodHolder) { - loadingPeriodHolder = lastValidPeriodHolder; - } - boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET - && readingPeriodHolderIndex <= lastValidPeriodHolder.index; - if (!seenReadingPeriodHolder && playingPeriodHolder != null) { + if (readingPeriodRemoved && queue.hasPlayingPeriod()) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - MediaPeriodId periodId = playingPeriodHolder.info.id; - long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); + MediaPeriodId periodId = queue.getPlayingPeriod().info.id; + long newPositionUs = + seekToPeriodPosition( + periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); if (newPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); @@ -513,11 +499,12 @@ import java.util.Collections; } private void updatePlaybackPositions() throws ExoPlaybackException { - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { return; } // Update the playback position. + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); @@ -545,12 +532,13 @@ import java.util.Collections; private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = clock.uptimeMillis(); updatePeriods(); - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // We're still waiting for the first period to be prepared. maybeThrowPeriodPrepareError(); scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); return; } + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); TraceUtil.beginSection("doSomeWork"); @@ -594,10 +582,13 @@ import java.util.Collections; stopRenderers(); } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { float playbackSpeed = mediaClock.getPlaybackParameters().speed; - boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( - rendererPositionUs, playbackSpeed, rebuffering)) - : isTimelineReady(playingPeriodDurationUs); + boolean isNewlyReady = + enabledRenderers.length > 0 + ? (allRenderersReadyOrEnded + && queue + .getLoadingPeriod() + .haveSufficientBuffer(rendererPositionUs, playbackSpeed, rebuffering)) + : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(Player.STATE_READY); if (playWhenReady) { @@ -672,6 +663,7 @@ import java.util.Collections; try { long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); if (playingPeriodHolder != null && newPeriodPositionUs != 0) { newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( @@ -698,58 +690,50 @@ import java.util.Collections; private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) throws ExoPlaybackException { + // Force disable renderers if they are reading from a period other than the one being played. + return seekToPeriodPosition( + periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod()); + } + + private long seekToPeriodPosition( + MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers) + throws ExoPlaybackException { stopRenderers(); rebuffering = false; setState(Player.STATE_BUFFERING); - MediaPeriodHolder newPlayingPeriodHolder = null; - if (playingPeriodHolder == null) { - // We're still waiting for the first period to be prepared. - if (loadingPeriodHolder != null) { - loadingPeriodHolder.release(); - } - } else { - // Clear the timeline, but keep the requested period if it is already prepared. - MediaPeriodHolder periodHolder = playingPeriodHolder; - while (periodHolder != null) { - if (newPlayingPeriodHolder == null - && shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { - newPlayingPeriodHolder = periodHolder; - } else { - periodHolder.release(); - } - periodHolder = periodHolder.next; + // Clear the timeline, but keep the requested period if it is already prepared. + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; + while (newPlayingPeriodHolder != null) { + if (shouldKeepPeriodHolder(periodId, periodPositionUs, newPlayingPeriodHolder)) { + queue.removeAfter(newPlayingPeriodHolder); + break; } + newPlayingPeriodHolder = queue.advancePlayingPeriod(); } - // Disable all the renderers if the period being played is changing, or if the renderers are - // reading from a period other than the one being played. - if (playingPeriodHolder != newPlayingPeriodHolder - || playingPeriodHolder != readingPeriodHolder) { + // Disable all the renderers if the period being played is changing, or if forced. + if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) { for (Renderer renderer : enabledRenderers) { disableRenderer(renderer); } enabledRenderers = new Renderer[0]; - playingPeriodHolder = null; + oldPlayingPeriodHolder = null; } // Update the holders. if (newPlayingPeriodHolder != null) { - newPlayingPeriodHolder.next = null; - loadingPeriodHolder = newPlayingPeriodHolder; - readingPeriodHolder = newPlayingPeriodHolder; - setPlayingPeriodHolder(newPlayingPeriodHolder); - if (playingPeriodHolder.hasEnabledTracks) { - periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); - playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs - backBufferDurationUs, - retainBackBufferFromKeyframe); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + if (newPlayingPeriodHolder.hasEnabledTracks) { + periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); + newPlayingPeriodHolder.mediaPeriod.discardBuffer( + periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { - loadingPeriodHolder = null; - readingPeriodHolder = null; - playingPeriodHolder = null; + queue.clear(); resetRendererPosition(periodPositionUs); } @@ -771,9 +755,10 @@ import java.util.Collections; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { - rendererPositionUs = playingPeriodHolder == null - ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US - : playingPeriodHolder.toRendererTime(periodPositionUs); + rendererPositionUs = + !queue.hasPlayingPeriod() + ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US + : queue.getPlayingPeriod().toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); @@ -825,11 +810,7 @@ import java.util.Collections; } } enabledRenderers = new Renderer[0]; - releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder); - loadingPeriodHolder = null; - readingPeriodHolder = null; - playingPeriodHolder = null; + queue.clear(); setIsLoading(false); Timeline timeline = playbackInfo.timeline; int firstPeriodIndex = @@ -1030,13 +1011,14 @@ import java.util.Collections; } private void reselectTracksInternal() throws ExoPlaybackException { - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // We don't have tracks yet, so we don't care. return; } float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. - MediaPeriodHolder periodHolder = playingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean selectionsChangedForReadPeriod = true; while (true) { if (periodHolder == null || !periodHolder.prepared) { @@ -1056,11 +1038,8 @@ import java.util.Collections; if (selectionsChangedForReadPeriod) { // Update streams and rebuffer for the new selection, recreating all streams if reading ahead. - boolean recreateStreams = readingPeriodHolder != playingPeriodHolder; - releasePeriodHoldersFrom(playingPeriodHolder.next); - playingPeriodHolder.next = null; - loadingPeriodHolder = playingPeriodHolder; - readingPeriodHolder = playingPeriodHolder; + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + boolean recreateStreams = queue.removeAfter(playingPeriodHolder); boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( @@ -1092,21 +1071,17 @@ import java.util.Collections; } } } - playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); + playbackInfo = + playbackInfo.copyWithTrackSelectorResult(playingPeriodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. - loadingPeriodHolder = periodHolder; - periodHolder = loadingPeriodHolder.next; - while (periodHolder != null) { - periodHolder.release(); - periodHolder = periodHolder.next; - } - loadingPeriodHolder.next = null; - if (loadingPeriodHolder.prepared) { - long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs, - loadingPeriodHolder.toPeriodTime(rendererPositionUs)); - loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); + queue.removeAfter(periodHolder); + if (periodHolder.prepared) { + long loadingPeriodPositionUs = + Math.max( + periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); + periodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } if (playbackInfo.playbackState != Player.STATE_ENDED) { @@ -1117,8 +1092,7 @@ import java.util.Collections; } private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { - MediaPeriodHolder periodHolder = - playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getFrontPeriod(); while (periodHolder != null) { if (periodHolder.trackSelectorResult != null) { TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); @@ -1133,6 +1107,7 @@ import java.util.Collections; } private boolean isTimelineReady(long playingPeriodDurationUs) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs || (playingPeriodHolder.next != null @@ -1140,6 +1115,8 @@ import java.util.Collections; } private void maybeThrowPeriodPrepareError() throws IOException { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) { for (Renderer renderer : enabledRenderers) { @@ -1202,8 +1179,7 @@ import java.util.Collections; } int playingPeriodIndex = playbackInfo.periodId.periodIndex; - MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getFrontPeriod(); if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { return; } @@ -1283,22 +1259,15 @@ import java.util.Collections; periodHolder = updatePeriodInfo(periodHolder, periodIndex); } else { // The holder is inconsistent with the new timeline. - boolean seenReadingPeriodHolder = - readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index; - if (!seenReadingPeriodHolder) { + boolean readingPeriodRemoved = queue.removeAfter(previousPeriodHolder); + if (readingPeriodRemoved) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. + MediaPeriodId id = queue.getPlayingPeriod().info.id; long newPositionUs = - seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs); - playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, newPositionUs, - playbackInfo.contentPositionUs); - } else { - // Update the loading period to be the last period that's still valid, and release all - // subsequent periods. - loadingPeriodHolder = previousPeriodHolder; - loadingPeriodHolder.next = null; - // Release the rest of the timeline. - releasePeriodHoldersFrom(periodHolder); + seekToPeriodPosition(id, playbackInfo.positionUs, /* forceDisableRenderers= */ true); + playbackInfo = + playbackInfo.fromNewPosition(id, newPositionUs, playbackInfo.contentPositionUs); } break; } @@ -1426,19 +1395,21 @@ import java.util.Collections; // Update the loading period if required. maybeUpdateLoadingPeriod(); - + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (loadingPeriodHolder != null && !playbackInfo.isLoading) { maybeContinueLoading(); } - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // We're waiting for the first period to be prepared. return; } // Advance the playing period if necessary. + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean advancedPlayingPeriod = false; while (playWhenReady && playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { @@ -1452,8 +1423,9 @@ import java.util.Collections; playingPeriodHolder.info.isLastInTimelinePeriod ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION : Player.DISCONTINUITY_REASON_AD_INSERTION; - playingPeriodHolder.release(); - setPlayingPeriodHolder(playingPeriodHolder.next); + MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder; + playingPeriodHolder = queue.advancePlayingPeriod(); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); @@ -1492,7 +1464,7 @@ import java.util.Collections; } TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult; - readingPeriodHolder = readingPeriodHolder.next; + readingPeriodHolder = queue.advanceReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult; boolean initialDiscontinuity = @@ -1536,6 +1508,7 @@ import java.util.Collections; private void maybeUpdateLoadingPeriod() throws IOException { MediaPeriodInfo info; + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null) { info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); } else { @@ -1544,12 +1517,9 @@ import java.util.Collections; || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { return; } - if (playingPeriodHolder != null) { - int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index; - if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) { - // We are already buffering the maximum number of periods ahead. - return; - } + if (queue.getLength() == MAXIMUM_BUFFER_AHEAD_PERIODS) { + // We are already buffering the maximum number of periods ahead. + return; } info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info, loadingPeriodHolder.getRendererOffset(), rendererPositionUs); @@ -1563,34 +1533,40 @@ import java.util.Collections; loadingPeriodHolder == null ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US) : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); - int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info); - if (loadingPeriodHolder != null) { - loadingPeriodHolder.next = newPeriodHolder; - } - loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); + MediaPeriodHolder newPeriodHolder = + new MediaPeriodHolder( + renderers, + rendererCapabilities, + rendererPositionOffsetUs, + trackSelector, + loadControl, + mediaSource, + uid, + info); + queue.enqueueLoadingPeriod(newPeriodHolder); + newPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); } private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. return; } loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // This is the first prepared period, so start playing it. - readingPeriodHolder = loadingPeriodHolder; - resetRendererPosition(readingPeriodHolder.info.startPositionUs); - setPlayingPeriodHolder(readingPeriodHolder); + MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); + resetRendererPosition(playingPeriodHolder.info.startPositionUs); + updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null); } maybeContinueLoading(); } private void handleContinueLoadingRequested(MediaPeriod period) { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. return; @@ -1600,6 +1576,7 @@ import java.util.Collections; } private void maybeContinueLoading() { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); boolean continueLoading = loadingPeriodHolder.shouldContinueLoading( rendererPositionUs, mediaClock.getPlaybackParameters().speed); setIsLoading(continueLoading); @@ -1608,38 +1585,32 @@ import java.util.Collections; } } - private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) { - while (periodHolder != null) { - periodHolder.release(); - periodHolder = periodHolder.next; - } - } - - private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { - if (playingPeriodHolder == periodHolder) { + private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder) + throws ExoPlaybackException { + MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod(); + if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) { return; } - int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - if (periodHolder.trackSelectorResult.renderersEnabled[i]) { + if (newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { enabledRendererCount++; } - if (rendererWasEnabledFlags[i] && (!periodHolder.trackSelectorResult.renderersEnabled[i] - || (renderer.isCurrentStreamFinal() - && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) { + if (rendererWasEnabledFlags[i] + && (!newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i] + || (renderer.isCurrentStreamFinal() + && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) { // The renderer should be disabled before playing the next period, either because it's not // needed to play the next period, or because we need to re-enable it as its current stream // is final and it's not reading ahead. disableRenderer(renderer); } } - - playingPeriodHolder = periodHolder; - playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); + playbackInfo = + playbackInfo.copyWithTrackSelectorResult(newPlayingPeriodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1647,6 +1618,7 @@ import java.util.Collections; throws ExoPlaybackException { enabledRenderers = new Renderer[totalEnabledRendererCount]; int enabledRendererCount = 0; + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); for (int i = 0; i < renderers.length; i++) { if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); @@ -1656,6 +1628,7 @@ import java.util.Collections; private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex) throws ExoPlaybackException { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); Renderer renderer = renderers[rendererIndex]; enabledRenderers[enabledRendererIndex] = renderer; if (renderer.getState() == Renderer.STATE_DISABLED) { @@ -1681,6 +1654,7 @@ import java.util.Collections; } private boolean rendererWaitingForNextStream(Renderer renderer) { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); return readingPeriodHolder.next != null && readingPeriodHolder.next.prepared && renderer.hasReadStreamToEnd(); } @@ -1696,6 +1670,146 @@ import java.util.Collections; return formats; } + /** + * Holds a queue of {@link MediaPeriodHolder}s from the currently playing period holder at the + * front to the loading period holder at the end of the queue. Also has a reference to the reading + * period holder. + */ + private static final class MediaPeriodHolderQueue { + + private MediaPeriodHolder playing; + private MediaPeriodHolder reading; + private MediaPeriodHolder loading; + private int length; + + /** + * Returns the loading period holder which is at the end of the queue, or null if the queue is + * empty. + */ + public MediaPeriodHolder getLoadingPeriod() { + return loading; + } + + /** + * Returns the playing period holder which is at the front of the queue, or null if the queue is + * empty or hasn't started playing. + */ + public MediaPeriodHolder getPlayingPeriod() { + return playing; + } + + /** + * Returns the reading period holder, or null if the queue is empty or the player hasn't started + * reading. + */ + public MediaPeriodHolder getReadingPeriod() { + return reading; + } + + /** + * Returns the period holder in the front of the queue which is the playing period holder when + * playing, or null if the queue is empty. + */ + public MediaPeriodHolder getFrontPeriod() { + return hasPlayingPeriod() ? playing : loading; + } + + /** Returns the current length of the queue. */ + public int getLength() { + return length; + } + + /** Returns whether the reading and playing period holders are set. */ + public boolean hasPlayingPeriod() { + return playing != null; + } + + /** + * Continues reading from the next period holder in the queue. + * + * @return The updated reading period holder. + */ + public MediaPeriodHolder advanceReadingPeriod() { + Assertions.checkState(reading != null && reading.next != null); + reading = reading.next; + return reading; + } + + /** Enqueues a new period holder at the end, which becomes the new loading period holder. */ + public void enqueueLoadingPeriod(MediaPeriodHolder mediaPeriodHolder) { + Assertions.checkState(mediaPeriodHolder != null); + if (loading != null) { + Assertions.checkState(hasPlayingPeriod()); + loading.next = mediaPeriodHolder; + } + loading = mediaPeriodHolder; + length++; + } + + /** + * Dequeues the playing period holder from the front of the queue and advances the playing + * period holder to be the next item in the queue. If the playing period holder is unset, set it + * to the item in the front of the queue. + * + * @return The updated playing period holder, or null if the queue is or becomes empty. + */ + public MediaPeriodHolder advancePlayingPeriod() { + if (playing != null) { + if (playing == reading) { + reading = playing.next; + } + playing.release(); + playing = playing.next; + length--; + if (length == 0) { + loading = null; + } + } else { + playing = loading; + reading = loading; + } + return playing; + } + + /** + * Removes all period holders after the given period holder. This process may also remove the + * currently reading period holder. If that is the case, the reading period holder is set to be + * the same as the playing period holder at the front of the queue. + * + * @param mediaPeriodHolder The media period holder that shall be the new end of the queue. + * @return Whether the reading period has been removed. + */ + public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) { + Assertions.checkState(mediaPeriodHolder != null); + boolean removedReading = false; + loading = mediaPeriodHolder; + while (mediaPeriodHolder.next != null) { + mediaPeriodHolder = mediaPeriodHolder.next; + if (mediaPeriodHolder == reading) { + reading = playing; + removedReading = true; + } + mediaPeriodHolder.release(); + length--; + } + loading.next = null; + return removedReading; + } + + /** Clears the queue. */ + public void clear() { + MediaPeriodHolder front = getFrontPeriod(); + if (front != null) { + front.release(); + removeAfter(front); + } + playing = null; + loading = null; + reading = null; + length = 0; + } + } + /** * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ @@ -1703,7 +1817,6 @@ import java.util.Collections; public final MediaPeriod mediaPeriod; public final Object uid; - public final int index; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; @@ -1722,9 +1835,15 @@ import java.util.Collections; private TrackSelectorResult periodTrackSelectorResult; - public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) { + public MediaPeriodHolder( + Renderer[] renderers, + RendererCapabilities[] rendererCapabilities, + long rendererPositionOffsetUs, + TrackSelector trackSelector, + LoadControl loadControl, + MediaSource mediaSource, + Object periodUid, + MediaPeriodInfo info) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; @@ -1732,7 +1851,6 @@ import java.util.Collections; this.loadControl = loadControl; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); - this.index = index; this.info = info; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; From 68387f98ee836d4b4491866162ac74693f441063 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jan 2018 09:11:46 -0800 Subject: [PATCH 285/417] Simplify demo app by moving EventLogger into core It seems good to have EventLogger available from the library. In particular because when app developers use it and then submit bug reports, it makes it much easier to work out what happened. It will also allow EventLogger to be used across our (now multiple) demo apps. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182389407 --- RELEASENOTES.md | 1 + .../com/google/android/exoplayer2/demo/PlayerActivity.java | 1 + .../com/google/android/exoplayer2/util}/EventLogger.java | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) rename {demos/main/src/main/java/com/google/android/exoplayer2/demo => library/core/src/main/java/com/google/android/exoplayer2/util}/EventLogger.java (99%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1a54a44058..b787838f41 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -69,6 +69,7 @@ cache after deciding to bypass cache. * IMA extension: Add support for playing non-Extractor content MediaSources in the IMA demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). +* `EventLogger` moved from the demo app into the core library. ### 2.6.1 ### 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 a2e671bd11..f2c728f516 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 @@ -73,6 +73,7 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java similarity index 99% rename from demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 9d28aa47f0..3a178a7f4a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.demo; +package com.google.android.exoplayer2.util; import android.os.SystemClock; import android.util.Log; @@ -53,8 +53,8 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; -/** Logs player events using {@link Log}. */ -/* package */ final class EventLogger +/** Logs events from {@link Player} and other core components using {@link Log}. */ +public class EventLogger implements Player.EventListener, MetadataOutput, AudioRendererEventListener, From 06be0fd712e6c48741752f4f43738ba522e152d0 Mon Sep 17 00:00:00 2001 From: kqyang Date: Thu, 18 Jan 2018 11:11:25 -0800 Subject: [PATCH 286/417] Add SAMPLE-AES-CTR, which replaces SAMPLE-AES-CENC per latest spefication: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182407790 --- .../hls/playlist/HlsPlaylistParser.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 9bdb01c2e4..100c4c78e6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -76,7 +76,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 19 Jan 2018 01:45:09 -0800 Subject: [PATCH 287/417] Mitigate OOM at poorly interleaved Mp4 streams. When determining the next sample to load, the Mp4Extractor now takes into account how far one stream is reading ahead of the others. If one stream is reading ahead more than a threshold (default: 10 seconds), the extractor continues reading the other stream even though it needs to reload the source at a new position. GitHub:#3481 GitHub:#3214 GitHub:#3670 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182504396 --- .../extractor/mp4/Mp4Extractor.java | 127 ++++++++++++++---- 1 file changed, 104 insertions(+), 23 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 2c56f9ac2f..112c2d1ba0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -88,6 +88,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + /** + * For poorly interleaved streams, the maximum byte difference one track is allowed to be read + * ahead before the source will be reloaded at a new position to read another track. + */ + private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024; + private final @Flags int flags; // Temporary arrays. @@ -103,12 +109,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int atomHeaderBytesRead; private ParsableByteArray atomData; + private int sampleTrackIndex; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; // Extractor outputs. private ExtractorOutput extractorOutput; private Mp4Track[] tracks; + private long[][] accumulatedSampleSizes; private int firstVideoTrackIndex; private long durationUs; private boolean isQuickTime; @@ -132,6 +140,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms = new Stack<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); + sampleTrackIndex = C.INDEX_UNSET; } @Override @@ -148,6 +157,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { public void seek(long position, long timeUs) { containerAtoms.clear(); atomHeaderBytesRead = 0; + sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; if (position == 0) { @@ -426,6 +436,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); + accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks); extractorOutput.endTracks(); extractorOutput.seekMap(this); @@ -449,26 +460,29 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private int readSample(ExtractorInput input, PositionHolder positionHolder) throws IOException, InterruptedException { - int trackIndex = getTrackIndexOfEarliestCurrentSample(); - if (trackIndex == C.INDEX_UNSET) { - return RESULT_END_OF_INPUT; + long inputPosition = input.getPosition(); + if (sampleTrackIndex == C.INDEX_UNSET) { + sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition); + if (sampleTrackIndex == C.INDEX_UNSET) { + return RESULT_END_OF_INPUT; + } } - Mp4Track track = tracks[trackIndex]; + Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex]; - if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { - // The sample information is contained in a cdat atom. The header must be discarded for - // committing. - position += Atom.HEADER_SIZE; - sampleSize -= Atom.HEADER_SIZE; - } - long skipAmount = position - input.getPosition() + sampleBytesWritten; + long skipAmount = position - inputPosition + sampleBytesWritten; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; } + if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + // The sample information is contained in a cdat atom. The header must be discarded for + // committing. + skipAmount += Atom.HEADER_SIZE; + sampleSize -= Atom.HEADER_SIZE; + } input.skipFully((int) skipAmount); if (track.track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case @@ -510,33 +524,61 @@ public final class Mp4Extractor implements Extractor, SeekMap { trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleIndex++; + sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; return RESULT_CONTINUE; } /** - * Returns the index of the track that contains the earliest current sample, or - * {@link C#INDEX_UNSET} if no samples remain. + * Returns the index of the track that contains the next sample to be read, or {@link + * C#INDEX_UNSET} if no samples remain. + * + *

      The preferred choice is the sample with the smallest offset not requiring a source reload, + * or if not available the sample with the smallest overall offset to avoid subsequent source + * reloads. + * + *

      To deal with poor sample interleaving, we also check whether the required memory to catch up + * with the next logical sample (based on sample time) exceeds {@link + * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even + * though it may require a source reload. */ - private int getTrackIndexOfEarliestCurrentSample() { - int earliestSampleTrackIndex = C.INDEX_UNSET; - long earliestSampleOffset = Long.MAX_VALUE; + private int getTrackIndexOfNextReadSample(long inputPosition) { + long preferredSkipAmount = Long.MAX_VALUE; + boolean preferredRequiresReload = true; + int preferredTrackIndex = C.INDEX_UNSET; + long preferredAccumulatedBytes = Long.MAX_VALUE; + long minAccumulatedBytes = Long.MAX_VALUE; + boolean minAccumulatedBytesRequiresReload = true; + int minAccumulatedBytesTrackIndex = C.INDEX_UNSET; for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { Mp4Track track = tracks[trackIndex]; int sampleIndex = track.sampleIndex; if (sampleIndex == track.sampleTable.sampleCount) { continue; } - - long trackSampleOffset = track.sampleTable.offsets[sampleIndex]; - if (trackSampleOffset < earliestSampleOffset) { - earliestSampleOffset = trackSampleOffset; - earliestSampleTrackIndex = trackIndex; + long sampleOffset = track.sampleTable.offsets[sampleIndex]; + long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex]; + long skipAmount = sampleOffset - inputPosition; + boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE; + if ((!requiresReload && preferredRequiresReload) + || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) { + preferredRequiresReload = requiresReload; + preferredSkipAmount = skipAmount; + preferredTrackIndex = trackIndex; + preferredAccumulatedBytes = sampleAccumulatedBytes; + } + if (sampleAccumulatedBytes < minAccumulatedBytes) { + minAccumulatedBytes = sampleAccumulatedBytes; + minAccumulatedBytesRequiresReload = requiresReload; + minAccumulatedBytesTrackIndex = trackIndex; } } - - return earliestSampleTrackIndex; + return minAccumulatedBytes == Long.MAX_VALUE + || !minAccumulatedBytesRequiresReload + || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM + ? preferredTrackIndex + : minAccumulatedBytesTrackIndex; } /** @@ -554,6 +596,45 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } + /** + * For each sample of each track, calculates accumulated size of all samples which need to be read + * before this sample can be used. + */ + private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) { + long[][] accumulatedSampleSizes = new long[tracks.length][]; + int[] nextSampleIndex = new int[tracks.length]; + long[] nextSampleTimesUs = new long[tracks.length]; + boolean[] tracksFinished = new boolean[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount]; + nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0]; + } + long accumulatedSampleSize = 0; + int finishedTracks = 0; + while (finishedTracks < tracks.length) { + long minTimeUs = Long.MAX_VALUE; + int minTimeTrackIndex = -1; + for (int i = 0; i < tracks.length; i++) { + if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) { + minTimeTrackIndex = i; + minTimeUs = nextSampleTimesUs[i]; + } + } + int trackSampleIndex = nextSampleIndex[minTimeTrackIndex]; + accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize; + accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex]; + nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex; + if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) { + nextSampleTimesUs[minTimeTrackIndex] = + tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex]; + } else { + tracksFinished[minTimeTrackIndex] = true; + finishedTracks++; + } + } + return accumulatedSampleSizes; + } + /** * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, * for a given {@code seekTimeUs}. From 4828f275c7177a486fa1518b6242ac962741cb87 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 19 Jan 2018 02:10:57 -0800 Subject: [PATCH 288/417] Make play button behave differently in IDLE and ENDED states - In IDLE, the button will now call a preparer. This allows removal of the separate retry button from the demo app. - In ENDED, the button will seek back to the default position and play. - Behavior is made consistent with LeanbackPlayerAdapter. Issue: #3689 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182506855 --- RELEASENOTES.md | 5 +++- .../exoplayer2/demo/PlayerActivity.java | 24 +++++++-------- .../src/main/res/layout/player_activity.xml | 10 +------ demos/main/src/main/res/values/strings.xml | 2 -- .../ext/leanback/LeanbackPlayerAdapter.java | 17 ++++++++++- .../android/exoplayer2/PlaybackPreparer.java | 23 +++++++++++++++ .../exoplayer2/ui/PlayerControlView.java | 29 +++++++++++++++++-- .../android/exoplayer2/ui/PlayerView.java | 11 +++++++ 8 files changed, 94 insertions(+), 27 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b787838f41..d60dc147d8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,12 +19,15 @@ periods. * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow more customization of the message. Now supports setting a message delivery - playback position and/or a delivery handler. + playback position and/or a delivery handler ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * UI components: * Generalized player and control views to allow them to bind with any `Player`, and renamed them to `PlayerView` and `PlayerControlView` respectively. + * Made `PlayerView`'s play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by 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 f2c728f516..f9185ec2d2 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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; @@ -82,7 +83,7 @@ import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends Activity - implements OnClickListener, PlayerControlView.VisibilityListener { + implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL = "drm_license_url"; @@ -114,7 +115,6 @@ public class PlayerActivity extends Activity private PlayerView playerView; private LinearLayout debugRootView; private TextView debugTextView; - private Button retryButton; private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; @@ -152,8 +152,6 @@ public class PlayerActivity extends Activity rootView.setOnClickListener(this); debugRootView = findViewById(R.id.controls_root); debugTextView = findViewById(R.id.debug_text_view); - retryButton = findViewById(R.id.retry_button); - retryButton.setOnClickListener(this); playerView = findViewById(R.id.player_view); playerView.setControllerVisibilityListener(this); @@ -229,9 +227,7 @@ public class PlayerActivity extends Activity @Override public void onClick(View view) { - if (view == retryButton) { - initializePlayer(); - } else if (view.getParent() == debugRootView) { + if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { trackSelectionHelper.showSelectionDialog( @@ -240,6 +236,13 @@ public class PlayerActivity extends Activity } } + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + initializePlayer(); + } + // PlaybackControlView.VisibilityListener implementation @Override @@ -301,9 +304,10 @@ public class PlayerActivity extends Activity player.addMetadataOutput(eventLogger); player.addAudioDebugListener(eventLogger); player.addVideoDebugListener(eventLogger); + player.setPlayWhenReady(shouldAutoPlay); playerView.setPlayer(player); - player.setPlayWhenReady(shouldAutoPlay); + playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } @@ -502,10 +506,6 @@ public class PlayerActivity extends Activity private void updateButtonVisibilities() { debugRootView.removeAllViews(); - - retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); - debugRootView.addView(retryButton); - if (player == null) { return; } diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml index b2894140fe..6b84033273 100644 --- a/demos/main/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -42,15 +42,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="gone"> - -