Add NTP client to get time offset for live streams without UTCTiming.

Dash live streams require that the client has an accurate wall clock
time and in absence of a UTCTiming element, this is assumed to be the
NTP time.

This change adds NTP time offset resolution for DASH live streams
without such timing elements.

PiperOrigin-RevId: 289098796
This commit is contained in:
tonihei 2020-01-10 16:35:18 +00:00 committed by Oliver Woodman
parent 3e08e42168
commit 01e661f21a
2 changed files with 337 additions and 3 deletions

View File

@ -0,0 +1,310 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
import android.os.SystemClock;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
import com.google.android.exoplayer2.upstream.Loader.Loadable;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Arrays;
/**
* Static utility to retrieve the device time offset using SNTP.
*
* <p>Based on the <a
* href="https://cs.android.com/android/_/android/platform/frameworks/base/+/ea1b85a5eb6e9dcddf7abc6c74479bfadf9017c7:core/java/android/net/SntpClient.java">Android
* framework SntpClient</a>.
*/
public final class SntpClient {
/** Callback for calls to {@link #initialize(Loader, InitializationCallback)}. */
public interface InitializationCallback {
/** Called when the device time offset has been initialized. */
void onInitialized();
/**
* Called when the device time offset failed to initialize.
*
* @param error The error that caused the initialization failure.
*/
void onInitializationFailed(IOException error);
}
private static final String NTP_HOST = "pool.ntp.org";
private static final int TIMEOUT_MS = 10_000;
private static final int ORIGINATE_TIME_OFFSET = 24;
private static final int RECEIVE_TIME_OFFSET = 32;
private static final int TRANSMIT_TIME_OFFSET = 40;
private static final int NTP_PACKET_SIZE = 48;
private static final int NTP_PORT = 123;
private static final int NTP_MODE_CLIENT = 3;
private static final int NTP_MODE_SERVER = 4;
private static final int NTP_MODE_BROADCAST = 5;
private static final int NTP_VERSION = 3;
private static final int NTP_LEAP_NOSYNC = 3;
private static final int NTP_STRATUM_DEATH = 0;
private static final int NTP_STRATUM_MAX = 15;
private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
private static final Object loaderLock = new Object();
private static final Object valueLock = new Object();
@GuardedBy("valueLock")
private static boolean isInitialized;
@GuardedBy("valueLock")
private static long elapsedRealtimeOffsetMs;
private SntpClient() {}
/**
* Returns whether the device time offset has already been loaded.
*
* <p>If {@code false}, use {@link #initialize(Loader, InitializationCallback)} to start the
* initialization.
*/
public static boolean isInitialized() {
synchronized (valueLock) {
return isInitialized;
}
}
/**
* Returns the offset between {@link SystemClock#elapsedRealtime()} and the NTP server time in
* milliseconds, or {@link C#TIME_UNSET} if {@link #isInitialized()} returns false.
*
* <p>The offset is calculated as {@code ntpServerTime - deviceElapsedRealTime}.
*/
public static long getElapsedRealtimeOffsetMs() {
synchronized (valueLock) {
return isInitialized ? elapsedRealtimeOffsetMs : C.TIME_UNSET;
}
}
/**
* Starts loading the device time offset.
*
* @param loader A {@link Loader} to use for loading the time offset, or null to create a new one.
* @param callback An optional {@link InitializationCallback} to be notified when the time offset
* has been initialized or initialization failed.
*/
public static void initialize(
@Nullable Loader loader, @Nullable InitializationCallback callback) {
if (isInitialized()) {
if (callback != null) {
callback.onInitialized();
}
return;
}
if (loader == null) {
loader = new Loader("SntpClient");
}
loader.startLoading(
new NtpTimeLoadable(), new NtpTimeCallback(callback), /* defaultMinRetryCount= */ 1);
}
private static long loadNtpTimeOffsetMs() throws IOException {
InetAddress address = InetAddress.getByName(NTP_HOST);
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(TIMEOUT_MS);
byte[] buffer = new byte[NTP_PACKET_SIZE];
DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);
// Set mode = 3 (client) and version = 3. Mode is in low 3 bits of the first byte and Version
// is in bits 3-5 of the first byte.
buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
// Get current time and write it to the request packet.
long requestTime = System.currentTimeMillis();
long requestTicks = SystemClock.elapsedRealtime();
writeTimestamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
socket.send(request);
// Read the response.
DatagramPacket response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
final long responseTicks = SystemClock.elapsedRealtime();
final long responseTime = requestTime + (responseTicks - requestTicks);
// Extract the results.
final byte leap = (byte) ((buffer[0] >> 6) & 0x3);
final byte mode = (byte) (buffer[0] & 0x7);
final int stratum = (int) (buffer[1] & 0xff);
final long originateTime = readTimestamp(buffer, ORIGINATE_TIME_OFFSET);
final long receiveTime = readTimestamp(buffer, RECEIVE_TIME_OFFSET);
final long transmitTime = readTimestamp(buffer, TRANSMIT_TIME_OFFSET);
// Do sanity check according to RFC.
checkValidServerReply(leap, mode, stratum, transmitTime);
// receiveTime = originateTime + transit + skew
// responseTime = transmitTime + transit - skew
// clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2
// = ((originateTime + transit + skew - originateTime) +
// (transmitTime - (transmitTime + transit - skew)))/2
// = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2
// = (transit + skew - transit + skew)/2
// = (2 * skew)/2 = skew
long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2;
// Save our results using the times on this side of the network latency (i.e. response rather
// than request time)
long ntpTime = responseTime + clockOffset;
long ntpTimeReference = responseTicks;
return ntpTime - ntpTimeReference;
}
}
private static long readTimestamp(byte[] buffer, int offset) {
long seconds = read32(buffer, offset);
long fraction = read32(buffer, offset + 4);
// Special case: zero means zero.
if (seconds == 0 && fraction == 0) {
return 0;
}
return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
}
private static void writeTimestamp(byte[] buffer, int offset, long time) {
// Special case: zero means zero.
if (time == 0) {
Arrays.fill(buffer, offset, offset + 8, (byte) 0x00);
return;
}
long seconds = time / 1000L;
long milliseconds = time - seconds * 1000L;
seconds += OFFSET_1900_TO_1970;
// Write seconds in big endian format.
buffer[offset++] = (byte) (seconds >> 24);
buffer[offset++] = (byte) (seconds >> 16);
buffer[offset++] = (byte) (seconds >> 8);
buffer[offset++] = (byte) (seconds >> 0);
long fraction = milliseconds * 0x100000000L / 1000L;
// Write fraction in big endian format.
buffer[offset++] = (byte) (fraction >> 24);
buffer[offset++] = (byte) (fraction >> 16);
buffer[offset++] = (byte) (fraction >> 8);
// Low order bits should be random data.
buffer[offset++] = (byte) (Math.random() * 255.0);
}
private static long read32(byte[] buffer, int offset) {
byte b0 = buffer[offset];
byte b1 = buffer[offset + 1];
byte b2 = buffer[offset + 2];
byte b3 = buffer[offset + 3];
// Convert signed bytes to unsigned values.
int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
return ((long) i0 << 24) + ((long) i1 << 16) + ((long) i2 << 8) + (long) i3;
}
private static void checkValidServerReply(byte leap, byte mode, int stratum, long transmitTime)
throws IOException {
if (leap == NTP_LEAP_NOSYNC) {
throw new IOException("SNTP: Unsynchronized server");
}
if ((mode != NTP_MODE_SERVER) && (mode != NTP_MODE_BROADCAST)) {
throw new IOException("SNTP: Untrusted mode: " + mode);
}
if ((stratum == NTP_STRATUM_DEATH) || (stratum > NTP_STRATUM_MAX)) {
throw new IOException("SNTP: Untrusted stratum: " + stratum);
}
if (transmitTime == 0) {
throw new IOException("SNTP: Zero transmitTime");
}
}
private static final class NtpTimeLoadable implements Loadable {
@Override
public void cancelLoad() {}
@Override
public void load() throws IOException {
// Synchronized to prevent redundant parallel requests.
synchronized (loaderLock) {
synchronized (valueLock) {
if (isInitialized) {
return;
}
}
long offsetMs = loadNtpTimeOffsetMs();
synchronized (valueLock) {
elapsedRealtimeOffsetMs = offsetMs;
isInitialized = true;
}
}
}
}
private static final class NtpTimeCallback implements Loader.Callback<Loadable> {
@Nullable private final InitializationCallback callback;
public NtpTimeCallback(@Nullable InitializationCallback callback) {
this.callback = callback;
}
@Override
public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
Assertions.checkState(SntpClient.isInitialized());
if (callback != null) {
callback.onInitialized();
}
}
@Override
public void onLoadCanceled(
Loadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {
// Ignore.
}
@Override
public LoadErrorAction onLoadError(
Loadable loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error,
int errorCount) {
if (callback != null) {
callback.onInitializationFailed(error);
}
return Loader.DONT_RETRY;
}
}
}

View File

@ -54,6 +54,7 @@ import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.SntpClient;
import com.google.android.exoplayer2.util.Util;
import java.io.BufferedReader;
import java.io.IOException;
@ -821,8 +822,12 @@ public final class DashMediaSource extends BaseMediaSource {
}
if (oldPeriodCount == 0) {
if (manifest.dynamic && manifest.utcTiming != null) {
if (manifest.dynamic) {
if (manifest.utcTiming != null) {
resolveUtcTimingElement(manifest.utcTiming);
} else {
loadNtpTimeOffset();
}
} else {
processManifest(true);
}
@ -915,6 +920,9 @@ public final class DashMediaSource extends BaseMediaSource {
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) {
resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser());
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:ntp:2014")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:ntp:2012")) {
loadNtpTimeOffset();
} else {
// Unsupported scheme.
onUtcTimestampResolutionError(new IOException("Unsupported UTC timing scheme"));
@ -936,13 +944,29 @@ public final class DashMediaSource extends BaseMediaSource {
C.DATA_TYPE_TIME_SYNCHRONIZATION, parser), new UtcTimestampCallback(), 1);
}
private void loadNtpTimeOffset() {
SntpClient.initialize(
loader,
new SntpClient.InitializationCallback() {
@Override
public void onInitialized() {
onUtcTimestampResolved(SntpClient.getElapsedRealtimeOffsetMs());
}
@Override
public void onInitializationFailed(IOException error) {
onUtcTimestampResolutionError(error);
}
});
}
private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) {
this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
processManifest(true);
}
private void onUtcTimestampResolutionError(IOException error) {
Log.e(TAG, "Failed to resolve UtcTiming element.", error);
Log.e(TAG, "Failed to resolve time offset.", error);
// Be optimistic and continue in the hope that the device clock is correct.
processManifest(true);
}