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:
Oliver Woodman 2015-02-20 15:10:25 +00:00
parent 4076b08e4b
commit 6d14fc3330
6 changed files with 389 additions and 60 deletions

View File

@ -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.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.demo.DemoUtil;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
@ -59,6 +62,7 @@ import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.UnsupportedSchemeException;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.widget.TextView;
@ -70,7 +74,9 @@ import java.util.List;
* A {@link RendererBuilder} for DASH.
*/
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 VIDEO_BUFFER_SEGMENTS = 200;
@ -96,6 +102,9 @@ public class DashRendererBuilder implements RendererBuilder,
private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private MediaPresentationDescription manifest;
private long elapsedRealtimeOffset;
public DashRendererBuilder(String userAgent, String url, String contentId,
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) {
this.userAgent = userAgent;
@ -116,13 +125,36 @@ public class DashRendererBuilder implements RendererBuilder,
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
public void onManifestError(String contentId, IOException e) {
callback.onRenderersError(e);
}
@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);
Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
@ -207,7 +239,7 @@ public class DashRendererBuilder implements RendererBuilder,
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
LIVE_EDGE_LATENCY_MS);
LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO);
@ -230,7 +262,8 @@ public class DashRendererBuilder implements RendererBuilder,
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
format.audioSamplingRate + "Hz)");
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);
}
// 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);
textTrackNameList.add(representation.format.id);
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS));
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset));
}
}
}

View File

@ -86,6 +86,7 @@ public class DashChunkSource implements ChunkSource {
private final Evaluation evaluation;
private final StringBuilder headerBuilder;
private final long liveEdgeLatencyUs;
private final long elapsedRealtimeOffsetUs;
private final int maxWidth;
private final int maxHeight;
@ -140,7 +141,8 @@ public class DashChunkSource implements ChunkSource {
*/
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
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
* 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.
* @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,
int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) {
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000);
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000);
}
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
MediaPresentationDescription initialManifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
long liveEdgeLatencyUs) {
long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) {
this.manifestFetcher = manifestFetcher;
this.currentManifest = initialManifest;
this.adaptationSetIndex = adaptationSetIndex;
@ -181,6 +186,7 @@ public class DashChunkSource implements ChunkSource {
this.dataSource = dataSource;
this.evaluator = formatEvaluator;
this.liveEdgeLatencyUs = liveEdgeLatencyUs;
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs;
this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder();
@ -326,8 +332,12 @@ public class DashChunkSource implements ChunkSource {
return;
}
// TODO: Use UtcTimingElement where possible.
long nowUs = System.currentTimeMillis() * 1000;
long nowUs;
if (elapsedRealtimeOffsetUs != 0) {
nowUs = (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs;
} else {
nowUs = System.currentTimeMillis() * 1000;
}
int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum();

View File

@ -28,4 +28,9 @@ public class UtcTimingElement {
this.value = value;
}
@Override
public String toString() {
return schemeIdUri + ", " + value;
}
}

View File

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

View File

@ -15,6 +15,7 @@
*/
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.Loadable;
@ -25,8 +26,6 @@ import android.util.Pair;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.CancellationException;
/**
@ -84,7 +83,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
private int enabledCount;
private Loader loader;
private ManifestLoadable currentLoadable;
private ManifestLoadable<T> currentLoadable;
private int loadExceptionCount;
private long loadExceptionTimestamp;
@ -204,7 +203,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
loader = new Loader("manifestLoader");
}
if (!loader.isLoading()) {
currentLoadable = new ManifestLoadable();
currentLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
}
@ -217,7 +216,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
return;
}
manifest = currentLoadable.result;
manifest = currentLoadable.getResult();
manifestLoadTimestamp = SystemClock.elapsedRealtime();
loadExceptionCount = 0;
loadException = null;
@ -244,6 +243,11 @@ public class ManifestFetcher<T> implements Loader.Callback {
notifyManifestError(loadException);
}
/* package */ void onSingleFetchCompleted(T result) {
manifest = result;
manifestLoadTimestamp = SystemClock.elapsedRealtime();
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
@ -286,13 +290,13 @@ public class ManifestFetcher<T> implements Loader.Callback {
private final Looper callbackLooper;
private final ManifestCallback<T> wrappedCallback;
private final Loader singleUseLoader;
private final ManifestLoadable singleUseLoadable;
private final ManifestLoadable<T> singleUseLoadable;
public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) {
this.callbackLooper = callbackLooper;
this.wrappedCallback = wrappedCallback;
singleUseLoader = new Loader("manifestLoader:single");
singleUseLoadable = new ManifestLoadable();
singleUseLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
}
public void startLoading() {
@ -302,9 +306,9 @@ public class ManifestFetcher<T> implements Loader.Callback {
@Override
public void onLoadCompleted(Loadable loadable) {
try {
manifest = singleUseLoadable.result;
manifestLoadTimestamp = SystemClock.elapsedRealtime();
wrappedCallback.onManifest(contentId, singleUseLoadable.result);
T result = singleUseLoadable.getResult();
onSingleFetchCompleted(result);
wrappedCallback.onManifest(contentId, result);
} finally {
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;
private volatile boolean isCanceled;
@Override
public 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;
public ManifestLoadable(String url, String userAgent, String contentId,
ManifestParser<T> parser) {
super(url, userAgent);
this.contentId = contentId;
this.parser = parser;
}
@Override
public boolean isLoadCanceled() {
return isCanceled;
}
@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;
protected T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
throws ParserException, IOException {
return parser.parse(inputStream, inputEncoding, contentId, Util.parseBaseUri(connectionUrl));
}
}

View File

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