diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java new file mode 100644 index 0000000000..bc29b2fdf1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; + +/** + * Media period that wraps a media source and defers calling its + * {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()} + * has been called. This is useful if you need to return a media period immediately but the media + * source that should create it is not yet prepared. + */ +public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + /** + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then + * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} + * to release the period. + */ + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + /** + * Releases the period. + */ + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + // MediaPeriod.Callback implementation + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 6b5c8b2637..c410456e7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -758,111 +757,5 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } - /** - * Media period used for periods created from unprepared media sources exposed through - * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes - * available. - */ - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - public final MediaSource mediaSource; - - private final MediaPeriodId id; - private final Allocator allocator; - - private MediaPeriod mediaPeriod; - private Callback callback; - private long preparePositionUs; - - public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { - this.id = id; - this.allocator = allocator; - this.mediaSource = mediaSource; - } - - public void createPeriod() { - mediaPeriod = mediaSource.createPeriod(id, allocator); - if (callback != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - public void releasePeriod() { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - } - } - - @Override - public void prepare(Callback callback, long preparePositionUs) { - this.callback = callback; - this.preparePositionUs = preparePositionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } else { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); - } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 202e31cba1..47a2540c38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.ads; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; @@ -23,15 +24,19 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -68,12 +73,12 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; private final Handler mainHandler; private final ComponentListener componentListener; - private final Map adMediaSourceByMediaPeriod; + private final AdMediaSourceFactory adMediaSourceFactory; + private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @Nullable private final Handler eventHandler; @@ -95,6 +100,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

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

+ * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -121,18 +132,18 @@ public final class AdsMediaSource implements MediaSource { AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable AdsListener eventListener) { this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); + adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - adsLoader.setSupportedContentTypes(C.TYPE_OTHER); + adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @Override @@ -173,10 +184,9 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource.Builder( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) - .setEventListener(mainHandler, componentListener) - .build(); + Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; + final MediaSource adMediaSource = + adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -186,30 +196,37 @@ public final class AdsMediaSource implements MediaSource { Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { + deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList()); + adMediaSource.prepareSource(player, false, new MediaSource.Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + @Nullable Object manifest) { + onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline); } }); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + if (mediaPeriods == null) { + deferredMediaPeriod.createPeriod(); + } else { + // Keep track of the deferred media period so it can be populated with the real media period + // when the source's info becomes available. + mediaPeriods.add(deferredMediaPeriod); + } + return deferredMediaPeriod; } else { - return contentMediaSource.createPeriod(id, allocator); + DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + mediaPeriod.createPeriod(); + return mediaPeriod; } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); } @Override @@ -264,9 +281,17 @@ public final class AdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, + int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) { + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + for (int i = 0; i < mediaPeriods.size(); i++) { + mediaPeriods.get(i).createPeriod(); + } + deferredMediaPeriodByAdMediaSource.remove(mediaSource); + } maybeUpdateSourceInfo(); } @@ -285,7 +310,7 @@ public final class AdsMediaSource implements MediaSource { * Listener for component events. All methods are called on the main thread. */ private final class ComponentListener implements AdsLoader.EventListener, - ExtractorMediaSource.EventListener { + AdMediaSourceLoadErrorListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -349,4 +374,76 @@ public final class AdsMediaSource implements MediaSource { } + /** + * Listener for errors while loading an ad {@link MediaSource}. + */ + private interface AdMediaSourceLoadErrorListener { + + /** + * Called when an error occurs loading media data. + * + * @param error The load error. + */ + void onLoadError(IOException error); + + } + + /** + * Factory for {@link MediaSource}s for loading ad media. + */ + private interface AdMediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the ad. + * @param handler A handler for listener events. + * @param listener A listener for ad load errors. To have ad media source load errors notified + * via the ads media source's listener, call this listener's onLoadError method from your + * new media source's load error listener using the specified {@code handler}. Otherwise, + * this parameter can be ignored. + * @return The new media source. + */ + MediaSource createAdMediaSource(Uri uri, Handler handler, + AdMediaSourceLoadErrorListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or + * {@link C#TYPE_OTHER}. + * + * @return The content types supported by the factory. + */ + int[] getSupportedTypes(); + + } + + private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public MediaSource createAdMediaSource(Uri uri, Handler handler, + final AdMediaSourceLoadErrorListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, + new EventListener() { + @Override + public void onLoadError(IOException error) { + listener.onLoadError(error); + } + }).build(); + } + + @Override + public int[] getSupportedTypes() { + // Only ExtractorMediaSource is supported. + return new int[] {C.TYPE_OTHER}; + } + + } + }