From 566170f308d2e29d7ee568f98be3344a1a3743ed Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 Nov 2017 08:55:50 -0800 Subject: [PATCH 001/105] 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 a87066415d4..a1f8fc78610 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 static final class Builder { */ 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 @@ SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, 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 Builder setMediaSource(MediaSource mediaSource) { 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 @@ private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource // 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 void run() { 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 ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { // 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 void assertTimelinesEqual(Timeline... timelines) { } } + /** + * 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 void assertManifestsEqual(Object... manifests) { } } + /** + * 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 bff221b85e365733cbc9bcf630fc5b27efcfe3c6 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 07:28:57 -0800 Subject: [PATCH 002/105] 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/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 147 ++++++++++++++++++ .../playbacktests/gts/DashTestRunner.java | 7 +- 3 files changed, 157 insertions(+), 4 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 08c5bddb093..614626077a6 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { 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 c529fcab4b2..3d5a9c393d8 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 @@ 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 DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourc * @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 DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourc * @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 DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFac * 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 DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFac * 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 85cefbc2f68..215d8a05185 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 @@ protected MediaSource buildSource(HostActivity host, String userAgent, 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 877c89a0e1f40b08c624ab5b0c2cb1bba3836846 Mon Sep 17 00:00:00 2001 From: arnaudberry Date: Mon, 13 Nov 2017 10:49:09 -0800 Subject: [PATCH 003/105] 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 7ffb4297841..137e29c5ab5 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 @@ -488,7 +488,7 @@ protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseU 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, @@ -535,7 +535,7 @@ protected Representation buildRepresentation(RepresentationInfo representationIn } 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); } @@ -986,7 +986,8 @@ protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) { } } - private static final class RepresentationInfo { + /** A parsed Representation element. */ + protected static final class RepresentationInfo { public final Format format; public final String baseUrl; @@ -994,16 +995,18 @@ private static final class RepresentationInfo { 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 5bf4c249a28b13acb06f8cd27db5fbe497237cc8 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 11:10:54 -0800 Subject: [PATCH 004/105] 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 56d5f05d00f..0edd19bc09c 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.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 void onSeekProcessed() { 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 4d1767b64c6..33889a2b573 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 @@ -1666,11 +1666,11 @@ public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStr // 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; @@ -1692,6 +1692,7 @@ public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStr } public void release() { + updatePeriodTrackSelectorResult(null); try { if (info.endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); @@ -1704,6 +1705,36 @@ public void release() { } } + 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 054ee7973f6..6bc6afb88bf 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 BaseTrackSelection(TrackGroup group, int... tracks) { 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 ad02b6c7756..027b2abde96 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 @@ interface Factory { } + /** + * 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 @@ interface Factory { /** * 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 @@ void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, * 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 @@ void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, * 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 00000000000..20346a03558 --- /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 00000000000..da9a1a18ade --- /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 79a9155438d92e459cd320b2f540e34cd029f325 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 07:24:10 -0700 Subject: [PATCH 005/105] 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 c9364aa6054..d24788f74ac 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 @@ 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()); - } - - private static ExtractorFactory getExtractorFactory() { - return getExtractorFactory(0); + 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(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 867e4501faf..e86157dd922 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.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 Extractor[] createExtractors() { */ @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 Extractor[] createExtractors() { * 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 Extractor[] createExtractors() { @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 FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjus * @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 @@ private void maybeInitExtraTracks() { 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 @@ private boolean readSample(ExtractorInput input) throws IOException, Interrupted 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 1eac1b56167..66455b2f042 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.Util; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -424,10 +425,12 @@ protected static final class RepresentationHolder { 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 957aefcdbc5..c8015209272 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 @@ -72,7 +72,8 @@ public Pair createExtractor(Extractor previousExtractor, Uri 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 25dd8aa1f8c173563d114e5a55f9c08cbd60b840 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 13 Nov 2017 13:18:42 -0800 Subject: [PATCH 006/105] 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 c8015209272..dc838c9506c 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 Pair createExtractor(Extractor previousExtractor, Uri // 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 c63ded62754..90644125b18 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 @@ -107,7 +107,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Tue, 14 Nov 2017 03:48:47 -0800 Subject: [PATCH 007/105] 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/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 117 +++++++------- .../source/smoothstreaming/SsMediaSource.java | 143 ++++++++++++++++++ 3 files changed, 206 insertions(+), 61 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 614626077a6..65e1c0e0831 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { : 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 3d5a9c393d8..54a5086d3b0 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 static final class Builder { */ 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 Builder setManifestParser( return this; } - /** * Builds a new {@link DashMediaSource} using the current parameters. *

@@ -236,7 +179,6 @@ public Builder setManifestParser( * @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 DashMediaSource build() { } + /** + * 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 548f7877415..5a938474285 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 @@ 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 SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFacto * @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 SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFacto * @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 SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFacto * 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 SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFacto * 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 77b48691aa0f5fc5218bfc9cb3d6cff4c8b06179 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 04:08:38 -0800 Subject: [PATCH 008/105] 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 27a5c68e28b..9233b016f5f 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 @@ private static String getAdaptiveSupportString(int trackCount, int adaptiveSuppo } } + // 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 eea8cd169c76b52bcf303c137ceab60e6773c47f Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 06:17:02 -0800 Subject: [PATCH 009/105] 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 --- .../android/exoplayer2/source/TrackGroupArray.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 394cec891b5..fb28da581ce 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 TrackGroup get(int index) { * @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 int indexOf(TrackGroup group) { 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 76ba1890aa48202d46cbbeccc7b69286e272d263 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 08:06:50 -0800 Subject: [PATCH 010/105] 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 e506d0a4b3f..e7b2a8d963b 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.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 void testPlaylistChangesBeforePreparation() throws InterruptedException { 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 void testPlaylistWithLazyMediaSource() throws InterruptedException { 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 void testPlaylistWithLazyMediaSource() throws InterruptedException { 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 void testPlaylistWithLazyMediaSource() throws InterruptedException { 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 void onPrepared(MediaPeriod mediaPeriod) { 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 void onContinueLoadingRequested(MediaPeriod source) {} 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 DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource } - 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 1f2524110a9..f4c84358010 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 @@ */ 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 FakeMediaSource(Timeline timeline, Object manifest, TrackGroupArray track 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 void maybeThrowSourceInfoRefreshError() throws IOException { @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 void releaseSource() { 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 4bb5cda5f1c48075675c45ba49ff29057ab19f8f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 02:19:48 -0800 Subject: [PATCH 011/105] 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 e7b2a8d963b..96d11678c99 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.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 void testCustomCallbackAfterPreparationAddSingle() throws InterruptedExce 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 void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedEx 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 void testCustomCallbackAfterPreparationAddSingleWithIndex() throws Interr 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 void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws Inte 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 void testCustomCallbackAfterPreparationRemove() throws InterruptedExcepti 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 void run() { }); 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 void testCustomCallbackAfterPreparationMove() throws InterruptedException 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 void run() { }); 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 void testPeriodCreationWithAds() throws InterruptedException { 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); - } - }); + 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 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 @@ private static FakeTimeline createFakeTimeline(int index) { 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 boolean handleMessage(Message msg) { } 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 4d53a6c89de..4a5beb0501f 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 @@ protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trac 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 boolean getPlayWhenReady() { 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 boolean isLoading() { 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; @@ -357,16 +322,6 @@ public void run() { }); } - @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 88b5de7f65d..7cae7094385 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 00000000000..e03f6fbad95 --- /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 57868092eaec2cb02ba6bd74407d0621ec08e734 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 15 Nov 2017 03:08:11 -0800 Subject: [PATCH 012/105] 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/demo/PlayerActivity.java | 5 +- .../exoplayer2/source/hls/HlsMediaSource.java | 136 +++++++++++++++++- 2 files changed, 138 insertions(+), 3 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 65e1c0e0831..3d669c94775 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { .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 21b27e655de..3f28981f0e5 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 HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Han * @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 HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, * @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 d49fc54968b33c027ca205597a81c1f36cf110ec Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Nov 2017 09:46:28 -0800 Subject: [PATCH 013/105] 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 73c0bc20be2..21806014810 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.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 InvalidAudioTrackTimestampException(String detailMessage) { 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 DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, drainingAudioProcessorIndex = C.INDEX_UNSET; this.audioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; - playbackParametersCheckpoints = new LinkedList<>(); + playbackParametersCheckpoints = new ArrayDeque<>(); } @Override From 108900cf5292211e2dc54c1b22ba1fd46cf63ea0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 01:07:51 -0800 Subject: [PATCH 014/105] 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 | 4 + .../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, 124 insertions(+), 109 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 579c2a92ac0..cca26f80636 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Release notes # +### dev-v2 (not yet released) ### + +* Support 32-bit PCM float output from `DefaultAudioSink`. + ### 2.6.0 ### * Removed "r" prefix from versions. This release is "2.6.0", not "r2.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 9d4049ada9e..592589e2212 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 @@ private 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 @@ private 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 @@ private 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 5408032907e..faf3160018b 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 @@ * 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 WriteException(int errorCode) { 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 WriteException(int errorCode) { /** * 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 WriteException(int errorCode) { * 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 @@ void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, * 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 21806014810..0d3365b5d86 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 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 InvalidAudioTrackTimestampException(String detailMessage) { */ 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 void setListener(Listener listener) { } @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 long getCurrentPositionUs(boolean sourceEnded) { } @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 void configure(String inputMimeType, int inputChannelCount, int inputSamp if (flush) { resetAudioProcessors(); } - } else { - encoding = getEncodingForMimeType(inputMimeType); } int channelConfig; @@ -411,11 +409,11 @@ public void configure(String inputMimeType, int inputChannelCount, int inputSamp // 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 void configure(String inputMimeType, int inputChannelCount, int inputSamp 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 void configure(String inputMimeType, int inputChannelCount, int inputSamp // 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 @@ private void initialize() throws InitializationException { 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 boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) 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 boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) } } - 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 @@ private void processBuffers(long avSyncPresentationTimeUs) throws WriteException } @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 @@ private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) } 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 @@ private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) 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 void playToEndOfStream() throws WriteException { 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 boolean hasPendingData() { @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 @@ private void maybeSampleSyncParams() { 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 @@ private long durationUsToFrames(long durationUs) { } 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 @@ private AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { 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 f8206e94cf8..18cbcea115f 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 @@ protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, * @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 @@ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackExceptio @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 @@ protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) } 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 98a84fdff8d..6be4b1d35d3 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 @@ private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderExc 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 2daf16d3d2c..d48d28caa56 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 static String getMediaMimeType(String codec) { } /** - * 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 static int getTrackType(String mimeType) { } } + /** + * 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 a79ed387559..5b2de1042e3 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 @@ -801,6 +801,7 @@ public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCou case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: return channelCount * 4; case C.ENCODING_INVALID: case Format.NO_VALUE: From 820a4459448176a9b06a64983127d92a52b4e0b3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 02:03:02 -0800 Subject: [PATCH 015/105] 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 cca26f80636..d2d0105b55f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,8 @@ ### dev-v2 (not yet released) ### -* 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 ed8a5b0eac2..3e23659bf8f 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.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 FfmpegAudioRenderer() { */ 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 @@ protected int supportsFormatInternal(DrmSessionManager drmSessio 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 int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { 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 @@ protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) 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 2af2101ee7e..8807738cfa7 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 @@ /* 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 @@ 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 @@ public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer, } 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 @@ public int getSampleRate() { 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 @@ private static byte[] getExtraData(String mimeType, List initializationD } } - 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 fa615f2ec1c..d077c819abc 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 dc376d2ea49..a72b03cd444 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 @@ protected int supportsFormatInternal(DrmSessionManager drmSessio 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 e4745d0c29c..b94f3e93321 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 @@ protected int supportsFormatInternal(DrmSessionManager drmSessio 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 faf3160018b..6bb5bf7d8ee 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 @@ interface Listener { * * @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 WriteException(int errorCode) { 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 0d3365b5d86..ba62ac126e4 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 void setListener(Listener listener) { } @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 18cbcea115f..25ad847f7e1 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 @@ protected int supportsFormat(MediaCodecSelector mediaCodecSelector, && 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 @@ protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, /** * 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 6be4b1d35d3..d9ad5491048 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 final int supportsFormat(Format format) { 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 d48d28caa56..c29a4c37176 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 static int getTrackType(String mimeType) { } /** - * 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 f5a3a277263aaa05057ac06100ad8d997ce7fbf2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Nov 2017 05:08:44 -0800 Subject: [PATCH 016/105] 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 --- .../google/android/exoplayer2/source/ExtractorMediaPeriod.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 1228061cdec..d43b2d87b20 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 @@ public long getNextLoadPositionUs() { @Override public long readDiscontinuity() { - if (notifyDiscontinuity) { + if (notifyDiscontinuity + && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) { notifyDiscontinuity = false; return lastSeekPositionUs; } From 1f70d3cdd7b8487c8754f303932cd16a9e7c2e42 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 17 Nov 2017 03:08:55 -0800 Subject: [PATCH 017/105] 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 | 2 + .../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 +- 8 files changed, 146 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d2d0105b55f..2a5ccb583cc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, + DashMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. 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 e11c840d123..6b840830c5e 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.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 @@ public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { 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 3d669c94775..ca253db809f 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.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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { .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 65fb4c81954..bd6e698dc6e 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 void run() { 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 591f43f38a1..aa61df74d9a 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 void run() { 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 c2c1867a900..746f3d273f9 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 void run() { 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 1b3f6cb95c0..066953b9987 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 interface EventListener { 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 interface EventListener { * 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 ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, * @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 ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, * 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 18aa8a63e7e..397b8effd30 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 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { 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 bb4be4314a942998e895114e471bd38c3e2ce62e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 17 Nov 2017 05:26:58 -0800 Subject: [PATCH 018/105] 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 | 74 ++++++++++++++++++- 3 files changed, 88 insertions(+), 36 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 0edd19bc09c..95d5d961633 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.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 void testPlayEmptyTimeline() throws Exception { * 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 void testPlaySinglePeriodTimeline() throws Exception { * 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 void testPlayMultiPeriodTimeline() throws Exception { * 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 boolean isEnded() { } 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 void run() { } 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 void testRepeatModeChanges() throws Exception { } 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 void testShuffleModeEnabledChanges() throws Exception { } 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 void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Excepti .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 void onSeekProcessed() { } 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 void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Ex } 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 void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Ex 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 void run() { 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 a1f8fc78610..591e63dc5b1 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.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 @@ SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, /** * 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 SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, } 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 2937ee27708..4a9d79f906d 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 static final class TimelineWindowDefinition { public final int adGroupsPerPeriodCount; public final int adsPerAdGroupCount; + /** + * 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, WINDOW_DURATION_US); + 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 TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, 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 int getIndexOfPeriod(Object uid) { 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 14299e0643cca287e3c4920c258ef9340e7ba38f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:18:06 -0800 Subject: [PATCH 019/105] 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 2c8deb74b46..79f646b5c44 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 @@ */ 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 1b7c950d1e6639e2a2f10140598d6646c8257122 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:22:17 -0800 Subject: [PATCH 020/105] 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 | 31 +- .../DynamicConcatenatingMediaSourceTest.java | 561 ++++++++---------- .../testutil/MediaSourceTestRunner.java | 290 +++++++++ .../android/exoplayer2/testutil/TestUtil.java | 1 + .../exoplayer2/testutil/TimelineAsserts.java | 50 -- 5 files changed, 573 insertions(+), 360 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 6f6556225e2..429325defc9 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.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 @@ */ 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 void testPeriodCreationWithAds() throws InterruptedException { 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)); + 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. + 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 96d11678c99..536180fafcb 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 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 void testPlaylistChangesAfterPreparation() throws InterruptedException { 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 void testPlaylistChangesBeforePreparation() throws InterruptedException { 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 void testPlaylistChangesBeforePreparation() throws InterruptedException { 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 void onContinueLoadingRequested(MediaPeriod source) {} } } - 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 void testEmptyTimelineMediaSource() throws InterruptedException { 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 void testEmptyTimelineMediaSource() throws InterruptedException { 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 void testIllegalArguments() { } public void testCustomCallbackBeforePreparationAddSingle() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -402,7 +386,6 @@ public void testCustomCallbackBeforePreparationAddSingle() { } public void testCustomCallbackBeforePreparationAddMultiple() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSources(Arrays.asList( @@ -411,7 +394,6 @@ public void testCustomCallbackBeforePreparationAddMultiple() { } public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); @@ -419,134 +401,159 @@ public void testCustomCallbackBeforePreparationAddSingleWithIndex() { } 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(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationRemove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); + + 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(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, - runnable); - } - }); - waitForCustomRunnable(); + 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(); + + 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 void testPeriodCreationWithAds() throws InterruptedException { 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 void testPeriodCreationWithAds() throws InterruptedException { 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 @@ private static FakeTimeline createFakeTimeline(int index) { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } - private static class DynamicConcatenatingMediaSourceAndHandler { + private static final class DummyMainThread { + + private final HandlerThread thread; + private final Handler handler; + + private DummyMainThread() { + thread = new HandlerThread("DummyMainThread"); + thread.start(); + handler = new Handler(thread.getLooper()); + } - public final DynamicConcatenatingMediaSource mediaSource; - public final Handler mainHandler; + /** + * 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 DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler mainHandler) { - this.mediaSource = mediaSource; - this.mainHandler = mainHandler; + 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; + + private Timeline timeline; + private AssertionError error; - public MessageHandlingExoPlayer() { - HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper(), this); + public TimelineGrabber(MediaSourceTestRunner testRunner) { + this.testRunner = testRunner; + finishedCondition = new ConditionVariable(); } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); + public void run() { + try { + timeline = testRunner.assertTimelineChange(); + } catch (AssertionError e) { + error = e; + } + finishedCondition.open(); } - @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 Timeline assertTimelineChangeBlocking() { + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (error != null) { + throw error; } - return true; + 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 00000000000..df1282c7e1a --- /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 61d1ecaeea7..9ee181024c0 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 static String getString(Instrumentation instrumentation, String 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; 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 b1df8f62e15..62af44f32f3 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 static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroup } } - /** - * 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 09f3055badc4ac99cd3a7639cef9a099891483a9 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 20 Nov 2017 02:41:38 -0800 Subject: [PATCH 021/105] 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 2a5ccb583cc..6683ee3f552 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. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. 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 dd901958fd7..2aa8ccc7125 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 interface EventListener { } + /** + * 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 interface EventListener { * 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 SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Fo * @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 SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Fo * @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 c20868ef00c..fa3e14f1c93 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 dc425a942c3cc6181113a3aa0549665af64825ea Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 02:49:39 -0800 Subject: [PATCH 022/105] 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 54a5086d3b0..02f928544b6 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 static final class Builder { * @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 2ad87f4f5b08a8f201b2ed750ac028933aaf46ff Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 03:17:29 -0800 Subject: [PATCH 023/105] 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 7d77ae82d95..dfcb9e72a55 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 void testCopySkipPeriod() throws Exception { } 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 02f928544b6..a82b5af5831 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 DashMediaSource build() { 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 @@ private void processManifest(boolean scheduleRefresh) { 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 @@ private void processManifest(boolean scheduleRefresh) { 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 @@ private void processManifest(boolean scheduleRefresh) { 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 @@ private void scheduleManifestRefresh() { 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 66455b2f042..b254c4f09a1 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 void getNextChunk(MediaChunk previous, long playbackPositionUs, long load 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 cd02e27fce4..6cc93975965 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.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 final Period getPeriod(int index) { 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); } @@ -110,10 +143,10 @@ public final DashManifest copy(List representationKeys) { copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); } } - 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 82ce68009cf84e681272cd628289b91e06af0097 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:20:52 -0800 Subject: [PATCH 024/105] 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 197dec80a53..0b6f9a587c0 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 a65bb0951b0..bd81750fcb5 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.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 4c6a42849fe..f92574b7ab1 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.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 22ae57932bb..02b29a31b5a 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.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 @@ private static DrmInitData newDrmInitData() { 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 472b5c724ba..f40ae0bc7e1 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 @@ 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 @@ private CacheSpan newCacheSpan(int position, int length) throws IOException { 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 8532e65a685..ec0292514a6 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.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 e7cd9baf59d..6bd1048bc00 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 static void setUpMockito(InstrumentationTestCase instrumentationTestCase) 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 c3b92f84562aa55dc79d764f6fa4cd2e827f39e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 04:32:56 -0800 Subject: [PATCH 025/105] 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 6683ee3f552..f772bc9f19d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ DashMediaSource, SingleSampleMediaSource. * 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 e1a70e2579e..e9ffab7acec 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 static Format parseEAc3AnnexFFormat(ParsableByteArray data, String trackI 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 static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { 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 static Ac3SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { } 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 6a1c566faf7..8383bfb8d24 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 f75ce5a9e56..7ae8eb3cd44 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.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 static MediaCodecInfo getPassthroughDecoderInfo() { * 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 static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) 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 static Pair getCodecProfileAndLevel(String codec) { // 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 @@ private static List getDecoderInfosInternal( 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 @@ private static List getDecoderInfosInternal( /** * 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 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S 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 c29a4c37176..a68e0142d69 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 static String getMediaMimeType(String codec) { 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 static int getTrackType(String mimeType) { 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 137e29c5ab5..aa4c6b1e303 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 @@ -452,6 +452,7 @@ protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseU String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -479,12 +480,14 @@ protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseU } } 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, @@ -494,9 +497,12 @@ protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseU 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); @@ -900,6 +906,18 @@ protected static int parseCea708AccessibilityChannel( 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 790316688657c5d9b7fa8a2a6bce58a1ee833f39 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 06:22:54 -0800 Subject: [PATCH 026/105] 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 f9e9c34158a..5ff7c5cb406 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 ca253db809f..efde7751764 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 @@ 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 @@ private void initializePlayer() { 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 @@ private void initializePlayer() { 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 1f84b1f29c8..308bab2a3b3 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.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 @@ private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOExc 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 @@ private SampleGroup getGroup(String groupName, List groups) { 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 DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl, 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 5c0e1f3e8aa057d419d87db403bf7edaf5438563 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:48:10 -0800 Subject: [PATCH 027/105] 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 5e615dbc7f5..3c870f06f4a 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.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 void testWindowAndPeriodIndices() { * 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 429325defc9..1ca32be46d2 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.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 @@ */ 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 void testPeriodCreationWithAds() throws InterruptedException { 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 @@ private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, } 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 536180fafcb..16c9e1a17cd 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 @@ */ 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 void run() { finishedCondition.open(); } }); - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); } public void release() { @@ -656,7 +656,7 @@ public void run() { } 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 79f646b5c44..6f69923ea23 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.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 void testEmptyTimelineLoop() { * 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 df1282c7e1a..235c04bef51 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.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 @@ */ 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 MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long * @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 Timeline assertTimelineChange() { */ 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 void assertPrepareAndReleaseAllPeriods() { 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 9ee181024c0..d10b8a8269e 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.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 static String getString(Instrumentation instrumentation, String fileName) 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 fa5cc5be5104094a7368cdbc039351cc1003f418 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:50:09 -0800 Subject: [PATCH 028/105] Bump target API level to 27 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366693 --- constants.gradle | 4 ++-- 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 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/constants.gradle b/constants.gradle index 2a7754d65cb..bad69389a53 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/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5252d2feeb0..f14feeda745 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 d041e24d805..ec8016e8a37 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + - + - + - + - + - + - + - + - + - + Date: Tue, 21 Nov 2017 10:16:50 -0800 Subject: [PATCH 029/105] 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 dfcb9e72a55..882b0eb3747 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 @@ private static void assertManifestEquals(DashManifest expected, DashManifest act 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 @@ private static Representation newRepresentation() { } 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 6cc93975965..cbfd0a5951f 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 DashManifest(long availabilityStartTimeMs, long durationMs, long minBuffe 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; @@ -145,8 +152,8 @@ public final DashManifest copy(List representationKeys) { } 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 aa4c6b1e303..9c50c6cf30f 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 @@ -115,6 +115,7 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, ? 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; @@ -167,17 +168,17 @@ protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, } 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 d5b79f3a43ec2b996c71736b4b31fa8c34b30a84 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 13:22:41 -0800 Subject: [PATCH 030/105] 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 2623db66fc2..9f9081a9450 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 cf3ab9051df177335eacfe14076b5dfb08448801 Mon Sep 17 00:00:00 2001 From: simophin Date: Fri, 24 Nov 2017 17:27:35 +1300 Subject: [PATCH 031/105] 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 5ca8675dd91..83167c152f4 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 @@ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, Inte 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 b19512fb20e5f832066067480d9c9151e62ba964 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 02:20:35 -0800 Subject: [PATCH 032/105] 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 591e63dc5b1..30e0214b623 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 @@ -517,7 +517,7 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { @Override public void onPlayerError(ExoPlaybackException error) { - handleException(exception); + handleException(error); } @Override From 6c1f562230f87bd9882a069d2f1b92fd00d31e9a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 10:00:44 -0800 Subject: [PATCH 033/105] 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 262d120af86..058a5d6dd20 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 synchronized void block() throws InterruptedException { } /** - * 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 86d91a59a0eb15d19952e01f09038d6f050874d0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:45:24 -0800 Subject: [PATCH 034/105] 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 4a5beb0501f..f6f56ead77c 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 PlaybackParameters getPlaybackParameters() { @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(); @@ -525,6 +511,20 @@ private void releaseMedia() { } } + 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 ce557b11fcec3515fdd4315ab8deb5001e379f48 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:57:45 -0800 Subject: [PATCH 035/105] 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 f6f56ead77c..d8f71535dae 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 @@ -511,7 +511,7 @@ private void releaseMedia() { } } - private void stop(boolean quitPlaybackThread) { + private void stop(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { From c1c892f5ec5546257d1da1b125fc4c3466daede2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 27 Nov 2017 06:29:47 -0800 Subject: [PATCH 036/105] 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 | 3 + .../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, 180 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f772bc9f19d..ae5bc0fb95d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,9 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* 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 592589e2212..6a35c0c5e86 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 @@ private 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 c789caded4a..0029cdbd315 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 @@ * 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 static final class Parameters { // 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 static final class Parameters { *

*/ 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 Parameters() { * @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 Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { 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 Parameters withPreferredTextLanguage(String preferredTextLanguage) { 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 Parameters withForceLowestBitrate(boolean forceLowestBitrate) { 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 Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiven 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 Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdapt 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 Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { 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 Parameters withMaxVideoBitrate(int maxVideoBitrate) { 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 Parameters withExceedVideoConstraintsIfNecessary( 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 Parameters withExceedRendererCapabilitiesIfNecessary( 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 Parameters withViewportSize(int viewportWidth, int viewportHeight, && 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 @@ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatS 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 @@ protected static boolean isSupported(int formatSupport, boolean allowExceedsCapa && 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 5b2de1042e3..24c5f4036de 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.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; @@ -238,13 +239,18 @@ public static void closeQuietly(Closeable closeable) { } /** - * 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 a0e499139c9..b2b149b004d 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 void testSelectTracksExceedingCapabilitiesPreferLowerSampleRateBeforeBitr .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 @@ private static TrackGroupArray singleTrackGroup(Format... formats) { 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 1861aea2b3159537e11d7e5329210dede110e048 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 07:02:33 -0800 Subject: [PATCH 037/105] 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 dc703f924ac..d911f83392e 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 @@ -368,6 +368,8 @@ public void onSeekProcessed() { * @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 23a7f2d994007f59bd88106e35db45b93f78fcb5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 12:42:53 -0800 Subject: [PATCH 038/105] 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 83167c152f4..1ad5acc5c55 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 @@ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, Inte 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 d3fd2d1b872d2584453a60b5aaf3fe8ef502f48e Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 28 Nov 2017 17:02:04 +0000 Subject: [PATCH 039/105] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1c..e85c0c28c76 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 e175bf9e4271bae6c85bc49ff2d51897660de5c2 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:45:54 +0100 Subject: [PATCH 040/105] 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 a82b5af5831..edd941c1dd2 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 @@ -934,19 +934,39 @@ public Long parse(Uri uri, InputStream inputStream) throws IOException { 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 ee05b60a19ef9bb1e43d9e7d337a41552f3f9f78 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:52:12 +0100 Subject: [PATCH 041/105] 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 edd941c1dd2..f1ee8130209 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 @@ -935,10 +935,9 @@ public Long parse(Uri uri, InputStream inputStream) throws IOException { 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 { @@ -947,10 +946,10 @@ public Long parse(Uri uri, InputStream inputStream) throws IOException { 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; } @@ -967,6 +966,7 @@ public Long parse(Uri uri, InputStream inputStream) throws IOException { throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } + } + } From 1d96492c1e370cf5c0792c987d106ea579285856 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 13:32:44 -0800 Subject: [PATCH 042/105] 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 1ad5acc5c55..c4e54d4bd3f 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 @@ private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, Inte 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 7a031980281555edd99d353469a7fdc5ea7fec22 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 03:58:30 -0800 Subject: [PATCH 043/105] 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 7288b398974..4a0d8e196da 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 @@ * 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 int hashCode() { /** * 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 int hashCode() { /** * 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 int hashCode() { * 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 int hashCode() { /** * Releases the period. + *

+ * Should not be called directly from application code. * * @param mediaPeriod The period to release. */ @@ -186,8 +195,7 @@ public int hashCode() { /** * 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 0e0300b802bc10e210b0e36db18ecf35ecd30af2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 08:40:21 -0800 Subject: [PATCH 044/105] 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 218e6ffd82d..30b66d65fd9 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.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 Extractor[] createExtractors() { }; - // 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 Extractor[] createExtractors() { 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 FlvExtractor() { 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 void init(ExtractorOutput output) { @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 void release() { 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 int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce return RESULT_CONTINUE; } break; + default: + // Never happens. + throw new IllegalStateException(); } } } @@ -199,7 +211,7 @@ private boolean readFlvHeader(ExtractorInput input) throws IOException, Interrup // 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 @@ private boolean readFlvHeader(ExtractorInput input) throws IOException, Interrup 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 @@ private boolean readTagHeader(ExtractorInput input) throws IOException, Interrup 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 @@ private boolean readTagData(ExtractorInput input) throws IOException, Interrupte 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 5aefd041c41..4b0bbda2756 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.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 a4349ada091..dc7d21851ae 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.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 5e8d72f18d2..9b1158dfa86 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 @@ public long getPosition(long timeUs) { 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 e86157dd922..4bc1b044185 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.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 f23af98e7f2..f2412bf4ba6 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.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 5470e2badce..77def572757 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 @@ public long getNextSeekPosition(long targetGranule, ExtractorInput input) 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 f4da6e3960d..304fb3dd963 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 @@ private int getFlacFrameBlockSize(ParsableByteArray packet) { 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 54e168c6656..a4d8f97d5bb 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 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 7840eafce65..aa77aba30ea 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 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 4d54600c6df..bc37277c574 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 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 5ce15952a51..a0a748660ef 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 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 69c5745eaa2..f3aad6ba6b0 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 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 213d30d47d4..13e669da230 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.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 cb46aa55195..cb9a2653d7e 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.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 a8d4ad94f4011ec310e3d3359d06d405bd9ef37e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 09:22:32 -0800 Subject: [PATCH 045/105] 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 0029cdbd315..49b8e8964be 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 Parameters withPreferredTextLanguage(String preferredTextLanguage) { /** * 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 b2b149b004d..6b14d139ae3 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 void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackExc 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 void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackExc 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 9eed1150e083a224a732506ed9db66bd3699f989 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 09:43:08 -0800 Subject: [PATCH 046/105] 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 30b66d65fd9..2da075ff53e 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 @@ /** * 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 Extractor[] createExtractors() { // 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 @@ private boolean readFlvHeader(ExtractorInput input) throws IOException, Interrup 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 @@ private boolean readTagHeader(ExtractorInput input) throws IOException, Interrup 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 @@ private ParsableByteArray prepareTagData(ExtractorInput input) throws IOExceptio 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 1a4f8f3e88b..2dec85ffcc9 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 @@ 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 @@ 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 cb9a2653d7e..4f2be71a692 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.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 @@ /** * 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 int read(ExtractorInput input, PositionHolder seekPosition) 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 int read(ExtractorInput input, PositionHolder seekPosition) 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 a57060f604a..1c1fc97a22a 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 @@ public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, i 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 @@ public int getNumChannels() { 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 c12349e2c937ed6bd9fcea6b9ac47b9031f62bda Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Nov 2017 09:53:30 -0800 Subject: [PATCH 047/105] Allow setting supported formats on AdsLoaders ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177175377 --- .../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 +-- 5 files changed, 39 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 5b61db0264a..cf8b8a3f6d2 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.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 com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { // 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 void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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 241750a21fc..99feccd2f35 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 @@ interface EventListener { } + /** + * 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 397b8effd30..202e31cba10 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 AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSou 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 a68e0142d69..8307e998a09 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 f1ee8130209..c5fbafb84e4 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 @@ -877,8 +877,7 @@ private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProject } - private final class ManifestCallback implements - Loader.Callback> { + private final class ManifestCallback implements Loader.Callback> { @Override public void onLoadCompleted(ParsingLoadable loadable, From 63dbf56b6c0bcd69d8e218a7aa55e3214c94b130 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 10:45:38 -0800 Subject: [PATCH 048/105] 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 efde7751764..cf0f8b8dc89 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 @@ private void initializePlayer() { 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 5a5a948d58b..1374b73709a 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 @@ void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, 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 @@ void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, 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 @@ protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector track 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, @@ -576,18 +578,64 @@ public void clearMetadataOutput(MetadataOutput output) { * 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 @@ -877,7 +925,7 @@ private final class ComponentListener implements VideoRendererEventListener, @Override public void onVideoEnabled(DecoderCounters counters) { videoDecoderCounters = counters; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoEnabled(counters); } } @@ -885,7 +933,7 @@ public void onVideoEnabled(DecoderCounters counters) { @Override public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -894,14 +942,14 @@ public void onVideoDecoderInitialized(String decoderName, long initializedTimest @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); } } @@ -913,7 +961,7 @@ public void onVideoSizeChanged(int width, int height, int unappliedRotationDegre videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -926,14 +974,14 @@ public void onRenderedFirstFrame(Surface surface) { 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; @@ -945,7 +993,7 @@ public void onVideoDisabled(DecoderCounters counters) { @Override public void onAudioEnabled(DecoderCounters counters) { audioDecoderCounters = counters; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioEnabled(counters); } } @@ -953,7 +1001,7 @@ public void onAudioEnabled(DecoderCounters counters) { @Override public void onAudioSessionId(int sessionId) { audioSessionId = sessionId; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } } @@ -961,7 +1009,7 @@ public void onAudioSessionId(int sessionId) { @Override public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -970,7 +1018,7 @@ public void onAudioDecoderInitialized(String decoderName, long initializedTimest @Override public void onAudioInputFormatChanged(Format format) { audioFormat = format; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioInputFormatChanged(format); } } @@ -978,14 +1026,14 @@ public void onAudioInputFormatChanged(Format format) { @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 ee4018ba0e8..5ff0533f714 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 final void setEventListener(Player.EventListener eventListener) { } } + /** + * 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 final void onStart(HostActivity host, Surface surface) { 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 91b324f6b970c8b036289872e03841ba1754ab64 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 08:53:27 -0800 Subject: [PATCH 049/105] 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 9b1158dfa86..55888066e71 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 @@ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArr } 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 @@ public long getPosition(long timeUs) { 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 @@ public long getTimeUs(long position) { 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 @@ public long getDurationUs() { * 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 800cfeea3d18d2f7d1e8198c2a483460413fa08f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 09:09:17 -0800 Subject: [PATCH 050/105] 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 778aa4d7151..964c43a45a3 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 long getDurationUs() { @Override public long getPosition(long timeUs) { - return 0; + return startPosition; } } @@ -78,7 +89,8 @@ public long getPosition(long timeUs) { * * @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 df7748a9108..47e12161a87 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 @@ public boolean isSeekable() { @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 4bc1b044185..28a1ffaa7b9 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 @@ private boolean readAtomHeader(ExtractorInput input) throws IOException, Interru 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 f0f726dfa9a6cdcb01a33d6ed24140fbc471ab44 Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Wed, 29 Nov 2017 16:59:41 -0800 Subject: [PATCH 051/105] 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 10c17e2888d..a78c5afa78d 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 @@ * @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 @@ 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 295fdc656f9..e16b231f7e6 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 @@ /** * 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 @@ public boolean isNormalCue() { * 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 54af4dbf634..80ebecdc0e9 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 @@ /** * 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 @@ public WebvttCueParser() { * @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 2567bf51fc10ee8466d4aefbe062f5c01e5add15 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 00:33:10 -0800 Subject: [PATCH 052/105] 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 9233b016f5f..4819c287531 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.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 @@ */ /* 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; @@ -369,6 +371,23 @@ public void onDownstreamFormatChanged(int trackType, Format trackFormat, int tra // 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 cf0f8b8dc89..7d0975a7505 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 @@ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) // 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 9526e8586adedc8022abf1b4b30883efe3c69ea5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:25:28 -0800 Subject: [PATCH 053/105] 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 | 108 ------------- .../exoplayer2/source/ads/AdsMediaSource.java | 143 +++++++++++++++--- 3 files changed, 259 insertions(+), 131 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 00000000000..bc29b2fdf1c --- /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 e80abad3ef9..b66e5ebe095 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.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,4 @@ public int getIndexOfPeriod(Object uid) { } - /** - * 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) { - mediaPeriod.discardBuffer(positionUs); - } - - @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 202e31cba10..47a2540c38d 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 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 interface AdsListener { 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 interface AdsListener { /** * 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 AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSou /** * 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 AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSou 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { 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 @@ private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { 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 @@ private void maybeUpdateSourceInfo() { * 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 void run() { } + /** + * 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 315a6c3558024038f3ca736844cb67534ab2806f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:27:07 -0800 Subject: [PATCH 054/105] 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 bf822d9db4a..95f6528fd64 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 9d3755b23b9..ebd33133e22 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 4e8b9282f518ea2568290c1a9b8655b0b8ca1d92 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 02:57:07 -0800 Subject: [PATCH 055/105] 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 941b413c090..649e4a6ee2f 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 8fbc2a5c9b0f9904eef67fb77792e3b42cf904b3 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 05:34:51 -0800 Subject: [PATCH 056/105] 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 0b6516ccdba..37a04215eee 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 0b6516ccdba..37a04215eee 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 0b6516ccdba..37a04215eee 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 47e12161a87..e02e99e1399 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 @@ 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 dc7d21851ae..7c579504c39 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 @@ private Seeker getConstantBitrateSeeker(ExtractorInput input) 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 1c1fc97a22a..2cdd31cb6fa 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 @@ 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 @@ public long getDurationUs() { @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 022b85a625468235075de679cc19374fca823d8a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 05:39:50 -0800 Subject: [PATCH 057/105] 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 ae5bc0fb95d..a123c783233 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* 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 ba62ac126e4..eb27c0fe55d 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 void configure(@C.Encoding int inputEncoding, int inputChannelCount, int encoding = audioProcessor.getOutputEncoding(); } } - if (flush) { - resetAudioProcessors(); - } } int channelConfig; @@ -492,6 +489,9 @@ private void initialize() throws InitializationException { // 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 0ea8c8bfa0db99fb2322e5ed2b3c9fb66c2bf9f7 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 07:20:45 -0800 Subject: [PATCH 058/105] 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 2e0b21050cc..7b6fe9db377 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 14EF6AFD + data = length 384, hash F7E344F4 sample 1: - time = 969782 + time = 952567 flags = 1 - data = length 384, hash 61C9B92C + data = length 384, hash 14EF6AFD sample 2: - time = 993782 + time = 976567 flags = 1 - data = length 384, hash ABE1368 + data = length 384, hash 61C9B92C sample 3: - time = 1017782 + time = 1000567 flags = 1 - data = length 384, hash 6A3B8547 + data = length 384, hash ABE1368 sample 4: - time = 1041782 + time = 1024567 flags = 1 - data = length 384, hash 30E905FA + data = length 384, hash 6A3B8547 sample 5: - time = 1065782 + time = 1048567 flags = 1 - data = length 384, hash 21A267CD + data = length 384, hash 30E905FA sample 6: - time = 1089782 + time = 1072567 flags = 1 - data = length 384, hash D96A2651 + data = length 384, hash 21A267CD sample 7: - time = 1113782 + time = 1096567 flags = 1 - data = length 384, hash 72340177 + data = length 384, hash D96A2651 sample 8: - time = 1137782 + time = 1120567 flags = 1 - data = length 384, hash 9345E744 + data = length 384, hash 72340177 sample 9: - time = 1161782 + time = 1144567 flags = 1 - data = length 384, hash FDE39E3A + data = length 384, hash 9345E744 sample 10: - time = 1185782 + time = 1168567 flags = 1 - data = length 384, hash F0B7465 + data = length 384, hash FDE39E3A sample 11: - time = 1209782 + time = 1192567 flags = 1 - data = length 384, hash 3693AB86 + data = length 384, hash F0B7465 sample 12: - time = 1233782 + time = 1216567 flags = 1 - data = length 384, hash F39719B1 + data = length 384, hash 3693AB86 sample 13: - time = 1257782 + time = 1240567 flags = 1 - data = length 384, hash DA3958DC + data = length 384, hash F39719B1 sample 14: - time = 1281782 + time = 1264567 flags = 1 - data = length 384, hash FDC7599F + data = length 384, hash DA3958DC sample 15: - time = 1305782 + time = 1288567 flags = 1 - data = length 384, hash AEFF8471 + data = length 384, hash FDC7599F sample 16: - time = 1329782 + time = 1312567 flags = 1 - data = length 384, hash 89C92C19 + data = length 384, hash AEFF8471 sample 17: - time = 1353782 + time = 1336567 flags = 1 - data = length 384, hash 5C786A4B + data = length 384, hash 89C92C19 sample 18: - time = 1377782 + time = 1360567 flags = 1 - data = length 384, hash 5ACA8B + data = length 384, hash 5C786A4B sample 19: - time = 1401782 + time = 1384567 flags = 1 - data = length 384, hash 7755974C + data = length 384, hash 5ACA8B sample 20: - time = 1425782 + time = 1408567 flags = 1 - data = length 384, hash 3934B73C + data = length 384, hash 7755974C sample 21: - time = 1449782 + time = 1432567 flags = 1 - data = length 384, hash DDD70A2F + data = length 384, hash 3934B73C sample 22: - time = 1473782 + time = 1456567 flags = 1 - data = length 384, hash 8FACE2EF + data = length 384, hash DDD70A2F sample 23: - time = 1497782 + time = 1480567 flags = 1 - data = length 384, hash 4A602591 + data = length 384, hash 8FACE2EF sample 24: - time = 1521782 + time = 1504567 flags = 1 - data = length 384, hash D019AA2D + data = length 384, hash 4A602591 sample 25: - time = 1545782 + time = 1528567 flags = 1 - data = length 384, hash 8A680B9D + data = length 384, hash D019AA2D sample 26: - time = 1569782 + time = 1552567 flags = 1 - data = length 384, hash B655C959 + data = length 384, hash 8A680B9D sample 27: - time = 1593782 + time = 1576567 flags = 1 - data = length 384, hash 2168336B + data = length 384, hash B655C959 sample 28: - time = 1617782 + time = 1600567 flags = 1 - data = length 384, hash D77F6D31 + data = length 384, hash 2168336B sample 29: - time = 1641782 + time = 1624567 flags = 1 - data = length 384, hash 524B4B2F + data = length 384, hash D77F6D31 sample 30: - time = 1665782 + time = 1648567 flags = 1 - data = length 384, hash 4752DDFC + data = length 384, hash 524B4B2F sample 31: - time = 1689782 + time = 1672567 flags = 1 - data = length 384, hash E786727F + data = length 384, hash 4752DDFC sample 32: - time = 1713782 + time = 1696567 flags = 1 - data = length 384, hash 5DA6FB8C + data = length 384, hash E786727F sample 33: - time = 1737782 + time = 1720567 flags = 1 - data = length 384, hash 92F24269 + data = length 384, hash 5DA6FB8C sample 34: - time = 1761782 + time = 1744567 flags = 1 - data = length 384, hash CD0A3BA1 + data = length 384, hash 92F24269 sample 35: - time = 1785782 + time = 1768567 flags = 1 - data = length 384, hash 7D00409F + data = length 384, hash CD0A3BA1 sample 36: - time = 1809782 + time = 1792567 flags = 1 - data = length 384, hash D7ADB5FA + data = length 384, hash 7D00409F sample 37: - time = 1833782 + time = 1816567 flags = 1 - data = length 384, hash 4A140209 + data = length 384, hash D7ADB5FA sample 38: - time = 1857782 + time = 1840567 flags = 1 - data = length 384, hash E801184A + data = length 384, hash 4A140209 sample 39: - time = 1881782 + time = 1864567 flags = 1 - data = length 384, hash 53C6CF9C + data = length 384, hash E801184A sample 40: - time = 1905782 + time = 1888567 flags = 1 - data = length 384, hash 19A8D99F + data = length 384, hash 53C6CF9C sample 41: - time = 1929782 + time = 1912567 flags = 1 - data = length 384, hash E47EB43F + data = length 384, hash 19A8D99F sample 42: - time = 1953782 + time = 1936567 flags = 1 - data = length 384, hash 4EA329E7 + data = length 384, hash E47EB43F sample 43: - time = 1977782 + time = 1960567 flags = 1 - data = length 384, hash 1CCAAE62 + data = length 384, hash 4EA329E7 sample 44: - time = 2001782 + time = 1984567 flags = 1 - data = length 384, hash ED3F8C66 + data = length 384, hash 1CCAAE62 sample 45: - time = 2025782 + time = 2008567 flags = 1 - data = length 384, hash D3D646B6 + data = length 384, hash ED3F8C66 sample 46: - time = 2049782 + time = 2032567 flags = 1 - data = length 384, hash 68CD1574 + data = length 384, hash D3D646B6 sample 47: - time = 2073782 + time = 2056567 flags = 1 - data = length 384, hash 8CEAB382 + data = length 384, hash 68CD1574 sample 48: - time = 2097782 + time = 2080567 flags = 1 - data = length 384, hash D54B1C48 + data = length 384, hash 8CEAB382 sample 49: - time = 2121782 + time = 2104567 flags = 1 - data = length 384, hash FFE2EE90 + data = length 384, hash D54B1C48 sample 50: - time = 2145782 + time = 2128567 flags = 1 - data = length 384, hash BFE8A673 + data = length 384, hash FFE2EE90 sample 51: - time = 2169782 + time = 2152567 flags = 1 - data = length 384, hash 978B1C92 + data = length 384, hash BFE8A673 sample 52: - time = 2193782 + time = 2176567 flags = 1 - data = length 384, hash 810CC71E + data = length 384, hash 978B1C92 sample 53: - time = 2217782 + time = 2200567 flags = 1 - data = length 384, hash 44FE42D9 + data = length 384, hash 810CC71E sample 54: - time = 2241782 + time = 2224567 flags = 1 - data = length 384, hash 2F5BB02C + data = length 384, hash 44FE42D9 sample 55: - time = 2265782 + time = 2248567 flags = 1 - data = length 384, hash 77DDB90 + data = length 384, hash 2F5BB02C sample 56: - time = 2289782 + time = 2272567 flags = 1 - data = length 384, hash 24FB5EDA + data = length 384, hash 77DDB90 sample 57: - time = 2313782 + time = 2296567 flags = 1 - data = length 384, hash E73203C6 + data = length 384, hash 24FB5EDA sample 58: - time = 2337782 + time = 2320567 flags = 1 - data = length 384, hash 14B525F1 + data = length 384, hash E73203C6 sample 59: - time = 2361782 + time = 2344567 flags = 1 - data = length 384, hash 5E0F4E2E + data = length 384, hash 14B525F1 sample 60: - time = 2385782 + time = 2368567 flags = 1 - data = length 384, hash 67EE4E31 + data = length 384, hash 5E0F4E2E sample 61: - time = 2409782 + time = 2392567 flags = 1 - data = length 384, hash 2E04EC4C + data = length 384, hash 67EE4E31 sample 62: - time = 2433782 + time = 2416567 flags = 1 - data = length 384, hash 852CABA7 + data = length 384, hash 2E04EC4C sample 63: - time = 2457782 + time = 2440567 flags = 1 - data = length 384, hash 19928903 + data = length 384, hash 852CABA7 sample 64: - time = 2481782 + time = 2464567 flags = 1 - data = length 384, hash 5DA42021 + data = length 384, hash 19928903 sample 65: - time = 2505782 + time = 2488567 flags = 1 - data = length 384, hash 45B20B7C + data = length 384, hash 5DA42021 sample 66: - time = 2529782 + time = 2512567 flags = 1 - data = length 384, hash D108A215 + data = length 384, hash 45B20B7C sample 67: - time = 2553782 + time = 2536567 flags = 1 - data = length 384, hash BD25DB7C + data = length 384, hash D108A215 sample 68: - time = 2577782 + time = 2560567 flags = 1 - data = length 384, hash DA7F9861 + data = length 384, hash BD25DB7C sample 69: - time = 2601782 + time = 2584567 flags = 1 - data = length 384, hash CCD576F + data = length 384, hash DA7F9861 sample 70: - time = 2625782 + time = 2608567 flags = 1 - data = length 384, hash 405C1EB5 + data = length 384, hash CCD576F sample 71: - time = 2649782 + time = 2632567 flags = 1 - data = length 384, hash 6640B74E + data = length 384, hash 405C1EB5 sample 72: - time = 2673782 + time = 2656567 flags = 1 - data = length 384, hash B4E5937A + data = length 384, hash 6640B74E sample 73: - time = 2697782 + time = 2680567 flags = 1 - data = length 384, hash CEE17733 + data = length 384, hash B4E5937A sample 74: - time = 2721782 + time = 2704567 flags = 1 - data = length 384, hash 2A0DA733 + data = length 384, hash CEE17733 sample 75: - time = 2745782 + time = 2728567 + flags = 1 + data = length 384, hash 2A0DA733 + 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 b3cb117cb2a..3f393e768e7 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 e02e99e1399..442e62decaf 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 @@ 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 7c579504c39..5c56dc460ad 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 @@ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, Inte 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 @@ private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, Inte 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 @@ private Seeker getConstantBitrateSeeker(ExtractorInput input) 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 c43f0655922..cc631d9f7ee 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 @@ */ /* 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 @@ public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArr 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 @@ public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArr 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 55888066e71..e532249a648 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 @@ */ /* 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 @@ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArr 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 @@ public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArr // 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 @@ public boolean isSeekable() { @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 @@ public long getDurationUs() { } /** - * 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 b43949b7c28..e644abc7efa 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 void testGetTimeUsAtFirstAudioFrame() { @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 void testGetPositionAtStartOfStream() { @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 b61d09c4166c798bd242cb3fa573de77b579ff88 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 08:36:03 -0800 Subject: [PATCH 059/105] 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 7b6fe9db377..a57894e81e3 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 393a7625630e50dcbb84b7c653d4d61f1168725e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 01:18:58 -0800 Subject: [PATCH 060/105] 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 a123c783233..3a73a9e7169 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * 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 4819c287531..b9be8f3846b 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.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.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; @@ -322,13 +323,6 @@ public void onDrmKeysLoaded() { 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 02aa4807a56..cd646daf422 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 ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory data } /** - * 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 ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory data * @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 be07cbb5dcc..2bc9d487260 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 { - - /** - * 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); +@Deprecated +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - /** - * 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 d43b2d87b20..e3c9012dbce 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 @@ 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.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 @@ interface Listener { 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 @@ interface Listener { * @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 @@ interface Listener { * @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 @@ public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, 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(); @@ -607,17 +626,6 @@ private boolean isLoadableExceptionFatal(IOException e) { 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; @@ -664,7 +672,9 @@ public int skipData(long positionUs) { 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) { @@ -700,7 +710,8 @@ public void load() throws IOException, InterruptedException { 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; } @@ -724,6 +735,7 @@ public void load() throws IOException, InterruptedException { 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 066953b9987..3ab2609c0e9 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 @@ 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 @@ * 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 interface EventListener { 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 static final class Builder { 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 Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckInte * @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 ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, 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 void maybeThrowSourceInfoRefreshError() throws IOException { @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 @@ -331,4 +378,94 @@ private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), 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 00000000000..82e8781d70d --- /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 47a2540c38d..54a8fd96ae6 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.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 @@ */ 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 interface AdsListener { 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 AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSou } /** - * 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 AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSou * @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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { 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 @@ private void maybeUpdateSourceInfo() { } } - /** - * 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 void run() { } - /** - * 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 @@ 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. + * @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 ExtractorAdMediaSourceFactory(DataSource.Factory 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(); + 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 ab1542c7a64..cbe971bc5d3 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 @@ 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 DataSpec(Uri uri, @Flags int flags) { * @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 DataSpec(Uri uri, long absoluteStreamPosition, long position, long length } /** - * 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 DataSpec(Uri uri, long absoluteStreamPosition, long position, long length * @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 e759462af8b2d6df4ff2f378cf216e409ae8e7b2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 02:06:42 -0800 Subject: [PATCH 061/105] 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 | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 2 +- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 2 +- .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../testutil/FakeAdaptiveMediaSource.java | 14 +++++--- 15 files changed, 41 insertions(+), 59 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 b9be8f3846b..6fe0f15232a 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.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 @@ MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, - AdaptiveMediaSourceEventListener, + MediaSourceEventListener, AdsMediaSource.EventListener, DefaultDrmSessionManager.EventListener { @@ -323,12 +323,19 @@ public void onDrmKeysLoaded() { 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. } @@ -369,7 +376,7 @@ public void onDownstreamFormatChanged(int trackType, Format trackFormat, int tra @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 2bc9d487260..ccc3beac551 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 3ab2609c0e9..247eacd519c 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.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 82e8781d70d..4d500f94bd4 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 @@ void onDownstreamFormatChanged( 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 EventDispatcher( 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 bb51ae074e3..fa952696906 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 35f3c2e1294..3c401624db9 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 @@ -19,10 +19,10 @@ 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.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 c5fbafb84e4..498c7c6de32 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 @@ -27,9 +27,9 @@ 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.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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; 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 ea9e52e62ea..bd73ad27f96 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.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.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; 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 3f28981f0e5..563f4da0594 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 @@ -22,9 +22,9 @@ 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.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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; 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 ddd6689fa65..beaa84556b1 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 @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; -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 355a8575caf..0677ff7ca0b 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.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 1cc2a6833d4..9b664d8a616 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 @@ 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.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 5a938474285..5a265858749 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 @@ -25,9 +25,9 @@ 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.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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; 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 3dcf5519439..1b2e1af9b7e 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 59bcaf3e7c8..fbb2a830276 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 @@ 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 10b24be6f089b77713e5f1b1fc1c431aa2b83599 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Dec 2017 21:40:58 +0000 Subject: [PATCH 062/105] Fix build --- .../source/DeferredMediaPeriod.java | 4 +-- .../source/dash/DashMediaSource.java | 23 +++++++--------- .../exoplayer2/source/hls/HlsMediaSource.java | 26 +++++++++---------- .../source/smoothstreaming/SsMediaSource.java | 26 ++++++++----------- 4 files changed, 34 insertions(+), 45 deletions(-) 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 bc29b2fdf1c..f93d30cb04d 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 @@ -95,8 +95,8 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF } @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs); } @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 498c7c6de32..548811cf920 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,9 +26,9 @@ 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.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.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; @@ -69,7 +69,7 @@ public static final class Builder { private final DashChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; @@ -151,8 +151,7 @@ public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { * @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; @@ -261,7 +260,7 @@ public DashMediaSource build() { */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -278,8 +277,7 @@ public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourc */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener - eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); } @@ -299,7 +297,7 @@ public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourc @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -325,8 +323,7 @@ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFac @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -353,8 +350,7 @@ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFac public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -363,8 +359,7 @@ private DashMediaSource(DashManifest manifest, Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + 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/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 563f4da0594..4e904032fd2 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,9 +21,9 @@ 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.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.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -57,7 +57,7 @@ public static final class Builder { private HlsExtractorFactory extractorFactory; private ParsingLoadable.Parser playlistParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; private boolean isBuildCalled; @@ -132,7 +132,7 @@ public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { * @return This builder. */ public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -193,13 +193,13 @@ public HlsMediaSource build() { * @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 An {@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) { + MediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -211,14 +211,13 @@ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Han * @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 An {@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) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); @@ -232,16 +231,15 @@ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, * @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 An {@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, - ParsingLoadable.Parser playlistParser) { + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; 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 5a265858749..34343e08e39 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,9 +24,9 @@ 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.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.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; @@ -63,7 +63,7 @@ public static final class Builder { private final SsChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; @@ -143,8 +143,7 @@ public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { * @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; @@ -233,9 +232,9 @@ public SsMediaSource build() { */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + Handler eventHandler, MediaSourceEventListener eventListener) { + this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + eventListener); } /** @@ -250,8 +249,7 @@ public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFacto */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); } @@ -271,7 +269,7 @@ public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFacto @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -295,8 +293,7 @@ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFacto @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -321,8 +318,7 @@ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFacto public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -332,7 +328,7 @@ private SsMediaSource(SsManifest manifest, Uri manifestUri, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null From a8298b4c563b7df7d80125882dd358a73d7a688d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:08:33 -0800 Subject: [PATCH 063/105] 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 3a73a9e7169..51ac077cef8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,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 e2c592be6b6..0483f909b35 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.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 void flush() { 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 @@ private void handlePreambleAddressCode(byte cc1, byte cc2) { 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 @@ private void handleMiscCode(byte cc2) { case CTRL_RESUME_DIRECT_CAPTIONING: setCaptionMode(CC_MODE_PAINT_ON); return; + default: + // Fall through. + break; } if (captionMode == CC_MODE_UNKNOWN) { @@ -484,6 +486,9 @@ private void handleMiscCode(byte cc2) { case CTRL_DELETE_TO_END_OF_ROW: // TODO: implement break; + default: + // Fall through. + break; } } @@ -515,8 +520,13 @@ private void setCaptionMode(int captionMode) { } } + 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 @@ private static class CueBuilder { 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 void reset(int captionMode, int captionRowCount) { 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 29f6351b192b192618bff721f63fc8c14669ea9a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:56:04 -0800 Subject: [PATCH 064/105] Support timezone offsets in ISO8601 timestamps Issue: #3524 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177808106 --- RELEASENOTES.md | 3 + .../source/dash/DashMediaSource.java | 56 ++++++++++--------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 51ac077cef8..a55044ad7e3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, DashMediaSource, SingleSampleMediaSource. +* DASH: + * Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos 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 548811cf920..22bc2b08ecb 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.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; @@ -48,6 +49,8 @@ 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}. @@ -926,41 +929,42 @@ public Long parse(Uri uri, InputStream inputStream) throws IOException { } - 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; - } else { - formatPattern = ISO_8601_FORMAT; + try { + Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); + if (!matcher.matches()) { + throw new ParserException("Couldn't parse timestamp: " + firstLine); } - //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); + // 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 { + 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; } - - } else { - throw new ParserException("Unable to parse ISO 8601. Input value is null"); + return timestampMs; + } catch (ParseException e) { + throw new ParserException(e); } } } - + } From bc7bfb4e7cccdbdb203e3e3af1a07c32e3b47d51 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 07:29:56 -0800 Subject: [PATCH 065/105] 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 cf8b8a3f6d2..00bf0bd4932 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 void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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 13a7037c82a91290757862e5704920e6f89d6392 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 07:35:50 -0800 Subject: [PATCH 066/105] 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 a55044ad7e3..ff0af256cf8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,8 @@ ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * 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 2da075ff53e..d908f28945b 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 Extractor[] createExtractors() { private ExtractorOutput extractorOutput; private @States int state; + private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; @@ -93,6 +94,7 @@ public FlvExtractor() { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -134,6 +136,7 @@ public void init(ExtractorOutput output) { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; bytesToNextTagHeader = 0; } @@ -255,11 +258,11 @@ private boolean readTagHeader(ExtractorInput input) throws IOException, Interrup 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 @@ private ParsableByteArray prepareTagData(ExtractorInput input) throws IOExceptio 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 aef9063e946a0ade455b90aa783220a18cc8c47d Mon Sep 17 00:00:00 2001 From: amesbah Date: Mon, 4 Dec 2017 10:50:51 -0800 Subject: [PATCH 067/105] 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 b21ecb02d50..1f44607f983 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 @@ */ public final class HlsMediaPlaylist extends HlsPlaylist { - /** - * Media segment reference. - */ + /** Media segment reference. */ + @SuppressWarnings("ComparableType") public static final class Segment implements Comparable { /** From 82122b9f3bf994ab326be6dd026b6b54f323c527 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 00:29:56 -0800 Subject: [PATCH 068/105] 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 00bf0bd4932..92ca24c889d 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.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 void detachPlayer() { @Override public void release() { - released = true; + pendingAdRequestContext = null; if (adsManager != null) { adsManager.destroy(); adsManager = null; @@ -308,10 +306,11 @@ public void release() { @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 void onAdError(AdErrorEvent adErrorEvent) { Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } @@ -622,10 +622,16 @@ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // 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 3c0bb7263b73c0472a5274f545b9be8b4f086cca Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 03:53:01 -0800 Subject: [PATCH 069/105] 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 e3c9012dbce..f557d4ac978 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 @@ private boolean suppressRead() { @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 @@ public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, 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 @@ public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, @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 @@ public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, loadable.bytesLoaded, error, /* wasCanceled= */ isErrorFatal); + copyLengthFromLoader(loadable); if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } From 9cb0c2f70292c65e0f430263e210e5ea48c11878 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 04:21:18 -0800 Subject: [PATCH 070/105] 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 1374b73709a..0d724d4fd21 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 @@ -865,15 +865,15 @@ public long getContentPosition() { // 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 02e32a183881490b581528407965d8de4f6469f6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 05:19:48 -0800 Subject: [PATCH 071/105] 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 b09e80c5917..dcc1c625697 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 void setPlayer(SimpleExoPlayer player) { 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 618f2fa3369..d89f82b7c4d 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.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 void onCues(List cues) { /** * 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 39e8f07566a61c4d45d5c8f4003f03d5440ca549 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Dec 2017 10:33:23 -0800 Subject: [PATCH 072/105] 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 0d724d4fd21..544b10b7ef4 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 @@ -726,7 +726,7 @@ public void seekTo(int windowIndex, long positionMs) { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { player.setPlaybackParameters(playbackParameters); } From e7d4524c27820e8bb0b9954b5113364b34eca0a9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 02:45:16 -0800 Subject: [PATCH 073/105] 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 7d0975a7505..0623f48a51d 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 void onClick(View view) { } 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 a4a02f74498c86e65b95c5961b8c794ef4f53f2a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 03:05:22 -0800 Subject: [PATCH 074/105] 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 ff0af256cf8..a7199301d7c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,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 92ca24c889d..58268d56704 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 @@ */ 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 void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGrou adsManager.resume(); } } else { + pendingContentPositionMs = player.getCurrentPosition(); requestAds(); } } @@ -311,19 +312,45 @@ public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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 void onAdEvent(AdEvent adEvent) { // 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(); @@ -740,4 +769,20 @@ private static long[] getAdGroupTimesUs(List cuePoints) { 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 f677d1309d7971d7f7bbdd07e5ca412288ea4c3d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Dec 2017 03:06:54 -0800 Subject: [PATCH 075/105] 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 2d7a9dfd33e..cc504432968 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 @@ private static void assertApiLevel17OrHigher() { */ @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 a99295b364d86d9113addc34ff5ff503343f9d40 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 06:38:39 -0800 Subject: [PATCH 076/105] 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 a7199301d7c..e4a0b40b23c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,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 58268d56704..0fa34e5144a 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 void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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 void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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 88d012bad0fbf482629d8a07d8d221fe735c9628 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 04:44:55 -0800 Subject: [PATCH 077/105] 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 0483f909b35..f018e055fba 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 Cue build() { // 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 3e9c86fcb5826fc3fab0c94bfae317f50ecb0477 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 06:51:51 -0800 Subject: [PATCH 078/105] 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 e4a0b40b23c..c702a22ce65 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,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 dcc1c625697..1f67b83ba09 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 @@ /** * 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 SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr 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 SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr 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 SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr 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 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}. * @@ -784,8 +797,7 @@ public boolean onTrackballEvent(MotionEvent ev) { * 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 void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selectio @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } else { maybeShowController(false); @@ -965,7 +977,7 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { @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 525f95768c5..1ab3854d216 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -47,6 +47,7 @@ + From a0d42b53b734ec8d533ef56681ffad9453903d59 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 07:57:06 -0800 Subject: [PATCH 079/105] 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/demo/PlayerActivity.java | 9 +- .../exoplayer2/source/ads/AdsMediaSource.java | 65 +++---- .../source/dash/DashMediaSource.java | 180 ++++++++++-------- .../playbacktests/gts/DashTestRunner.java | 5 +- 4 files changed, 135 insertions(+), 124 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 0623f48a51d..1be6df8437f 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { .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 54a8fd96ae6..c701d6ca647 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 @@ */ 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 interface EventListener extends MediaSourceEventListener { @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 AdsMediaSource( 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 MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { 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 void run() { } - /** - * 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 22bc2b08ecb..e2a60974114 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 @@ -31,6 +31,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.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; @@ -61,59 +62,31 @@ 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 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; } @@ -123,76 +96,119 @@ private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, * {@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. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. + * + * @param manifestParser A parser for loaded manifest data. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setManifestParser( + ParsingLoadable.Parser manifestParser) { + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); + return this; + } + + /** + * Returns a new {@link DashMediaSource} using the current parameters and the specified + * sideloaded manifest. * + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. * @param eventHandler A handler for events. * @param eventListener A listener of events. - * @return This builder. + * @return The new {@link DashMediaSource}. + * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; + 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, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); } /** - * Sets the manifest parser to parse loaded manifest data. The default is - * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * Returns a new {@link DashMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link DashMediaSource}. */ - public Builder setManifestParser( - ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; - return this; + public DashMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); } /** - * 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. * - * @return The newly built {@link DashMediaSource}. + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. */ - public DashMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { + @Override + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { manifestParser = new DashManifestParser(); } - return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + return new DashMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, eventListener); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH}; + } } /** @@ -259,7 +275,7 @@ public DashMediaSource build() { * @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(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, @@ -276,7 +292,7 @@ public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourc * @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(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, @@ -295,7 +311,7 @@ public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourc * @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(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -321,7 +337,7 @@ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFac * 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 Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -347,7 +363,7 @@ public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFac * 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 Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, 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 215d8a05185..8973853245e 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 @@ protected MediaSource buildSource(HostActivity host, String userAgent, 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 4c71d6361ddb322f53aca12be0f333a8e287abfe Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 08:32:46 -0800 Subject: [PATCH 080/105] Make SsMediaSource.Builder a factory for SsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178380856 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../source/smoothstreaming/SsMediaSource.java | 173 ++++++++++-------- 2 files changed, 99 insertions(+), 83 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 1be6df8437f..38938bd3677 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { : 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 34343e08e39..eb6ceb3dcc1 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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; @@ -52,59 +53,31 @@ 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 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; } @@ -114,73 +87,117 @@ private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, * {@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. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. + * + * @param manifestParser A parser for loaded manifest data. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setManifestParser(ParsingLoadable.Parser manifestParser) { + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); + return this; + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded + * manifest. * + * @param manifest The manifest. {@link SsManifest#isLive} must be false. * @param eventHandler A handler for events. * @param eventListener A listener of events. - * @return This builder. + * @return The new {@link SsMediaSource}. + * @throws IllegalArgumentException If {@link SsManifest#isLive} is true. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; + 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, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); } /** - * 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. + * Returns a new {@link SsMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link SsMediaSource}. */ - public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; - return this; + public SsMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); } /** - * 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. * - * @return The newly built {@link SsMediaSource}. + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. */ - public SsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { + @Override + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { manifestParser = new SsManifestParser(); } - return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + return new SsMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, eventListener); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_SS}; + } + } /** @@ -228,7 +245,7 @@ public SsMediaSource build() { * @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(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -245,7 +262,7 @@ public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFacto * @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(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -264,7 +281,7 @@ public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFacto * @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(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -288,7 +305,7 @@ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFacto * 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(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -312,7 +329,7 @@ public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFacto * 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(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, From 42b612a49f801f95bfa72b81cafef90329ac2737 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 09:07:55 -0800 Subject: [PATCH 081/105] Make HlsMediaSource.Builder a factory for HlsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178384204 --- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/source/hls/HlsMediaSource.java | 143 +++++++++--------- 2 files changed, 71 insertions(+), 78 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 38938bd3677..215c4708e85 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { 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 4e904032fd2..3a55cb8a171 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 @@ 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; @@ -26,6 +27,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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; @@ -47,66 +49,51 @@ 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 int minLoadableRetryCount; - 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; } /** - * 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; } @@ -114,63 +101,71 @@ public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { * 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. + * Sets the parser to parse HLS playlists. The default is an instance of {@link + * HlsPlaylistParser}. * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setEventListener(Handler eventHandler, - MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; + public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { + Assertions.checkState(!isCreateCalled); + this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } /** - * Sets the parser to parse HLS playlists. The default is an instance of - * {@link HlsPlaylistParser}. + * Returns a new {@link HlsMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @return This builder. + * @return The new {@link HlsMediaSource}. */ - public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { - this.playlistParser = playlistParser; - return this; + public MediaSource createMediaSource(Uri playlistUri) { + return createMediaSource(playlistUri, null, null); } /** - * 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. * - * @return The newly built {@link HlsMediaSource}. + * @param playlistUri The playlist {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @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; - } + @Override + public MediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } - return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - minLoadableRetryCount, eventHandler, eventListener, playlistParser); + return new HlsMediaSource( + playlistUri, + hlsDataSourceFactory, + extractorFactory, + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_HLS}; + } } /** @@ -195,7 +190,7 @@ public HlsMediaSource build() { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@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(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, @@ -213,7 +208,7 @@ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Han * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@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(Uri manifestUri, DataSource.Factory dataSourceFactory, @@ -234,7 +229,7 @@ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, * @param eventListener An {@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(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, From babd5750e3411cf78ea597dbbe92f08ecdd54b59 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:08:01 -0800 Subject: [PATCH 082/105] 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 c702a22ce65..01c91995a7a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,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 cc504432968..2c172c086b9 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_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.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 static DummySurface newInstanceV17(Context context, boolean secure) { 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 @@ private static void assertApiLevel17OrHigher() { } } - /** - * 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 DummySurfaceThread() { 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 boolean handleMessage(Message msg) { 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 boolean handleMessage(Message msg) { } } - 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 @@ private void initInternal(boolean secure) { 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 f31696b79a608328454f1ae20ef8801aba91dfd0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:23:00 -0800 Subject: [PATCH 083/105] Make ExtractorMediaSource.Builder a factory for ExtractorMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178605481 --- .../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 | 159 +++++++++--------- .../exoplayer2/source/ads/AdsMediaSource.java | 26 +-- 7 files changed, 104 insertions(+), 119 deletions(-) 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 6b840830c5e..ec21f6d2654 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 @@ public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { // 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 215c4708e85..a60ae0c8766 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 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension) { 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 bd6e698dc6e..fd18a3b1ae0 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.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 void run() { 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 aa61df74d9a..d3ab4216552 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.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 void run() { 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 746f3d273f9..3cc1a1d340b 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.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 void run() { 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 247eacd519c..e787c34a9cd 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.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 interface EventListener { 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 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 + * 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 builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { + 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. - * Default value is null. + * The default value is {@code null}. * * @param customCacheKey A custom key that uniquely identifies the original stream. Used for * cache indexing. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCustomCacheKey(String customCacheKey) { + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); 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 minimum number of times to retry if a loading error occurs. The default value is + * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. * - * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between - * each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @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 setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { - this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); + this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * 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 eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * 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. */ - @Deprecated - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); + 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. + * @param uri The {@link Uri}. + * @return The new {@link ExtractorMediaSource}. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; + public MediaSource createMediaSource(Uri uri) { + return createMediaSource(uri, null, null); } /** - * Builds a new {@link ExtractorMediaSource} using the current parameters. - *

- * After this call, the builder should not be re-used. + * Returns a new {@link ExtractorMediaSource} using the current parameters. * - * @return The newly built {@link ExtractorMediaSource}. + * @param uri The {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@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 ExtractorMediaSource build() { continueLoadingCheckIntervalBytes); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } } /** @@ -244,11 +233,15 @@ public ExtractorMediaSource build() { * 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 ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, * @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 ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, * 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 c701d6ca647..5611bedccac 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 AdsMediaSource( 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 void run() { } - 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 7a089c3a293e32642e8c81f4ba6dbb1749507619 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 07:19:53 -0800 Subject: [PATCH 084/105] 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 01c91995a7a..dfc6e3d0871 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,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 c32228de28b..536d8d4662a 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 ec21f6d2654..51959451d1a 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 @@ 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.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 @@ 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 @@ public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { .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 @@ public void release() { 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 a60ae0c8766..fa3c7d401aa 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.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.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 @@ private void initializePlayer() { } 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 @@ private void initializePlayer() { 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 @@ private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) // 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 5611bedccac..0980e9d0115 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 interface EventListener extends MediaSourceEventListener { 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 interface EventListener extends MediaSourceEventListener { 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 AdsMediaSource( 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 b223988e30c9f176de8a156c9ed49e5cef124b80 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 Dec 2017 09:03:15 -0800 Subject: [PATCH 085/105] 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 dfc6e3d0871..1dab497cf26 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,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 0fa34e5144a..b4bb8861756 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.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 @@ */ 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 ImaAdsLoader(Context context, Uri adTagUri) { * 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 com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { 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 void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGrou 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 void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { 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; - } - - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); + if (player != null) { + // If a player is attached already, start playback immediately. + startAdPlayback(); } - - updateAdPlaybackState(); } // AdEvent.AdEventListener implementation. @@ -384,14 +470,12 @@ public void onAdEvent(AdEvent adEvent) { 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 void onAdError(AdErrorEvent adErrorEvent) { 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. @@ -653,18 +738,56 @@ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // 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 bf9a919005a0968909f565e5a6840d8eb4b12e08 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Dec 2017 11:05:57 -0800 Subject: [PATCH 086/105] 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 aa007ea1d6e..1b1224273fb 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 interface CustomActionProvider { 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 MediaSessionConnector(MediaSessionCompat mediaSession) { /** * 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 MediaSessionConnector(MediaSessionCompat mediaSession, * 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 @@ private void updateMediaSessionMetadata() { 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 65ccff24483a60bb93ac31255b062803d9c60f86 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Dec 2017 23:43:16 -0800 Subject: [PATCH 087/105] Update release notes to reflect builder -> factory change ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178866131 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1dab497cf26..61e6d2759d8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,7 @@ ### dev-v2 (not yet released) ### -* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, +* Add Factory to ExtractorMediaSource, HlsMediaSource, SsMediaSource, DashMediaSource, SingleSampleMediaSource. * DASH: * Support time zone designators in ISO8601 UTCTiming elements From ae514b68ff3c93e09972d6410ab2b64cffe62684 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 02:16:15 -0800 Subject: [PATCH 088/105] Update release notes for current 2.6.1 feature set ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178877884 --- RELEASENOTES.md | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 61e6d2759d8..2f9008045e0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,29 +1,11 @@ # Release notes # -### dev-v2 (not yet released) ### +### 2.6.1 ### -* Add Factory to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource, SingleSampleMediaSource. -* DASH: - * Support time zone designators in ISO8601 UTCTiming elements - ([#3524](https://github.com/google/ExoPlayer/issues/3524)). -* 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)). -* DefaultTrackSelector: Support undefined language text track selection when the - preferred language is not available - ([#2980](https://github.com/google/ExoPlayer/issues/2980)). -* 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)). +* 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)). @@ -36,6 +18,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 8a6c375c53005fec5de6790bb1109f3937363632 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:09:21 -0800 Subject: [PATCH 089/105] 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 7b193997c35..57ce487ac75 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 @@ */ 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 fd18a3b1ae0..b236b706b82 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 void onPlayerStateChanged(boolean playWhenReady, int playbackState) { 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 d3ab4216552..c547cff434a 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 3cc1a1d340b..0a902e2efee 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 void onPlayerStateChanged(boolean playWhenReady, int playbackState) { Looper.myLooper().quit(); } } - } } From 67b94a72a55ba1e1e50665bbede907a0e3fc749e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 08:32:28 -0800 Subject: [PATCH 090/105] 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 f557d4ac978..f907dc6229a 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 @@ -431,9 +431,6 @@ public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - if (released) { - return; - } eventDispatcher.loadCanceled( loadable.dataSpec, C.DATA_TYPE_MEDIA, @@ -446,12 +443,14 @@ public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, 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 e787c34a9cd..3b650482f5e 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 Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckInte * @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 MediaSource createMediaSource(Uri uri) { * @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 6101c79b7f0..5069a2d633f 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,13 +15,11 @@ */ 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.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; @@ -43,14 +41,14 @@ */ 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; @@ -62,16 +60,20 @@ /* 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<>(); @@ -125,7 +127,9 @@ public boolean continueLoading(long positionUs) { 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; } @@ -158,6 +162,18 @@ public long seekToUs(long positionUs) { @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; @@ -167,34 +183,46 @@ public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, @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; @@ -259,14 +287,15 @@ public int skipData(long positionUs) { /* 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; } @@ -286,7 +315,7 @@ public void load() throws IOException, InterruptedException { 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 2aa8ccc7125..3b0a5a16c82 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 @@ 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 interface EventListener { } - /** - * 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 Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, lon * {@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 Builder setEventSourceId(int eventSourceId) { * @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. * - * @return The newly built {@link SingleSampleMediaSource}. + * @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 build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; + public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { + return createMediaSource(uri, format, durationUs, null, null); + } - return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, - minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + /** + * 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 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 SingleSampleMediaSource build() { */ 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 SingleSampleMediaSource build() { * 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 SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Fo * @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 SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Fo * @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); } @@ -235,8 +262,14 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { @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 void releaseSource() { // 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 3a55cb8a171..8ed202d9619 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 @@ -131,7 +131,7 @@ public Factory setPlaylistParser(ParsingLoadable.Parser playlistPar * * @return The new {@link HlsMediaSource}. */ - public MediaSource createMediaSource(Uri playlistUri) { + public HlsMediaSource createMediaSource(Uri playlistUri) { return createMediaSource(playlistUri, null, null); } @@ -144,7 +144,7 @@ public MediaSource createMediaSource(Uri playlistUri) { * @return The new {@link HlsMediaSource}. */ @Override - public MediaSource createMediaSource( + public HlsMediaSource createMediaSource( Uri playlistUri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { From 403f773f8703f013b129ef02336b881678f1311f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 08:27:32 -0800 Subject: [PATCH 091/105] 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 1ab3854d216..b6ed4b17af7 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,10 +51,13 @@ + + - + + From b97ce44182ea61fc42584938c79a554c9891654c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:17:30 -0800 Subject: [PATCH 092/105] 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 b4bb8861756..e0bca20d381 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 ImaAdsLoader buildForAdsResponse(String adsResponse) { 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 void onAdError(AdErrorEvent adErrorEvent) { @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 VideoProgressUpdate getContentProgress() { 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 04108eec48f85cc0bf1b5eecadba8b7a5127fdb2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 093/105] 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 e0bca20d381..d65bf876058 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 @@ -702,6 +702,9 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); + } } } @@ -797,16 +800,20 @@ private void maybeNotifyAdError() { 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(); @@ -818,7 +825,6 @@ private void updateImaStateForPlayerState() { } } } - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } private void resumeContentInternal() { From 52794e749d1ae214cbb9be1a2f7124ff1dd1e2d0 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 094/105] 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 030f0cdbb0c..6bdbebc73bb 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 @@ private void handleC1Command(int command) { 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 ab7503843c3c336b5df397a0a97ece2278da168d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 03:06:10 -0800 Subject: [PATCH 095/105] 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 d65bf876058..70a8322bba9 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 VideoProgressUpdate getContentProgress() { 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 VideoProgressUpdate getContentProgress() { 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 236df9a571890d0eed256782e5ed484652b12776 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 08:58:02 -0800 Subject: [PATCH 096/105] 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 d8bc042ad78..a89655c8e9d 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 final 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; @@ -171,11 +167,11 @@ public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) @Override public boolean shouldContinueLoading(long bufferedDurationUs) { - 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); @@ -186,11 +182,6 @@ public boolean shouldContinueLoading(long bufferedDurationUs) { 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 1e36c76654004a00b9805d9bf6a365334f724c03 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 08:38:02 -0800 Subject: [PATCH 097/105] 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 2f9008045e0..2a2347686d2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,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 a89655c8e9d..b7b68de7d29 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 final 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 DefaultLoadControl() { * @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 DefaultLoadControl(DefaultAllocator allocator) { * @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 DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBu * @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 void onPrepared() { @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); } @@ -162,16 +206,28 @@ public Allocator getAllocator() { @Override public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { 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) { 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); @@ -182,6 +238,25 @@ public boolean shouldContinueLoading(long bufferedDurationUs) { 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 3fae0b8e6e756c1210636071088dd1fe7e9c080e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 11:59:36 -0800 Subject: [PATCH 098/105] 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 c3f3e32526c..17b90680dd7 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 @@ public ChannelMappingAudioProcessor() { /** * 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 2c172c086b9..9fcf89d6282 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_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 @@ private void initInternal(@SecureMode int secureMode) { 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 16ad280817f68376d0017fdeb7cb16508571701a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 12:38:05 -0800 Subject: [PATCH 099/105] Bump version to 2.6.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179227114 --- constants.gradle | 2 +- demos/ima/src/main/AndroidManifest.xml | 4 ++-- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/constants.gradle b/constants.gradle index bad69389a53..c18fb28d4d8 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/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index f14feeda745..0efeaf6f7fa 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 ec8016e8a37..00326157a21 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 f13a7de0ca0..b2200b66714 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 ad95a147d24d2a16412d70ba5981ea1f8f5fa303 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 22 Dec 2017 05:33:11 -0800 Subject: [PATCH 100/105] 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 89af07a3f09..d27c329845b 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 @@ -278,9 +278,10 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 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 f657893973a27ee57dd1ece40ed19c30b9b33662 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:07:15 -0800 Subject: [PATCH 101/105] 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 eec4a1269c0..0cb6f668989 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 @@ private void parseFormatLine(String formatLine) { 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 @@ private void parseFormatLine(String formatLine) { */ 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 f8c76f62e40fb29913635fc6b0d8225698cd4bf3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Jan 2018 13:50:24 +0000 Subject: [PATCH 102/105] Fix ClippingSampleStream --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 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 d27c329845b..1114a563b6e 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 @@ -112,7 +112,7 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF if (internalStreams[i] == null) { sampleStreams[i] = null; } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, + sampleStreams[i] = new ClippingSampleStream(internalStreams[i], startUs, endUs, pendingInitialDiscontinuity); } streams[i] = sampleStreams[i]; @@ -222,9 +222,8 @@ private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selection /** * 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; @@ -232,9 +231,8 @@ private static final class ClippingSampleStream implements SampleStream { private boolean pendingDiscontinuity; private boolean sentEos; - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; + public ClippingSampleStream(SampleStream stream, long startUs, long endUs, + boolean pendingDiscontinuity) { this.stream = stream; this.startUs = startUs; this.endUs = endUs; From 42a3e2e9d2c4383c00bca1b2b032fedeb47aadb7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Dec 2017 07:35:30 -0800 Subject: [PATCH 103/105] 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 2a2347686d2..ef0facd6e28 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,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 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 0e99380a1c4..d0810a0629a 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 @@ /** 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 @@ public static WavHeader peek(ExtractorInput input) throws IOException, Interrupt + 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 134c494b1b4b6ae03706c3a6859ce264020856ae Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 27 Dec 2017 09:02:40 -0800 Subject: [PATCH 104/105] 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 ecfe3eb96f3..7f353295168 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 ef0facd6e28..5fe00907271 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,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 b704a1d6436ba64a0d6fab2868d36a6b47030fdc Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 07:00:04 -0800 Subject: [PATCH 105/105] 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 5fe00907271..9d949570d7e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,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