Commit 01e661f2 by tonihei Committed by Oliver Woodman

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
parent 3e08e421
/*
* 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;
}
}
}
......@@ -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) {
resolveUtcTimingElement(manifest.utcTiming);
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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment