mirror of
https://github.com/androidx/media.git
synced 2025-05-07 23:50:44 +08:00
Use Loader directly for manifest + utc timing loads.
Aside from deleting code, this change (a) adds retry functionality for initial manifest fetches, (b) uses the same loader for the utc timing fetch as for the manifest. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=123634812
This commit is contained in:
parent
b61b3ab011
commit
911d86bf2f
@ -68,6 +68,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream,
|
||||
private final Loader loader;
|
||||
private final Format format;
|
||||
private final long durationUs;
|
||||
private final int minLoadableRetryCount;
|
||||
private final TrackGroupArray tracks;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
@ -96,10 +97,11 @@ public final class SingleSampleSource implements SampleSource, TrackStream,
|
||||
this.dataSource = dataSource;
|
||||
this.format = format;
|
||||
this.durationUs = durationUs;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.eventSourceId = eventSourceId;
|
||||
loader = new Loader("Loader:SingleSampleSource", minLoadableRetryCount);
|
||||
loader = new Loader("Loader:SingleSampleSource");
|
||||
tracks = new TrackGroupArray(new TrackGroup(format));
|
||||
sampleData = new byte[INITIAL_SAMPLE_SIZE];
|
||||
}
|
||||
@ -268,7 +270,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream,
|
||||
if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) {
|
||||
return;
|
||||
}
|
||||
loader.startLoading(this, this);
|
||||
loader.startLoading(this, this, minLoadableRetryCount);
|
||||
}
|
||||
|
||||
private void notifyLoadError(final IOException e) {
|
||||
|
@ -41,6 +41,7 @@ public class ChunkTrackStream implements TrackStream, Loader.Callback<Chunk> {
|
||||
|
||||
private final Loader loader;
|
||||
private final ChunkSource chunkSource;
|
||||
private final int minLoadableRetryCount;
|
||||
private final LinkedList<BaseMediaChunk> mediaChunks;
|
||||
private final List<BaseMediaChunk> readOnlyMediaChunks;
|
||||
private final DefaultTrackOutput sampleQueue;
|
||||
@ -76,7 +77,8 @@ public class ChunkTrackStream implements TrackStream, Loader.Callback<Chunk> {
|
||||
ChunkTrackStreamEventListener eventListener, int eventSourceId, int minLoadableRetryCount) {
|
||||
this.chunkSource = chunkSource;
|
||||
this.loadControl = loadControl;
|
||||
loader = new Loader("Loader:ChunkTrackStream", minLoadableRetryCount);
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
loader = new Loader("Loader:ChunkTrackStream");
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
|
||||
nextChunkHolder = new ChunkHolder();
|
||||
mediaChunks = new LinkedList<>();
|
||||
@ -317,7 +319,7 @@ public class ChunkTrackStream implements TrackStream, Loader.Callback<Chunk> {
|
||||
eventDispatcher.loadStarted(loadable.dataSpec.length, loadable.type, loadable.trigger,
|
||||
loadable.format, -1, -1);
|
||||
}
|
||||
loader.startLoading(loadable, this);
|
||||
loader.startLoading(loadable, this, minLoadableRetryCount);
|
||||
// Update the load control again to indicate that we're now loading.
|
||||
loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), true);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.Format;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackGroup;
|
||||
import com.google.android.exoplayer.TrackGroupArray;
|
||||
@ -34,13 +35,12 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||
import com.google.android.exoplayer.dash.mpd.Period;
|
||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElement;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback;
|
||||
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.UriLoadable;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.net.Uri;
|
||||
@ -49,35 +49,46 @@ import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* A {@link SampleSource} for DASH media.
|
||||
*/
|
||||
public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
public final class DashSampleSource implements SampleSource {
|
||||
|
||||
private static final String TAG = "DashSampleSource";
|
||||
|
||||
/**
|
||||
* The minimum number of times to retry loading data prior to failing.
|
||||
*/
|
||||
// TODO: Use this for manifest loads as well.
|
||||
private static final int MIN_LOADABLE_RETRY_COUNT = 3;
|
||||
|
||||
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||
private final DataSourceFactory dataSourceFactory;
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final Handler eventHandler;
|
||||
private final ChunkTrackStreamEventListener eventListener;
|
||||
private final LoadControl loadControl;
|
||||
private final Loader loader;
|
||||
private final DataSource dataSource;
|
||||
private final MediaPresentationDescriptionParser manifestParser;
|
||||
private final ManifestCallback manifestCallback;
|
||||
|
||||
private Uri manifestUri;
|
||||
private long manifestLoadTimestamp;
|
||||
private MediaPresentationDescription manifest;
|
||||
|
||||
private boolean prepared;
|
||||
private boolean released;
|
||||
private long durationUs;
|
||||
private long elapsedRealtimeOffset;
|
||||
private MediaPresentationDescription manifest;
|
||||
private TrackGroupArray trackGroups;
|
||||
private int[] trackGroupAdaptationSetIndices;
|
||||
private boolean pendingReset;
|
||||
@ -86,21 +97,21 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
private DashChunkSource[] chunkSources;
|
||||
private ChunkTrackStream[] trackStreams;
|
||||
|
||||
public DashSampleSource(Uri uri, DataSourceFactory dataSourceFactory,
|
||||
public DashSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory,
|
||||
BandwidthMeter bandwidthMeter, Handler eventHandler,
|
||||
ChunkTrackStreamEventListener eventListener) {
|
||||
this.manifestUri = manifestUri;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
|
||||
loadControl = new DefaultLoadControl(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE));
|
||||
loader = new Loader("Loader:DashSampleSource");
|
||||
dataSource = dataSourceFactory.createDataSource();
|
||||
manifestParser = new MediaPresentationDescriptionParser();
|
||||
manifestCallback = new ManifestCallback();
|
||||
chunkSources = new DashChunkSource[0];
|
||||
trackStreams = new ChunkTrackStream[0];
|
||||
|
||||
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
||||
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -108,25 +119,11 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
if (prepared) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (manifest == null) {
|
||||
manifest = manifestFetcher.getManifest();
|
||||
if (manifest == null) {
|
||||
manifestFetcher.maybeThrowError();
|
||||
manifestFetcher.requestRefresh();
|
||||
return false;
|
||||
}
|
||||
durationUs = manifest.dynamic ? C.UNSET_TIME_US : manifest.duration * 1000;
|
||||
buildTrackGroups(manifest);
|
||||
if (manifest.utcTiming != null) {
|
||||
UtcTimingElementResolver.resolveTimingElement(dataSourceFactory.createDataSource(),
|
||||
manifest.utcTiming, manifestFetcher.getManifestLoadCompleteTimestamp(), this);
|
||||
} else {
|
||||
prepared = true;
|
||||
}
|
||||
loader.maybeThrowError();
|
||||
if (!loader.isLoading() && manifest == null) {
|
||||
startLoadingManifest();
|
||||
}
|
||||
|
||||
return prepared;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -177,14 +174,6 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
@Override
|
||||
public void continueBuffering(long positionUs) {
|
||||
if (manifest.dynamic) {
|
||||
MediaPresentationDescription newManifest = manifestFetcher.getManifest();
|
||||
if (newManifest != manifest) {
|
||||
manifest = newManifest;
|
||||
for (DashChunkSource chunkSource : chunkSources) {
|
||||
chunkSource.updateManifest(newManifest);
|
||||
}
|
||||
}
|
||||
|
||||
long minUpdatePeriod = manifest.minUpdatePeriod;
|
||||
if (minUpdatePeriod == 0) {
|
||||
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
|
||||
@ -193,13 +182,11 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
|
||||
minUpdatePeriod = 5000;
|
||||
}
|
||||
|
||||
if (SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadStartTimestamp()
|
||||
+ minUpdatePeriod) {
|
||||
manifestFetcher.requestRefresh();
|
||||
if (!loader.isLoading()
|
||||
&& SystemClock.elapsedRealtime() > manifestLoadTimestamp + minUpdatePeriod) {
|
||||
startLoadingManifest();
|
||||
}
|
||||
}
|
||||
|
||||
for (ChunkTrackStream trackStream : trackStreams) {
|
||||
trackStream.continueBuffering(positionUs);
|
||||
}
|
||||
@ -241,39 +228,87 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
manifestFetcher.release();
|
||||
loader.release();
|
||||
for (DashChunkSource chunkSource : chunkSources) {
|
||||
chunkSource.release();
|
||||
}
|
||||
for (ChunkTrackStream trackStream : trackStreams) {
|
||||
trackStream.release();
|
||||
}
|
||||
released = true;
|
||||
}
|
||||
|
||||
// UtcTimingCallback implementation.
|
||||
// Loadable callbacks.
|
||||
|
||||
@Override
|
||||
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
|
||||
if (released) {
|
||||
return;
|
||||
/* package */ void onManifestLoadCompleted(MediaPresentationDescription manifest) {
|
||||
this.manifest = manifest;
|
||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||
if (manifest.location != null) {
|
||||
manifestUri = manifest.location;
|
||||
}
|
||||
if (!prepared) {
|
||||
durationUs = manifest.dynamic ? C.UNSET_TIME_US : manifest.duration * 1000;
|
||||
buildTrackGroups(manifest);
|
||||
if (manifest.utcTiming != null) {
|
||||
resolveUtcTimingElement(manifest.utcTiming);
|
||||
} else {
|
||||
prepared = true;
|
||||
}
|
||||
} else {
|
||||
for (DashChunkSource chunkSource : chunkSources) {
|
||||
chunkSource.updateManifest(manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void onUtcTimestampLoadCompleted(long elapsedRealtimeOffset) {
|
||||
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||
prepared = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
|
||||
/* package */ void onUtcTimestampLoadError(IOException e) {
|
||||
Log.e(TAG, "Failed to resolve UtcTiming element.", e);
|
||||
// Be optimistic and continue in the hope that the device clock is correct.
|
||||
prepared = true;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void startLoadingManifest() {
|
||||
loader.startLoading(new UriLoadable<>(manifestUri, dataSource, manifestParser),
|
||||
manifestCallback, MIN_LOADABLE_RETRY_COUNT);
|
||||
}
|
||||
|
||||
private void resolveUtcTimingElement(UtcTimingElement timingElement) {
|
||||
String scheme = timingElement.schemeIdUri;
|
||||
if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
|
||||
resolveUtcTimingElementDirect(timingElement);
|
||||
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) {
|
||||
resolveUtcTimingElementHttp(timingElement, new Iso8601Parser());
|
||||
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
|
||||
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
|
||||
resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser());
|
||||
} else {
|
||||
// Unsupported scheme.
|
||||
onUtcTimestampLoadError(new IOException("Unsupported UTC timing scheme"));
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) {
|
||||
try {
|
||||
long utcTimestamp = Util.parseXsDateTime(timingElement.value);
|
||||
long elapsedRealtimeOffset = utcTimestamp - manifestLoadTimestamp;
|
||||
onUtcTimestampLoadCompleted(elapsedRealtimeOffset);
|
||||
} catch (ParseException e) {
|
||||
onUtcTimestampLoadError(new ParserException(e));
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveUtcTimingElementHttp(UtcTimingElement timingElement,
|
||||
UriLoadable.Parser<Long> parser) {
|
||||
loader.startLoading(new UriLoadable<>(Uri.parse(timingElement.value), dataSource, parser),
|
||||
new UtcTimestampCallback(), 1);
|
||||
}
|
||||
|
||||
private void buildTrackGroups(MediaPresentationDescription manifest) {
|
||||
Period period = manifest.getPeriod(0);
|
||||
int trackGroupCount = 0;
|
||||
@ -321,4 +356,78 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback {
|
||||
return Pair.create(chunkSource, trackStream);
|
||||
}
|
||||
|
||||
|
||||
private final class ManifestCallback implements
|
||||
Loader.Callback<UriLoadable<MediaPresentationDescription>> {
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(UriLoadable<MediaPresentationDescription> loadable,
|
||||
long elapsedMs) {
|
||||
onManifestLoadCompleted(loadable.getResult());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(UriLoadable<MediaPresentationDescription> loadable, long elapsedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(UriLoadable<MediaPresentationDescription> loadable, long elapsedMs,
|
||||
IOException exception) {
|
||||
return (exception instanceof ParserException) ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class UtcTimestampCallback implements Loader.Callback<UriLoadable<Long>> {
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(UriLoadable<Long> loadable, long elapsedMs) {
|
||||
onUtcTimestampLoadCompleted(loadable.getResult() - SystemClock.elapsedRealtime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(UriLoadable<Long> loadable, long elapsedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(UriLoadable<Long> loadable, long elapsedMs, IOException exception) {
|
||||
onUtcTimestampLoadError(exception);
|
||||
return Loader.DONT_RETRY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class XsDateTimeParser implements UriLoadable.Parser<Long> {
|
||||
|
||||
@Override
|
||||
public Long parse(Uri uri, InputStream inputStream) throws IOException {
|
||||
String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
|
||||
try {
|
||||
return Util.parseXsDateTime(firstLine);
|
||||
} catch (ParseException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class Iso8601Parser implements UriLoadable.Parser<Long> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,8 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.dash.mpd;
|
||||
|
||||
import com.google.android.exoplayer.util.ManifestFetcher.RedirectingManifest;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Collections;
|
||||
@ -25,7 +23,7 @@ import java.util.List;
|
||||
/**
|
||||
* Represents a DASH media presentation description (mpd).
|
||||
*/
|
||||
public class MediaPresentationDescription implements RedirectingManifest {
|
||||
public class MediaPresentationDescription {
|
||||
|
||||
public final long availabilityStartTime;
|
||||
|
||||
@ -59,11 +57,6 @@ public class MediaPresentationDescription implements RedirectingManifest {
|
||||
this.periods = periods == null ? Collections.<Period>emptyList() : periods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Uri getNextManifestUri() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public final int getPeriodCount() {
|
||||
return periods.size();
|
||||
}
|
||||
|
@ -1,184 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.exoplayer.dash.mpd;
|
||||
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.UriLoadable;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
/**
|
||||
* Resolves a {@link UtcTimingElement}.
|
||||
*/
|
||||
public final class UtcTimingElementResolver implements Loader.Callback<UriLoadable<Long>> {
|
||||
|
||||
/**
|
||||
* Callback for timing element resolution.
|
||||
*/
|
||||
public interface UtcTimingCallback {
|
||||
|
||||
/**
|
||||
* Invoked when the element has been resolved.
|
||||
*
|
||||
* @param utcTiming The element that was resolved.
|
||||
* @param elapsedRealtimeOffset The offset between the resolved UTC time and
|
||||
* {@link SystemClock#elapsedRealtime()} in milliseconds, specified as the UTC time minus
|
||||
* the local elapsed time.
|
||||
*/
|
||||
void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset);
|
||||
|
||||
/**
|
||||
* Invoked when the element was not successfully resolved.
|
||||
*
|
||||
* @param utcTiming The element that was not resolved.
|
||||
* @param e The cause of the failure.
|
||||
*/
|
||||
void onTimestampError(UtcTimingElement utcTiming, IOException e);
|
||||
}
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final UtcTimingElement timingElement;
|
||||
private final long timingElementElapsedRealtime;
|
||||
private final UtcTimingCallback callback;
|
||||
|
||||
private Loader singleUseLoader;
|
||||
|
||||
/**
|
||||
* Resolves a {@link UtcTimingElement}.
|
||||
*
|
||||
* @param dataSource A {@link DataSource} to use should loading from a {@link Uri} be necessary.
|
||||
* @param timingElement The element to resolve.
|
||||
* @param timingElementElapsedRealtime The {@link SystemClock#elapsedRealtime()} timestamp at
|
||||
* which the element was obtained. Used if the element contains a timestamp directly.
|
||||
* @param callback The callback to invoke on resolution or failure.
|
||||
*/
|
||||
public static void resolveTimingElement(DataSource dataSource, UtcTimingElement timingElement,
|
||||
long timingElementElapsedRealtime, UtcTimingCallback callback) {
|
||||
UtcTimingElementResolver resolver = new UtcTimingElementResolver(dataSource, timingElement,
|
||||
timingElementElapsedRealtime, callback);
|
||||
resolver.resolve();
|
||||
}
|
||||
|
||||
private UtcTimingElementResolver(DataSource dataSource, UtcTimingElement timingElement,
|
||||
long timingElementElapsedRealtime, UtcTimingCallback callback) {
|
||||
this.dataSource = dataSource;
|
||||
this.timingElement = Assertions.checkNotNull(timingElement);
|
||||
this.timingElementElapsedRealtime = timingElementElapsedRealtime;
|
||||
this.callback = Assertions.checkNotNull(callback);
|
||||
}
|
||||
|
||||
private void resolve() {
|
||||
String scheme = timingElement.schemeIdUri;
|
||||
if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
|
||||
resolveDirect();
|
||||
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) {
|
||||
resolveHttp(new Iso8601Parser());
|
||||
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
|
||||
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
|
||||
resolveHttp(new XsDateTimeParser());
|
||||
} else {
|
||||
// Unsupported scheme.
|
||||
callback.onTimestampError(timingElement, new IOException("Unsupported utc timing scheme"));
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveDirect() {
|
||||
try {
|
||||
long utcTimestamp = Util.parseXsDateTime(timingElement.value);
|
||||
long elapsedRealtimeOffset = utcTimestamp - timingElementElapsedRealtime;
|
||||
callback.onTimestampResolved(timingElement, elapsedRealtimeOffset);
|
||||
} catch (ParseException e) {
|
||||
callback.onTimestampError(timingElement, new ParserException(e));
|
||||
}
|
||||
}
|
||||
|
||||
private void resolveHttp(UriLoadable.Parser<Long> parser) {
|
||||
singleUseLoader = new Loader("Loader:UtcTiming", 0);
|
||||
singleUseLoader.startLoading(
|
||||
new UriLoadable<>(Uri.parse(timingElement.value), dataSource, parser), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(UriLoadable<Long> loadable, long elapsedMs) {
|
||||
releaseLoader();
|
||||
long elapsedRealtimeOffset = loadable.getResult() - SystemClock.elapsedRealtime();
|
||||
callback.onTimestampResolved(timingElement, elapsedRealtimeOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(UriLoadable<Long> loadable, long elapsedMs) {
|
||||
onLoadError(loadable, elapsedMs,
|
||||
new IOException("Load cancelled", new CancellationException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(UriLoadable<Long> loadable, long elapsedMs, IOException exception) {
|
||||
releaseLoader();
|
||||
callback.onTimestampError(timingElement, exception);
|
||||
return Loader.DONT_RETRY;
|
||||
}
|
||||
|
||||
private void releaseLoader() {
|
||||
singleUseLoader.release();
|
||||
}
|
||||
|
||||
private static class XsDateTimeParser implements UriLoadable.Parser<Long> {
|
||||
|
||||
@Override
|
||||
public Long parse(Uri uri, InputStream inputStream) throws IOException {
|
||||
String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
|
||||
try {
|
||||
return Util.parseXsDateTime(firstLine);
|
||||
} catch (ParseException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Iso8601Parser implements UriLoadable.Parser<Long> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -231,10 +231,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
|
||||
this.allocator = allocator;
|
||||
this.requestedBufferSize = requestedBufferSize;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
// Assume on-demand until we know otherwise.
|
||||
int initialMinRetryCount = minLoadableRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA
|
||||
? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND : minLoadableRetryCount;
|
||||
loader = new Loader("Loader:ExtractorSampleSource", initialMinRetryCount);
|
||||
loader = new Loader("Loader:ExtractorSampleSource");
|
||||
extractorHolder = new ExtractorHolder(extractors, this);
|
||||
pendingResetPositionUs = C.UNSET_TIME_US;
|
||||
sampleQueues = new DefaultTrackOutput[0];
|
||||
@ -363,10 +360,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
|
||||
trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat());
|
||||
}
|
||||
tracks = new TrackGroupArray(trackArray);
|
||||
if (minLoadableRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA && !seekMap.isSeekable()
|
||||
&& durationUs == C.UNSET_TIME_US) {
|
||||
loader.setMinRetryCount(DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE);
|
||||
}
|
||||
prepared = true;
|
||||
return true;
|
||||
}
|
||||
@ -594,7 +587,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
|
||||
pendingResetPositionUs = C.UNSET_TIME_US;
|
||||
}
|
||||
extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
|
||||
loader.startLoading(loadable, this);
|
||||
|
||||
int minRetryCount = minLoadableRetryCount;
|
||||
if (minRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) {
|
||||
// We assume on-demand before we're prepared.
|
||||
minRetryCount = !prepared || (length != C.LENGTH_UNBOUNDED
|
||||
|| (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US))
|
||||
? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND
|
||||
: DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE;
|
||||
}
|
||||
loader.startLoading(loadable, this, minRetryCount);
|
||||
}
|
||||
|
||||
private void configureRetry() {
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.Format;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackGroup;
|
||||
import com.google.android.exoplayer.TrackGroupArray;
|
||||
@ -35,7 +36,8 @@ import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.UriLoadable;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
|
||||
import android.net.Uri;
|
||||
@ -51,14 +53,15 @@ import java.util.List;
|
||||
/**
|
||||
* A {@link SampleSource} for HLS streams.
|
||||
*/
|
||||
public final class HlsSampleSource implements SampleSource {
|
||||
public final class HlsSampleSource implements SampleSource,
|
||||
Loader.Callback<UriLoadable<HlsPlaylist>> {
|
||||
|
||||
/**
|
||||
* The minimum number of times to retry loading data prior to failing.
|
||||
*/
|
||||
// TODO: Use this for playlist loads as well.
|
||||
private static final int MIN_LOADABLE_RETRY_COUNT = 3;
|
||||
|
||||
private final Uri manifestUri;
|
||||
private final DataSourceFactory dataSourceFactory;
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final Handler eventHandler;
|
||||
@ -66,7 +69,9 @@ public final class HlsSampleSource implements SampleSource {
|
||||
private final LoadControl loadControl;
|
||||
private final IdentityHashMap<TrackStream, HlsTrackStreamWrapper> trackStreamSources;
|
||||
private final PtsTimestampAdjusterProvider timestampAdjusterProvider;
|
||||
private final ManifestFetcher<HlsPlaylist> manifestFetcher;
|
||||
private final Loader manifestFetcher;
|
||||
private final DataSource manifestDataSource;
|
||||
private final HlsPlaylistParser manifestParser;
|
||||
|
||||
private boolean seenFirstTrackSelection;
|
||||
private long durationUs;
|
||||
@ -78,9 +83,10 @@ public final class HlsSampleSource implements SampleSource {
|
||||
private boolean pendingReset;
|
||||
private long lastSeekPositionUs;
|
||||
|
||||
public HlsSampleSource(Uri uri, DataSourceFactory dataSourceFactory,
|
||||
public HlsSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory,
|
||||
BandwidthMeter bandwidthMeter, Handler eventHandler,
|
||||
ChunkTrackStreamEventListener eventListener) {
|
||||
this.manifestUri = manifestUri;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.eventHandler = eventHandler;
|
||||
@ -90,9 +96,9 @@ public final class HlsSampleSource implements SampleSource {
|
||||
timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
|
||||
trackStreamSources = new IdentityHashMap<>();
|
||||
|
||||
HlsPlaylistParser parser = new HlsPlaylistParser();
|
||||
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
||||
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
|
||||
manifestDataSource = dataSourceFactory.createDataSource();
|
||||
manifestParser = new HlsPlaylistParser();
|
||||
manifestFetcher = new Loader("Loader:ManifestFetcher");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -102,16 +108,13 @@ public final class HlsSampleSource implements SampleSource {
|
||||
}
|
||||
|
||||
if (trackStreamWrappers == null) {
|
||||
HlsPlaylist playlist = manifestFetcher.getManifest();
|
||||
if (playlist == null) {
|
||||
manifestFetcher.maybeThrowError();
|
||||
manifestFetcher.requestRefresh();
|
||||
return false;
|
||||
manifestFetcher.maybeThrowError();
|
||||
if (!manifestFetcher.isLoading()) {
|
||||
manifestFetcher.startLoading(
|
||||
new UriLoadable<>(manifestUri, manifestDataSource, manifestParser), this,
|
||||
MIN_LOADABLE_RETRY_COUNT);
|
||||
}
|
||||
List<HlsTrackStreamWrapper> trackStreamWrapperList = buildTrackStreamWrappers(playlist);
|
||||
trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()];
|
||||
trackStreamWrapperList.toArray(trackStreamWrappers);
|
||||
selectedTrackCounts = new int[trackStreamWrappers.length];
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean trackStreamWrappersPrepared = true;
|
||||
@ -231,6 +234,27 @@ public final class HlsSampleSource implements SampleSource {
|
||||
}
|
||||
}
|
||||
|
||||
// Loader.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(UriLoadable<HlsPlaylist> loadable, long elapsedMs) {
|
||||
HlsPlaylist playlist = loadable.getResult();
|
||||
List<HlsTrackStreamWrapper> trackStreamWrapperList = buildTrackStreamWrappers(playlist);
|
||||
trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()];
|
||||
trackStreamWrapperList.toArray(trackStreamWrappers);
|
||||
selectedTrackCounts = new int[trackStreamWrappers.length];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(UriLoadable<HlsPlaylist> loadable, long elapsedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(UriLoadable<HlsPlaylist> loadable, long elapsedMs, IOException exception) {
|
||||
return exception instanceof ParserException ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private List<HlsTrackStreamWrapper> buildTrackStreamWrappers(HlsPlaylist playlist) {
|
||||
|
@ -54,16 +54,17 @@ import java.util.List;
|
||||
private static final int PRIMARY_TYPE_AUDIO = 2;
|
||||
private static final int PRIMARY_TYPE_VIDEO = 3;
|
||||
|
||||
private final Loader loader;
|
||||
private final HlsChunkSource chunkSource;
|
||||
private final SparseArray<DefaultTrackOutput> sampleQueues;
|
||||
private final LinkedList<HlsMediaChunk> mediaChunks;
|
||||
private final int bufferSizeContribution;
|
||||
private final ChunkHolder nextChunkHolder;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final LoadControl loadControl;
|
||||
private final int bufferSizeContribution;
|
||||
private final Format muxedAudioFormat;
|
||||
private final Format muxedCaptionFormat;
|
||||
private final int minLoadableRetryCount;
|
||||
private final Loader loader;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final ChunkHolder nextChunkHolder;
|
||||
private final SparseArray<DefaultTrackOutput> sampleQueues;
|
||||
private final LinkedList<HlsMediaChunk> mediaChunks;
|
||||
|
||||
private volatile boolean sampleQueuesBuilt;
|
||||
|
||||
@ -110,7 +111,8 @@ import java.util.List;
|
||||
this.bufferSizeContribution = bufferSizeContribution;
|
||||
this.muxedAudioFormat = muxedAudioFormat;
|
||||
this.muxedCaptionFormat = muxedCaptionFormat;
|
||||
loader = new Loader("Loader:HlsTrackStreamWrapper", minLoadableRetryCount);
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
loader = new Loader("Loader:HlsTrackStreamWrapper");
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId);
|
||||
nextChunkHolder = new ChunkHolder();
|
||||
sampleQueues = new SparseArray<>();
|
||||
@ -575,7 +577,7 @@ import java.util.List;
|
||||
eventDispatcher.loadStarted(loadable.dataSpec.length, loadable.type, loadable.trigger,
|
||||
loadable.format, -1, -1);
|
||||
}
|
||||
loader.startLoading(loadable, this);
|
||||
loader.startLoading(loadable, this, minLoadableRetryCount);
|
||||
if (prepared) {
|
||||
// Update the load control again to indicate that we're now loading.
|
||||
loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), true);
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.Format;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.SampleSource;
|
||||
import com.google.android.exoplayer.TrackGroup;
|
||||
import com.google.android.exoplayer.TrackGroupArray;
|
||||
@ -35,7 +36,8 @@ import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DataSourceFactory;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.UriLoadable;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.net.Uri;
|
||||
@ -51,26 +53,32 @@ import java.util.List;
|
||||
/**
|
||||
* A {@link SampleSource} for SmoothStreaming media.
|
||||
*/
|
||||
public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
public final class SmoothStreamingSampleSource implements SampleSource,
|
||||
Loader.Callback<UriLoadable<SmoothStreamingManifest>> {
|
||||
|
||||
/**
|
||||
* The minimum number of times to retry loading data prior to failing.
|
||||
*/
|
||||
// TODO: Use this for manifest loads as well.
|
||||
private static final int MIN_LOADABLE_RETRY_COUNT = 3;
|
||||
|
||||
private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
|
||||
private static final int INITIALIZATION_VECTOR_SIZE = 8;
|
||||
|
||||
private final Uri manifestUri;
|
||||
private final DataSourceFactory dataSourceFactory;
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final Handler eventHandler;
|
||||
private final ChunkTrackStreamEventListener eventListener;
|
||||
private final LoadControl loadControl;
|
||||
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
|
||||
private final Loader manifestLoader;
|
||||
private final DataSource manifestDataSource;
|
||||
private final SmoothStreamingManifestParser manifestParser;
|
||||
|
||||
private long manifestLoadTimestamp;
|
||||
private SmoothStreamingManifest manifest;
|
||||
|
||||
private boolean prepared;
|
||||
private long durationUs;
|
||||
private SmoothStreamingManifest currentManifest;
|
||||
private TrackEncryptionBox[] trackEncryptionBoxes;
|
||||
private TrackGroupArray trackGroups;
|
||||
private int[] trackGroupElementIndices;
|
||||
@ -80,9 +88,11 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
private SmoothStreamingChunkSource[] chunkSources;
|
||||
private ChunkTrackStream[] trackStreams;
|
||||
|
||||
public SmoothStreamingSampleSource(Uri uri, DataSourceFactory dataSourceFactory,
|
||||
public SmoothStreamingSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory,
|
||||
BandwidthMeter bandwidthMeter, Handler eventHandler,
|
||||
ChunkTrackStreamEventListener eventListener) {
|
||||
this.manifestUri = Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest")
|
||||
? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest");
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.eventHandler = eventHandler;
|
||||
@ -92,37 +102,21 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
chunkSources = new SmoothStreamingChunkSource[0];
|
||||
trackStreams = new ChunkTrackStream[0];
|
||||
|
||||
if (!Util.toLowerInvariant(uri.getLastPathSegment()).equals("manifest")) {
|
||||
uri = Uri.withAppendedPath(uri, "Manifest");
|
||||
}
|
||||
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||
DataSource manifestDataSource = dataSourceFactory.createDataSource();
|
||||
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
|
||||
manifestDataSource = dataSourceFactory.createDataSource();
|
||||
manifestParser = new SmoothStreamingManifestParser();
|
||||
manifestLoader = new Loader("Loader:Manifest");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(long positionUs) throws IOException {
|
||||
if (currentManifest != null) {
|
||||
// Already prepared.
|
||||
if (prepared) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentManifest = manifestFetcher.getManifest();
|
||||
if (currentManifest == null) {
|
||||
manifestFetcher.maybeThrowError();
|
||||
manifestFetcher.requestRefresh();
|
||||
return false;
|
||||
manifestLoader.maybeThrowError();
|
||||
if (!manifestLoader.isLoading()) {
|
||||
startLoadingManifest();
|
||||
}
|
||||
|
||||
durationUs = currentManifest.durationUs;
|
||||
buildTrackGroups(currentManifest);
|
||||
ProtectionElement protectionElement = currentManifest.protectionElement;
|
||||
if (protectionElement != null) {
|
||||
byte[] keyId = getProtectionElementKeyId(protectionElement.data);
|
||||
trackEncryptionBoxes = new TrackEncryptionBox[] {
|
||||
new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)};
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -173,26 +167,17 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
|
||||
@Override
|
||||
public void continueBuffering(long positionUs) {
|
||||
if (currentManifest.isLive) {
|
||||
SmoothStreamingManifest newManifest = manifestFetcher.getManifest();
|
||||
if (newManifest != currentManifest) {
|
||||
currentManifest = newManifest;
|
||||
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
|
||||
chunkSource.updateManifest(newManifest);
|
||||
}
|
||||
}
|
||||
|
||||
if (SystemClock.elapsedRealtime()
|
||||
> manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS) {
|
||||
if (manifest.isLive) {
|
||||
if (!manifestLoader.isLoading() && SystemClock.elapsedRealtime()
|
||||
> manifestLoadTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS) {
|
||||
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
|
||||
if (chunkSource.needManifestRefresh()) {
|
||||
manifestFetcher.requestRefresh();
|
||||
startLoadingManifest();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ChunkTrackStream trackStream : trackStreams) {
|
||||
trackStream.continueBuffering(positionUs);
|
||||
}
|
||||
@ -234,7 +219,7 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
manifestFetcher.release();
|
||||
manifestLoader.release();
|
||||
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
|
||||
chunkSource.release();
|
||||
}
|
||||
@ -243,7 +228,46 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
// Loader.Callback implementation
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(UriLoadable<SmoothStreamingManifest> loadable, long elapsedMs) {
|
||||
manifest = loadable.getResult();
|
||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||
if (!prepared) {
|
||||
durationUs = manifest.durationUs;
|
||||
buildTrackGroups(manifest);
|
||||
ProtectionElement protectionElement = manifest.protectionElement;
|
||||
if (protectionElement != null) {
|
||||
byte[] keyId = getProtectionElementKeyId(protectionElement.data);
|
||||
trackEncryptionBoxes = new TrackEncryptionBox[] {
|
||||
new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)};
|
||||
}
|
||||
prepared = true;
|
||||
} else {
|
||||
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
|
||||
chunkSource.updateManifest(manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(UriLoadable<SmoothStreamingManifest> loadable, long elapsedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(UriLoadable<SmoothStreamingManifest> loadable, long elapsedMs,
|
||||
IOException exception) {
|
||||
return (exception instanceof ParserException) ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private void startLoadingManifest() {
|
||||
manifestLoader.startLoading(new UriLoadable<>(manifestUri, manifestDataSource, manifestParser),
|
||||
this, MIN_LOADABLE_RETRY_COUNT);
|
||||
}
|
||||
|
||||
private void buildTrackGroups(SmoothStreamingManifest manifest) {
|
||||
int trackGroupCount = 0;
|
||||
@ -273,11 +297,11 @@ public final class SmoothStreamingSampleSource implements SampleSource {
|
||||
FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1
|
||||
? new AdaptiveEvaluator(bandwidthMeter) : null;
|
||||
int streamElementIndex = trackGroupElementIndices[selection.group];
|
||||
StreamElement streamElement = currentManifest.streamElements[streamElementIndex];
|
||||
StreamElement streamElement = manifest.streamElements[streamElementIndex];
|
||||
int streamElementType = streamElement.type;
|
||||
int bufferSize = Util.getDefaultBufferSize(streamElementType);
|
||||
DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter);
|
||||
SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(currentManifest,
|
||||
SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(manifest,
|
||||
streamElementIndex, trackGroups.get(selection.group), selectedTracks, dataSource,
|
||||
adaptiveEvaluator, trackEncryptionBoxes);
|
||||
ChunkTrackStream trackStream = new ChunkTrackStream(chunkSource, loadControl, bufferSize,
|
||||
|
@ -120,17 +120,14 @@ public final class Loader {
|
||||
|
||||
private final ExecutorService downloadExecutorService;
|
||||
|
||||
private int minRetryCount;
|
||||
private LoadTask<? extends Loadable> currentTask;
|
||||
private IOException fatalError;
|
||||
|
||||
/**
|
||||
* @param threadName A name for the loader's thread.
|
||||
* @param minRetryCount The minimum retry count.
|
||||
*/
|
||||
public Loader(String threadName, int minRetryCount) {
|
||||
public Loader(String threadName) {
|
||||
this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
|
||||
this.minRetryCount = minRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,12 +138,15 @@ public final class Loader {
|
||||
*
|
||||
* @param loadable The {@link Loadable} to load.
|
||||
* @param callback A callback to invoke when the load ends.
|
||||
* @param minRetryCount The minimum number of times the load must be retried before
|
||||
* {@link #maybeThrowError()} will propagate an error.
|
||||
* @throws IllegalStateException If the calling thread does not have an associated {@link Looper}.
|
||||
*/
|
||||
public <T extends Loadable> void startLoading(T loadable, Callback<T> callback) {
|
||||
public <T extends Loadable> void startLoading(T loadable, Callback<T> callback,
|
||||
int minRetryCount) {
|
||||
Looper looper = Looper.myLooper();
|
||||
Assertions.checkState(looper != null);
|
||||
new LoadTask<>(looper, loadable, callback).start(0);
|
||||
new LoadTask<>(looper, loadable, callback, minRetryCount).start(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,18 +158,9 @@ public final class Loader {
|
||||
return currentTask != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum retry count.
|
||||
*
|
||||
* @param minRetryCount The minimum retry count.
|
||||
*/
|
||||
public void setMinRetryCount(int minRetryCount) {
|
||||
this.minRetryCount = minRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a fatal error has been encountered, or if the current {@link Loadable} has incurred a number
|
||||
* of errors greater than the minimum number of retries and if the load is currently backed off,
|
||||
* of errors greater than its minimum number of retries and if the load is currently backed off,
|
||||
* then an error is thrown. Else does nothing.
|
||||
*
|
||||
* @throws IOException The error.
|
||||
@ -178,7 +169,7 @@ public final class Loader {
|
||||
if (fatalError != null) {
|
||||
throw fatalError;
|
||||
} else if (currentTask != null) {
|
||||
currentTask.maybeThrowError(minRetryCount);
|
||||
currentTask.maybeThrowError();
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,20 +202,22 @@ public final class Loader {
|
||||
private final T loadable;
|
||||
private final Loader.Callback<T> callback;
|
||||
private final long startTimeMs;
|
||||
private final int minRetryCount;
|
||||
|
||||
private IOException currentError;
|
||||
private int errorCount;
|
||||
|
||||
private volatile Thread executorThread;
|
||||
|
||||
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback) {
|
||||
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback, int minRetryCount) {
|
||||
super(looper);
|
||||
this.loadable = loadable;
|
||||
this.callback = callback;
|
||||
this.minRetryCount = minRetryCount;
|
||||
this.startTimeMs = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
public void maybeThrowError(int minRetryCount) throws IOException {
|
||||
public void maybeThrowError() throws IOException {
|
||||
if (currentError != null && errorCount > minRetryCount) {
|
||||
throw currentError;
|
||||
}
|
||||
|
@ -1,252 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.exoplayer.util;
|
||||
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.Loader;
|
||||
import com.google.android.exoplayer.upstream.UriLoadable;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Loads and refreshes a media manifest.
|
||||
*
|
||||
* @param <T> The type of manifest.
|
||||
*/
|
||||
public class ManifestFetcher<T> implements Loader.Callback<UriLoadable<T>> {
|
||||
|
||||
/**
|
||||
* Thrown when an error occurs trying to fetch a manifest.
|
||||
*/
|
||||
public static final class ManifestIOException extends IOException{
|
||||
public ManifestIOException(Throwable cause) { super(cause); }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be notified of {@link ManifestFetcher} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
void onManifestRefreshStarted();
|
||||
|
||||
void onManifestRefreshed();
|
||||
|
||||
void onManifestError(IOException e);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for manifests that are able to specify that subsequent loads should use a different
|
||||
* URI.
|
||||
*/
|
||||
public interface RedirectingManifest {
|
||||
|
||||
/**
|
||||
* Returns the {@link Uri} from which subsequent manifests should be requested, or null to
|
||||
* continue using the current {@link Uri}.
|
||||
*/
|
||||
Uri getNextManifestUri();
|
||||
|
||||
}
|
||||
|
||||
private final Loader loader;
|
||||
private final UriLoadable.Parser<T> parser;
|
||||
private final DataSource dataSource;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
|
||||
private volatile Uri manifestUri;
|
||||
|
||||
private long currentLoadStartTimestamp;
|
||||
|
||||
private volatile T manifest;
|
||||
private volatile long manifestLoadStartTimestamp;
|
||||
private volatile long manifestLoadCompleteTimestamp;
|
||||
|
||||
/**
|
||||
* @param manifestUri The manifest {@link Uri}.
|
||||
* @param dataSource The {@link DataSource} to use when loading the manifest.
|
||||
* @param parser A parser to parse the loaded manifest data.
|
||||
*/
|
||||
public ManifestFetcher(Uri manifestUri, DataSource dataSource, UriLoadable.Parser<T> parser) {
|
||||
this(manifestUri, dataSource, parser, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param manifestUri The manifest {@link Uri}.
|
||||
* @param dataSource The {@link DataSource} to use when loading the manifest.
|
||||
* @param parser A parser to parse the loaded manifest data.
|
||||
* @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.
|
||||
*/
|
||||
public ManifestFetcher(Uri manifestUri, DataSource dataSource, UriLoadable.Parser<T> parser,
|
||||
Handler eventHandler, EventListener eventListener) {
|
||||
this.parser = parser;
|
||||
this.manifestUri = manifestUri;
|
||||
this.dataSource = dataSource;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
loader = new Loader("Loader:ManifestFetcher", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the manifest {@link Uri}.
|
||||
*
|
||||
* @param manifestUri The manifest {@link Uri}.
|
||||
*/
|
||||
public void updateManifestUri(Uri manifestUri) {
|
||||
this.manifestUri = manifestUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link Pair} containing the most recently loaded manifest together with the timestamp
|
||||
* at which the load completed.
|
||||
*
|
||||
* @return The most recently loaded manifest and the timestamp at which the load completed, or
|
||||
* null if no manifest has loaded.
|
||||
*/
|
||||
public T getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of {@link SystemClock#elapsedRealtime()} when the last completed load started.
|
||||
*
|
||||
* @return The value of {@link SystemClock#elapsedRealtime()} when the last completed load
|
||||
* started.
|
||||
*/
|
||||
public long getManifestLoadStartTimestamp() {
|
||||
return manifestLoadStartTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of {@link SystemClock#elapsedRealtime()} when the last load completed.
|
||||
*
|
||||
* @return The value of {@link SystemClock#elapsedRealtime()} when the last load completed.
|
||||
*/
|
||||
public long getManifestLoadCompleteTimestamp() {
|
||||
return manifestLoadCompleteTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws the error that affected the most recent attempt to load the manifest. Does nothing if
|
||||
* the most recent attempt was successful.
|
||||
*
|
||||
* @throws ManifestIOException The error that affected the most recent attempt to load the
|
||||
* manifest.
|
||||
*/
|
||||
public void maybeThrowError() throws ManifestIOException {
|
||||
if (loader != null) {
|
||||
try {
|
||||
loader.maybeThrowError();
|
||||
} catch (IOException e) {
|
||||
throw new ManifestIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be invoked repeatedly by callers who require an updated manifest.
|
||||
*/
|
||||
public void requestRefresh() {
|
||||
if (loader.isLoading()) {
|
||||
return;
|
||||
}
|
||||
currentLoadStartTimestamp = SystemClock.elapsedRealtime();
|
||||
loader.startLoading(new UriLoadable<>(manifestUri, dataSource, parser), this);
|
||||
notifyManifestRefreshStarted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the fetcher.
|
||||
* <p>
|
||||
* This method should be called when the fetcher is no longer required.
|
||||
*/
|
||||
public void release() {
|
||||
loader.release();
|
||||
}
|
||||
|
||||
// Loadable.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(UriLoadable<T> loadable, long elapsedMs) {
|
||||
manifest = loadable.getResult();
|
||||
manifestLoadStartTimestamp = currentLoadStartTimestamp;
|
||||
manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime();
|
||||
if (manifest instanceof RedirectingManifest) {
|
||||
RedirectingManifest redirectingManifest = (RedirectingManifest) manifest;
|
||||
Uri nextUri = redirectingManifest.getNextManifestUri();
|
||||
if (nextUri != null) {
|
||||
manifestUri = nextUri;
|
||||
}
|
||||
}
|
||||
notifyManifestRefreshed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(UriLoadable<T> loadable, long elapsedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(UriLoadable<T> loadable, long elapsedMs, IOException exception) {
|
||||
notifyManifestError(new ManifestIOException(exception));
|
||||
return Loader.RETRY;
|
||||
}
|
||||
|
||||
// Private methods.
|
||||
|
||||
private void notifyManifestRefreshStarted() {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onManifestRefreshStarted();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyManifestRefreshed() {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onManifestRefreshed();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyManifestError(final IOException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onManifestError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user