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
This commit is contained in:
andrewlewis 2017-11-30 01:25:28 -08:00 committed by Oliver Woodman
parent 882d698d5f
commit 5865f1fe40
3 changed files with 259 additions and 130 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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<MediaPeriod, MediaSource> adMediaSourceByMediaPeriod;
private final AdMediaSourceFactory adMediaSourceFactory;
private final Map<MediaSource, List<DeferredMediaPeriod>> 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}.
* <p>
* 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}.
* <p>
* 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<DeferredMediaPeriod>());
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<DeferredMediaPeriod> 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<DeferredMediaPeriod> 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};
}
}
}