mirror of
https://github.com/androidx/media.git
synced 2025-05-10 00:59:51 +08:00
Implement UTC time synchronization for DASH Live.
Support is provided for the following schemes: urn:mpeg:dash:utc:direct:2012 urn:mpeg:dash:utc:http-iso:2014 urn:mpeg:dash:utc:http-xsdate:2012 urn:mpeg:dash:utc:http-xsdate:2014
This commit is contained in:
parent
4076b08e4b
commit
6d14fc3330
@ -37,6 +37,9 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
|||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||||
import com.google.android.exoplayer.dash.mpd.Period;
|
import com.google.android.exoplayer.dash.mpd.Period;
|
||||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
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.demo.DemoUtil;
|
import com.google.android.exoplayer.demo.DemoUtil;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
||||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
|
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
|
||||||
@ -59,6 +62,7 @@ import android.annotation.TargetApi;
|
|||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.UnsupportedSchemeException;
|
import android.media.UnsupportedSchemeException;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -70,7 +74,9 @@ import java.util.List;
|
|||||||
* A {@link RendererBuilder} for DASH.
|
* A {@link RendererBuilder} for DASH.
|
||||||
*/
|
*/
|
||||||
public class DashRendererBuilder implements RendererBuilder,
|
public class DashRendererBuilder implements RendererBuilder,
|
||||||
ManifestCallback<MediaPresentationDescription> {
|
ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
|
||||||
|
|
||||||
|
private static final String TAG = "DashRendererBuilder";
|
||||||
|
|
||||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||||
@ -96,6 +102,9 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
private RendererBuilderCallback callback;
|
private RendererBuilderCallback callback;
|
||||||
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||||
|
|
||||||
|
private MediaPresentationDescription manifest;
|
||||||
|
private long elapsedRealtimeOffset;
|
||||||
|
|
||||||
public DashRendererBuilder(String userAgent, String url, String contentId,
|
public DashRendererBuilder(String userAgent, String url, String contentId,
|
||||||
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) {
|
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) {
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
@ -116,13 +125,36 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onManifest(String contentId, MediaPresentationDescription manifest) {
|
||||||
|
this.manifest = manifest;
|
||||||
|
if (manifest.dynamic && manifest.utcTiming != null) {
|
||||||
|
UtcTimingElementResolver.resolveTimingElement(userAgent, manifest.utcTiming,
|
||||||
|
manifestFetcher.getManifestLoadTimestamp(), this);
|
||||||
|
} else {
|
||||||
|
buildRenderers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onManifestError(String contentId, IOException e) {
|
public void onManifestError(String contentId, IOException e) {
|
||||||
callback.onRenderersError(e);
|
callback.onRenderersError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onManifest(String contentId, MediaPresentationDescription manifest) {
|
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
|
||||||
|
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||||
|
buildRenderers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
|
||||||
|
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
|
||||||
|
// Be optimistic and continue in the hope that the device clock is correct.
|
||||||
|
buildRenderers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildRenderers() {
|
||||||
Period period = manifest.periods.get(0);
|
Period period = manifest.periods.get(0);
|
||||||
Handler mainHandler = player.getMainHandler();
|
Handler mainHandler = player.getMainHandler();
|
||||||
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
|
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
|
||||||
@ -207,7 +239,7 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||||
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
|
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
|
||||||
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
|
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
|
||||||
LIVE_EDGE_LATENCY_MS);
|
LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset);
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
||||||
DemoPlayer.TYPE_VIDEO);
|
DemoPlayer.TYPE_VIDEO);
|
||||||
@ -230,7 +262,8 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
|
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
|
||||||
format.audioSamplingRate + "Hz)");
|
format.audioSamplingRate + "Hz)");
|
||||||
audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
|
audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
|
||||||
new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS));
|
new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS,
|
||||||
|
elapsedRealtimeOffset));
|
||||||
haveAc3Tracks |= AC_3_CODEC.equals(format.codecs) || E_AC_3_CODEC.equals(format.codecs);
|
haveAc3Tracks |= AC_3_CODEC.equals(format.codecs) || E_AC_3_CODEC.equals(format.codecs);
|
||||||
}
|
}
|
||||||
// Filter out non-AC-3 tracks if there is an AC-3 track, to avoid having to switch renderers.
|
// Filter out non-AC-3 tracks if there is an AC-3 track, to avoid having to switch renderers.
|
||||||
@ -285,7 +318,7 @@ public class DashRendererBuilder implements RendererBuilder,
|
|||||||
Representation representation = representations.get(j);
|
Representation representation = representations.get(j);
|
||||||
textTrackNameList.add(representation.format.id);
|
textTrackNameList.add(representation.format.id);
|
||||||
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
|
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
|
||||||
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS));
|
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,7 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
private final StringBuilder headerBuilder;
|
private final StringBuilder headerBuilder;
|
||||||
private final long liveEdgeLatencyUs;
|
private final long liveEdgeLatencyUs;
|
||||||
|
private final long elapsedRealtimeOffsetUs;
|
||||||
private final int maxWidth;
|
private final int maxWidth;
|
||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
|
|
||||||
@ -140,7 +141,8 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
*/
|
*/
|
||||||
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
|
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
|
||||||
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
|
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
|
||||||
this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0);
|
this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0,
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,18 +164,21 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
* manifest). Choosing a small value will minimize latency introduced by the player, however
|
* manifest). Choosing a small value will minimize latency introduced by the player, however
|
||||||
* note that the value sets an upper bound on the length of media that the player can buffer.
|
* note that the value sets an upper bound on the length of media that the player can buffer.
|
||||||
* Hence a small value may increase the probability of rebuffering and playback failures.
|
* Hence a small value may increase the probability of rebuffering and playback failures.
|
||||||
|
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
|
||||||
|
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
|
||||||
|
* as the server's unix time minus the local elapsed time. It unknown, set to 0.
|
||||||
*/
|
*/
|
||||||
public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
|
public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
|
||||||
int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
|
int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
|
||||||
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
|
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) {
|
||||||
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
|
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
|
||||||
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000);
|
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
|
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
|
||||||
MediaPresentationDescription initialManifest, int adaptationSetIndex,
|
MediaPresentationDescription initialManifest, int adaptationSetIndex,
|
||||||
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
|
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
|
||||||
long liveEdgeLatencyUs) {
|
long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) {
|
||||||
this.manifestFetcher = manifestFetcher;
|
this.manifestFetcher = manifestFetcher;
|
||||||
this.currentManifest = initialManifest;
|
this.currentManifest = initialManifest;
|
||||||
this.adaptationSetIndex = adaptationSetIndex;
|
this.adaptationSetIndex = adaptationSetIndex;
|
||||||
@ -181,6 +186,7 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = formatEvaluator;
|
this.evaluator = formatEvaluator;
|
||||||
this.liveEdgeLatencyUs = liveEdgeLatencyUs;
|
this.liveEdgeLatencyUs = liveEdgeLatencyUs;
|
||||||
|
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs;
|
||||||
this.evaluation = new Evaluation();
|
this.evaluation = new Evaluation();
|
||||||
this.headerBuilder = new StringBuilder();
|
this.headerBuilder = new StringBuilder();
|
||||||
|
|
||||||
@ -326,8 +332,12 @@ public class DashChunkSource implements ChunkSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use UtcTimingElement where possible.
|
long nowUs;
|
||||||
long nowUs = System.currentTimeMillis() * 1000;
|
if (elapsedRealtimeOffsetUs != 0) {
|
||||||
|
nowUs = (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs;
|
||||||
|
} else {
|
||||||
|
nowUs = System.currentTimeMillis() * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
|
int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
|
||||||
int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum();
|
int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
|
@ -28,4 +28,9 @@ public class UtcTimingElement {
|
|||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return schemeIdUri + ", " + value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* 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.Loader;
|
||||||
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.NetworkLoadable;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
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.concurrent.CancellationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a {@link UtcTimingElement}.
|
||||||
|
*/
|
||||||
|
public class UtcTimingElementResolver implements Loader.Callback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 static final int TYPE_XS = 0;
|
||||||
|
private static final int TYPE_ISO = 1;
|
||||||
|
|
||||||
|
private final String userAgent;
|
||||||
|
private final UtcTimingElement timingElement;
|
||||||
|
private final long timingElementElapsedRealtime;
|
||||||
|
private final UtcTimingCallback callback;
|
||||||
|
|
||||||
|
private Loader singleUseLoader;
|
||||||
|
private HttpTimestampLoadable singleUseLoadable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a {@link UtcTimingElement}.
|
||||||
|
*
|
||||||
|
* @param userAgent A user agent to use should network requests 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(String userAgent, UtcTimingElement timingElement,
|
||||||
|
long timingElementElapsedRealtime, UtcTimingCallback callback) {
|
||||||
|
UtcTimingElementResolver resolver = new UtcTimingElementResolver(userAgent, timingElement,
|
||||||
|
timingElementElapsedRealtime, callback);
|
||||||
|
resolver.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UtcTimingElementResolver(String userAgent, UtcTimingElement timingElement,
|
||||||
|
long timingElementElapsedRealtime, UtcTimingCallback callback) {
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
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(TYPE_ISO);
|
||||||
|
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
|
||||||
|
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
|
||||||
|
resolveHttp(TYPE_XS);
|
||||||
|
} 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(int type) {
|
||||||
|
singleUseLoader = new Loader("utctiming");
|
||||||
|
singleUseLoadable = new HttpTimestampLoadable(timingElement.value, userAgent, type);
|
||||||
|
singleUseLoader.startLoading(singleUseLoadable, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCanceled(Loadable loadable) {
|
||||||
|
onLoadError(loadable, new IOException("Load cancelled", new CancellationException()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(Loadable loadable) {
|
||||||
|
releaseLoader();
|
||||||
|
long elapsedRealtimeOffset = singleUseLoadable.getResult() - SystemClock.elapsedRealtime();
|
||||||
|
callback.onTimestampResolved(timingElement, elapsedRealtimeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadError(Loadable loadable, IOException exception) {
|
||||||
|
releaseLoader();
|
||||||
|
callback.onTimestampError(timingElement, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLoader() {
|
||||||
|
singleUseLoader.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HttpTimestampLoadable extends NetworkLoadable<Long> {
|
||||||
|
|
||||||
|
private final int type;
|
||||||
|
|
||||||
|
public HttpTimestampLoadable(String url, String userAgent, int type) {
|
||||||
|
super(url, userAgent);
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Long parse(String connectionUrl, InputStream inputStream, String inputEncoding)
|
||||||
|
throws ParserException, IOException {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||||
|
String firstLine = reader.readLine();
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case TYPE_XS:
|
||||||
|
return Util.parseXsDateTime(firstLine);
|
||||||
|
case TYPE_ISO:
|
||||||
|
// 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);
|
||||||
|
return format.parse(firstLine).getTime();
|
||||||
|
default:
|
||||||
|
// Never happens.
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new ParserException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.util;
|
package com.google.android.exoplayer.util;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.upstream.Loader;
|
import com.google.android.exoplayer.upstream.Loader;
|
||||||
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
|
|
||||||
@ -25,8 +26,6 @@ import android.util.Pair;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +83,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
|
|
||||||
private int enabledCount;
|
private int enabledCount;
|
||||||
private Loader loader;
|
private Loader loader;
|
||||||
private ManifestLoadable currentLoadable;
|
private ManifestLoadable<T> currentLoadable;
|
||||||
|
|
||||||
private int loadExceptionCount;
|
private int loadExceptionCount;
|
||||||
private long loadExceptionTimestamp;
|
private long loadExceptionTimestamp;
|
||||||
@ -204,7 +203,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
loader = new Loader("manifestLoader");
|
loader = new Loader("manifestLoader");
|
||||||
}
|
}
|
||||||
if (!loader.isLoading()) {
|
if (!loader.isLoading()) {
|
||||||
currentLoadable = new ManifestLoadable();
|
currentLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
|
||||||
loader.startLoading(currentLoadable, this);
|
loader.startLoading(currentLoadable, this);
|
||||||
notifyManifestRefreshStarted();
|
notifyManifestRefreshStarted();
|
||||||
}
|
}
|
||||||
@ -217,7 +216,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest = currentLoadable.result;
|
manifest = currentLoadable.getResult();
|
||||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||||
loadExceptionCount = 0;
|
loadExceptionCount = 0;
|
||||||
loadException = null;
|
loadException = null;
|
||||||
@ -244,6 +243,11 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
notifyManifestError(loadException);
|
notifyManifestError(loadException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* package */ void onSingleFetchCompleted(T result) {
|
||||||
|
manifest = result;
|
||||||
|
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||||
|
}
|
||||||
|
|
||||||
private long getRetryDelayMillis(long errorCount) {
|
private long getRetryDelayMillis(long errorCount) {
|
||||||
return Math.min((errorCount - 1) * 1000, 5000);
|
return Math.min((errorCount - 1) * 1000, 5000);
|
||||||
}
|
}
|
||||||
@ -286,13 +290,13 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
private final Looper callbackLooper;
|
private final Looper callbackLooper;
|
||||||
private final ManifestCallback<T> wrappedCallback;
|
private final ManifestCallback<T> wrappedCallback;
|
||||||
private final Loader singleUseLoader;
|
private final Loader singleUseLoader;
|
||||||
private final ManifestLoadable singleUseLoadable;
|
private final ManifestLoadable<T> singleUseLoadable;
|
||||||
|
|
||||||
public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) {
|
public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) {
|
||||||
this.callbackLooper = callbackLooper;
|
this.callbackLooper = callbackLooper;
|
||||||
this.wrappedCallback = wrappedCallback;
|
this.wrappedCallback = wrappedCallback;
|
||||||
singleUseLoader = new Loader("manifestLoader:single");
|
singleUseLoader = new Loader("manifestLoader:single");
|
||||||
singleUseLoadable = new ManifestLoadable();
|
singleUseLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startLoading() {
|
public void startLoading() {
|
||||||
@ -302,9 +306,9 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(Loadable loadable) {
|
public void onLoadCompleted(Loadable loadable) {
|
||||||
try {
|
try {
|
||||||
manifest = singleUseLoadable.result;
|
T result = singleUseLoadable.getResult();
|
||||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
onSingleFetchCompleted(result);
|
||||||
wrappedCallback.onManifest(contentId, singleUseLoadable.result);
|
wrappedCallback.onManifest(contentId, result);
|
||||||
} finally {
|
} finally {
|
||||||
releaseLoader();
|
releaseLoader();
|
||||||
}
|
}
|
||||||
@ -336,50 +340,22 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ManifestLoadable implements Loadable {
|
private static class ManifestLoadable<T> extends NetworkLoadable<T> {
|
||||||
|
|
||||||
private static final int TIMEOUT_MILLIS = 10000;
|
private final String contentId;
|
||||||
|
private final ManifestParser<T> parser;
|
||||||
|
|
||||||
/* package */ volatile T result;
|
public ManifestLoadable(String url, String userAgent, String contentId,
|
||||||
private volatile boolean isCanceled;
|
ManifestParser<T> parser) {
|
||||||
|
super(url, userAgent);
|
||||||
@Override
|
this.contentId = contentId;
|
||||||
public void cancelLoad() {
|
this.parser = parser;
|
||||||
// We don't actually cancel anything, but we need to record the cancellation so that
|
|
||||||
// isLoadCanceled can return the correct value.
|
|
||||||
isCanceled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLoadCanceled() {
|
protected T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
|
||||||
return isCanceled;
|
throws ParserException, IOException {
|
||||||
}
|
return parser.parse(inputStream, inputEncoding, contentId, Util.parseBaseUri(connectionUrl));
|
||||||
|
|
||||||
@Override
|
|
||||||
public void load() throws IOException, InterruptedException {
|
|
||||||
String inputEncoding;
|
|
||||||
InputStream inputStream = null;
|
|
||||||
try {
|
|
||||||
URLConnection connection = configureConnection(new URL(manifestUrl));
|
|
||||||
inputStream = connection.getInputStream();
|
|
||||||
inputEncoding = connection.getContentEncoding();
|
|
||||||
result = parser.parse(inputStream, inputEncoding, contentId,
|
|
||||||
Util.parseBaseUri(connection.getURL().toString()));
|
|
||||||
} finally {
|
|
||||||
if (inputStream != null) {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private URLConnection configureConnection(URL url) throws IOException {
|
|
||||||
URLConnection connection = url.openConnection();
|
|
||||||
connection.setConnectTimeout(TIMEOUT_MILLIS);
|
|
||||||
connection.setReadTimeout(TIMEOUT_MILLIS);
|
|
||||||
connection.setDoOutput(false);
|
|
||||||
connection.setRequestProperty("User-Agent", userAgent);
|
|
||||||
connection.connect();
|
|
||||||
return connection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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.ParserException;
|
||||||
|
import com.google.android.exoplayer.upstream.Loader.Loadable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Loadable} for loading an object over the network.
|
||||||
|
*
|
||||||
|
* @param <T> The type of the object being loaded.
|
||||||
|
*/
|
||||||
|
public abstract class NetworkLoadable<T> implements Loadable {
|
||||||
|
|
||||||
|
public static final int DEFAULT_TIMEOUT_MILLIS = 10000;
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
private final String userAgent;
|
||||||
|
private final int timeoutMillis;
|
||||||
|
|
||||||
|
private volatile T result;
|
||||||
|
private volatile boolean isCanceled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param url The url from which the object should be loaded.
|
||||||
|
* @param userAgent The user agent to use when requesting the object.
|
||||||
|
*/
|
||||||
|
public NetworkLoadable(String url, String userAgent) {
|
||||||
|
this(url, userAgent, DEFAULT_TIMEOUT_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param url The url from which the object should be loaded.
|
||||||
|
* @param userAgent The user agent to use when requesting the object.
|
||||||
|
* @param timeoutMillis The desired http timeout in milliseconds.
|
||||||
|
*/
|
||||||
|
public NetworkLoadable(String url, String userAgent, int timeoutMillis) {
|
||||||
|
this.url = url;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.timeoutMillis = timeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the loaded object, or null if an object has not been loaded.
|
||||||
|
*/
|
||||||
|
public final T getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void cancelLoad() {
|
||||||
|
// We don't actually cancel anything, but we need to record the cancellation so that
|
||||||
|
// isLoadCanceled can return the correct value.
|
||||||
|
isCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean isLoadCanceled() {
|
||||||
|
return isCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void load() throws IOException, InterruptedException {
|
||||||
|
String inputEncoding;
|
||||||
|
InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
URLConnection connection = configureConnection(new URL(url));
|
||||||
|
inputStream = connection.getInputStream();
|
||||||
|
inputEncoding = connection.getContentEncoding();
|
||||||
|
result = parse(connection.getURL().toString(), inputStream, inputEncoding);
|
||||||
|
} finally {
|
||||||
|
if (inputStream != null) {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the raw data into an object.
|
||||||
|
*
|
||||||
|
* @param connectionUrl The url, after any redirection has taken place.
|
||||||
|
* @param inputStream An {@link InputStream} from which the raw data can be read.
|
||||||
|
* @param inputEncoding The encoding of the raw data, if available.
|
||||||
|
* @return The parsed object.
|
||||||
|
* @throws ParserException If an error occurs parsing the data.
|
||||||
|
* @throws IOException If an error occurs reading data from the stream.
|
||||||
|
*/
|
||||||
|
protected abstract T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
|
||||||
|
throws ParserException, IOException;
|
||||||
|
|
||||||
|
private URLConnection configureConnection(URL url) throws IOException {
|
||||||
|
URLConnection connection = url.openConnection();
|
||||||
|
connection.setConnectTimeout(timeoutMillis);
|
||||||
|
connection.setReadTimeout(timeoutMillis);
|
||||||
|
connection.setDoOutput(false);
|
||||||
|
connection.setRequestProperty("User-Agent", userAgent);
|
||||||
|
connection.connect();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user