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:
parent
3e08e42168
commit
01e661f21a
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user