summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java201
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java49
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java384
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java277
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java312
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java31
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java651
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java42
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java384
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java404
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java61
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java68
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java177
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java84
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java43
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java465
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java519
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java34
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java134
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java323
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java586
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java211
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java119
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java95
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java158
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java104
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java161
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java186
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java279
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java2298
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java131
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java17
40 files changed, 9688 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java
new file mode 100644
index 0000000000..361b895695
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Assertions.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.Looper;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+
+/**
+ * Provides methods for asserting the truth of expressions and properties.
+ */
+public final class Assertions {
+
+ private Assertions() {}
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @param errorMessage The exception message if an exception is thrown. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalArgumentException If {@code expression} is false.
+ */
+ public static void checkArgument(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws {@link IndexOutOfBoundsException} if {@code index} falls outside the specified bounds.
+ *
+ * @param index The index to test.
+ * @param start The start of the allowed range (inclusive).
+ * @param limit The end of the allowed range (exclusive).
+ * @return The {@code index} that was validated.
+ * @throws IndexOutOfBoundsException If {@code index} falls outside the specified bounds.
+ */
+ public static int checkIndex(int index, int start, int limit) {
+ if (index < start || index >= limit) {
+ throw new IndexOutOfBoundsException();
+ }
+ return index;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code expression} evaluates to false.
+ *
+ * @param expression The expression to evaluate.
+ * @param errorMessage The exception message if an exception is thrown. The message is converted
+ * to a {@link String} using {@link String#valueOf(Object)}.
+ * @throws IllegalStateException If {@code expression} is false.
+ */
+ public static void checkState(boolean expression, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code reference} is null.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @return The non-null reference that was validated.
+ * @throws IllegalStateException If {@code reference} is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static <T> T checkStateNotNull(@Nullable T reference) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new IllegalStateException();
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if {@code reference} is null.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null reference that was validated.
+ * @throws IllegalStateException If {@code reference} is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static <T> T checkStateNotNull(@Nullable T reference, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link NullPointerException} if {@code reference} is null.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static <T> T checkNotNull(@Nullable T reference) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException();
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link NullPointerException} if {@code reference} is null.
+ *
+ * @param <T> The type of the reference.
+ * @param reference The reference.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null reference that was validated.
+ * @throws NullPointerException If {@code reference} is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static <T> T checkNotNull(@Nullable T reference, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.
+ *
+ * @param string The string to check.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static String checkNotEmpty(@Nullable String string) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
+ throw new IllegalArgumentException();
+ }
+ return string;
+ }
+
+ /**
+ * Throws {@link IllegalArgumentException} if {@code string} is null or zero length.
+ *
+ * @param string The string to check.
+ * @param errorMessage The exception message to use if the check fails. The message is converted
+ * to a string using {@link String#valueOf(Object)}.
+ * @return The non-null, non-empty string that was validated.
+ * @throws IllegalArgumentException If {@code string} is null or 0-length.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull({"#1"})
+ public static String checkNotEmpty(@Nullable String string, Object errorMessage) {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ return string;
+ }
+
+ /**
+ * Throws {@link IllegalStateException} if the calling thread is not the application's main
+ * thread.
+ *
+ * @throws IllegalStateException If the calling thread is not the application's main thread.
+ */
+ public static void checkMainThread() {
+ if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("Not in applications main thread");
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java
new file mode 100644
index 0000000000..d868a7d22a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A helper class for performing atomic operations on a file by creating a backup file until a write
+ * has successfully completed.
+ *
+ * <p>Atomic file guarantees file integrity by ensuring that a file has been completely written and
+ * synced to disk before removing its backup. As long as the backup file exists, the original file
+ * is considered to be invalid (left over from a previous attempt to write the file).
+ *
+ * <p>Atomic file does not confer any file locking semantics. Do not use this class when the file
+ * may be accessed or modified concurrently by multiple threads or processes. The caller is
+ * responsible for ensuring appropriate mutual exclusion invariants whenever it accesses the file.
+ */
+public final class AtomicFile {
+
+ private static final String TAG = "AtomicFile";
+
+ private final File baseName;
+ private final File backupName;
+
+ /**
+ * Create a new AtomicFile for a file located at the given File path. The secondary backup file
+ * will be the same file path with ".bak" appended.
+ */
+ public AtomicFile(File baseName) {
+ this.baseName = baseName;
+ backupName = new File(baseName.getPath() + ".bak");
+ }
+
+ /** Returns whether the file or its backup exists. */
+ public boolean exists() {
+ return baseName.exists() || backupName.exists();
+ }
+
+ /** Delete the atomic file. This deletes both the base and backup files. */
+ public void delete() {
+ baseName.delete();
+ backupName.delete();
+ }
+
+ /**
+ * Start a new write operation on the file. This returns an {@link OutputStream} to which you can
+ * write the new file data. If the whole data is written successfully you <em>must</em> call
+ * {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()}
+ * only to free up resources used by it.
+ *
+ * <p>Example usage:
+ *
+ * <pre>
+ * DataOutputStream dataOutput = null;
+ * try {
+ * OutputStream outputStream = atomicFile.startWrite();
+ * dataOutput = new DataOutputStream(outputStream); // Wrapper stream
+ * dataOutput.write(data1);
+ * dataOutput.write(data2);
+ * atomicFile.endWrite(dataOutput); // Pass wrapper stream
+ * } finally{
+ * if (dataOutput != null) {
+ * dataOutput.close();
+ * }
+ * }
+ * </pre>
+ *
+ * <p>Note that if another thread is currently performing a write, this will simply replace
+ * whatever that thread is writing with the new file being written by this thread, and when the
+ * other thread finishes the write the new write operation will no longer be safe (or will be
+ * lost). You must do your own threading protection for access to AtomicFile.
+ */
+ public OutputStream startWrite() throws IOException {
+ // Rename the current file so it may be used as a backup during the next read
+ if (baseName.exists()) {
+ if (!backupName.exists()) {
+ if (!baseName.renameTo(backupName)) {
+ Log.w(TAG, "Couldn't rename file " + baseName + " to backup file " + backupName);
+ }
+ } else {
+ baseName.delete();
+ }
+ }
+ OutputStream str;
+ try {
+ str = new AtomicFileOutputStream(baseName);
+ } catch (FileNotFoundException e) {
+ File parent = baseName.getParentFile();
+ if (parent == null || !parent.mkdirs()) {
+ throw new IOException("Couldn't create " + baseName, e);
+ }
+ // Try again now that we've created the parent directory.
+ try {
+ str = new AtomicFileOutputStream(baseName);
+ } catch (FileNotFoundException e2) {
+ throw new IOException("Couldn't create " + baseName, e2);
+ }
+ }
+ return str;
+ }
+
+ /**
+ * Call when you have successfully finished writing to the stream returned by {@link
+ * #startWrite()}. This will close, sync, and commit the new data. The next attempt to read the
+ * atomic file will return the new file stream.
+ *
+ * @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link
+ * #startWrite()}.
+ * @see #startWrite()
+ */
+ public void endWrite(OutputStream str) throws IOException {
+ str.close();
+ // If close() throws exception, the next line is skipped.
+ backupName.delete();
+ }
+
+ /**
+ * Open the atomic file for reading. If there previously was an incomplete write, this will roll
+ * back to the last good data before opening for read.
+ *
+ * <p>Note that if another thread is currently performing a write, this will incorrectly consider
+ * it to be in the state of a bad write and roll back, causing the new data currently being
+ * written to be dropped. You must do your own threading protection for access to AtomicFile.
+ */
+ public InputStream openRead() throws FileNotFoundException {
+ restoreBackup();
+ return new FileInputStream(baseName);
+ }
+
+ private void restoreBackup() {
+ if (backupName.exists()) {
+ baseName.delete();
+ backupName.renameTo(baseName);
+ }
+ }
+
+ private static final class AtomicFileOutputStream extends OutputStream {
+
+ private final FileOutputStream fileOutputStream;
+ private boolean closed = false;
+
+ public AtomicFileOutputStream(File file) throws FileNotFoundException {
+ fileOutputStream = new FileOutputStream(file);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ flush();
+ try {
+ fileOutputStream.getFD().sync();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to sync file descriptor:", e);
+ }
+ fileOutputStream.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ fileOutputStream.flush();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ fileOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ fileOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ fileOutputStream.write(b, off, len);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java
new file mode 100644
index 0000000000..4247e1db7b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Clock.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.Nullable;
+
+/**
+ * An interface through which system clocks can be read and {@link HandlerWrapper}s created. The
+ * {@link #DEFAULT} implementation must be used for all non-test cases.
+ */
+public interface Clock {
+
+ /**
+ * Default {@link Clock} to use for all non-test cases.
+ */
+ Clock DEFAULT = new SystemClock();
+
+ /** @see android.os.SystemClock#elapsedRealtime() */
+ long elapsedRealtime();
+
+ /** @see android.os.SystemClock#uptimeMillis() */
+ long uptimeMillis();
+
+ /** @see android.os.SystemClock#sleep(long) */
+ void sleep(long sleepTimeMs);
+
+ /**
+ * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling
+ * messages.
+ *
+ * @see Handler#Handler(Looper, Handler.Callback)
+ */
+ HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
new file mode 100644
index 0000000000..9c821c47c8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides static utility methods for manipulating various types of codec specific data.
+ */
+public final class CodecSpecificDataUtil {
+
+ private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ private static final int AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY = 0xF;
+
+ private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] {
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
+ };
+
+ private static final int AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID = -1;
+ /**
+ * In the channel configurations below, <A> indicates a single channel element; (A, B) indicates a
+ * channel pair element; and [A] indicates a low-frequency effects element.
+ * The speaker mapping short forms used are:
+ * - FC: front center
+ * - BC: back center
+ * - FL/FR: front left/right
+ * - FCL/FCR: front center left/right
+ * - FTL/FTR: front top left/right
+ * - SL/SR: back surround left/right
+ * - BL/BR: back left/right
+ * - LFE: low frequency effects
+ */
+ private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE =
+ new int[] {
+ 0,
+ 1, /* mono: <FC> */
+ 2, /* stereo: (FL, FR) */
+ 3, /* 3.0: <FC>, (FL, FR) */
+ 4, /* 4.0: <FC>, (FL, FR), <BC> */
+ 5, /* 5.0 back: <FC>, (FL, FR), (SL, SR) */
+ 6, /* 5.1 back: <FC>, (FL, FR), (SL, SR), <BC>, [LFE] */
+ 8, /* 7.1 wide back: <FC>, (FCL, FCR), (FL, FR), (SL, SR), [LFE] */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ 7, /* 6.1: <FC>, (FL, FR), (SL, SR), <RC>, [LFE] */
+ 8, /* 7.1: <FC>, (FL, FR), (SL, SR), (BL, BR), [LFE] */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID,
+ 8, /* 7.1 top: <FC>, (FL, FR), (SL, SR), [LFE], (FTL, FTR) */
+ AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID
+ };
+
+ // Advanced Audio Coding Low-Complexity profile.
+ private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2;
+ // Spectral Band Replication.
+ private static final int AUDIO_OBJECT_TYPE_SBR = 5;
+ // Error Resilient Bit-Sliced Arithmetic Coding.
+ private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22;
+ // Parametric Stereo.
+ private static final int AUDIO_OBJECT_TYPE_PS = 29;
+ // Escape code for extended audio object types.
+ private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31;
+
+ private CodecSpecificDataUtil() {}
+
+ /**
+ * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse.
+ * @return A pair consisting of the sample rate in Hz and the channel count.
+ * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.
+ */
+ public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig)
+ throws ParserException {
+ return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false);
+ }
+
+ /**
+ * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The
+ * position is advanced to the end of the AudioSpecificConfig.
+ * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for
+ * knowing the length of the configuration payload.
+ * @return A pair consisting of the sample rate in Hz and the channel count.
+ * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.
+ */
+ public static Pair<Integer, Integer> parseAacAudioSpecificConfig(
+ ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException {
+ int audioObjectType = getAacAudioObjectType(bitArray);
+ int sampleRate = getAacSamplingFrequency(bitArray);
+ int channelConfiguration = bitArray.readBits(4);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) {
+ // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with
+ // explicit signaling, we return the extension sampling frequency as the sample rate of the
+ // content; this is identical to the sample rate of the decoded output but may differ from
+ // the sample rate set above.
+ // Use the extensionSamplingFrequencyIndex.
+ sampleRate = getAacSamplingFrequency(bitArray);
+ audioObjectType = getAacAudioObjectType(bitArray);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) {
+ // Use the extensionChannelConfiguration.
+ channelConfiguration = bitArray.readBits(4);
+ }
+ }
+
+ if (forceReadToEnd) {
+ switch (audioObjectType) {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 6:
+ case 7:
+ case 17:
+ case 19:
+ case 20:
+ case 21:
+ case 22:
+ case 23:
+ parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration);
+ break;
+ default:
+ throw new ParserException("Unsupported audio object type: " + audioObjectType);
+ }
+ switch (audioObjectType) {
+ case 17:
+ case 19:
+ case 20:
+ case 21:
+ case 22:
+ case 23:
+ int epConfig = bitArray.readBits(2);
+ if (epConfig == 2 || epConfig == 3) {
+ throw new ParserException("Unsupported epConfig: " + epConfig);
+ }
+ break;
+ }
+ }
+ // For supported containers, bits_to_decode() is always 0.
+ int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration];
+ Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID);
+ return Pair.create(sampleRate, channelCount);
+ }
+
+ /**
+ * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param sampleRate The sample rate in Hz.
+ * @param channelCount The channel count.
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int channelCount) {
+ int sampleRateIndex = C.INDEX_UNSET;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) {
+ if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) {
+ sampleRateIndex = i;
+ }
+ }
+ int channelConfig = C.INDEX_UNSET;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) {
+ if (channelCount == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {
+ channelConfig = i;
+ }
+ }
+ if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) {
+ throw new IllegalArgumentException(
+ "Invalid sample rate or number of channels: " + sampleRate + ", " + channelCount);
+ }
+ return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig);
+ }
+
+ /**
+ * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioObjectType The audio object type.
+ * @param sampleRateIndex The sample rate index.
+ * @param channelConfig The channel configuration.
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sampleRateIndex,
+ int channelConfig) {
+ byte[] specificConfig = new byte[2];
+ specificConfig[0] = (byte) (((audioObjectType << 3) & 0xF8) | ((sampleRateIndex >> 1) & 0x07));
+ specificConfig[1] = (byte) (((sampleRateIndex << 7) & 0x80) | ((channelConfig << 3) & 0x78));
+ return specificConfig;
+ }
+
+ /**
+ * Parses an ALAC AudioSpecificConfig (i.e. an <a
+ * href="https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt">ALACSpecificConfig</a>).
+ *
+ * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse.
+ * @return A pair consisting of the sample rate in Hz and the channel count.
+ */
+ public static Pair<Integer, Integer> parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) {
+ ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig);
+ byteArray.setPosition(9);
+ int channelCount = byteArray.readUnsignedByte();
+ byteArray.setPosition(20);
+ int sampleRate = byteArray.readUnsignedIntToInt();
+ return Pair.create(sampleRate, channelCount);
+ }
+
+ /**
+ * Builds an RFC 6381 AVC codec string using the provided parameters.
+ *
+ * @param profileIdc The encoding profile.
+ * @param constraintsFlagsAndReservedZero2Bits The constraint flags followed by the reserved zero
+ * 2 bits, all contained in the least significant byte of the integer.
+ * @param levelIdc The encoding level.
+ * @return An RFC 6381 AVC codec string built using the provided parameters.
+ */
+ public static String buildAvcCodecString(
+ int profileIdc, int constraintsFlagsAndReservedZero2Bits, int levelIdc) {
+ return String.format(
+ "avc1.%02X%02X%02X", profileIdc, constraintsFlagsAndReservedZero2Bits, levelIdc);
+ }
+
+ /**
+ * Constructs a NAL unit consisting of the NAL start code followed by the specified data.
+ *
+ * @param data An array containing the data that should follow the NAL start code.
+ * @param offset The start offset into {@code data}.
+ * @param length The number of bytes to copy from {@code data}
+ * @return The constructed NAL unit.
+ */
+ public static byte[] buildNalUnit(byte[] data, int offset, int length) {
+ byte[] nalUnit = new byte[length + NAL_START_CODE.length];
+ System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length);
+ System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length);
+ return nalUnit;
+ }
+
+ /**
+ * Splits an array of NAL units.
+ *
+ * <p>If the input consists of NAL start code delimited units, then the returned array consists of
+ * the split NAL units, each of which is still prefixed with the NAL start code. For any other
+ * input, null is returned.
+ *
+ * @param data An array of data.
+ * @return The individual NAL units, or null if the input did not consist of NAL start code
+ * delimited units.
+ */
+ public static @Nullable byte[][] splitNalUnits(byte[] data) {
+ if (!isNalStartCode(data, 0)) {
+ // data does not consist of NAL start code delimited units.
+ return null;
+ }
+ List<Integer> starts = new ArrayList<>();
+ int nalUnitIndex = 0;
+ do {
+ starts.add(nalUnitIndex);
+ nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length);
+ } while (nalUnitIndex != C.INDEX_UNSET);
+ byte[][] split = new byte[starts.size()][];
+ for (int i = 0; i < starts.size(); i++) {
+ int startIndex = starts.get(i);
+ int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length;
+ byte[] nal = new byte[endIndex - startIndex];
+ System.arraycopy(data, startIndex, nal, 0, nal.length);
+ split[i] = nal;
+ }
+ return split;
+ }
+
+ /**
+ * Finds the next occurrence of the NAL start code from a given index.
+ *
+ * @param data The data in which to search.
+ * @param index The first index to test.
+ * @return The index of the first byte of the found start code, or {@link C#INDEX_UNSET}.
+ */
+ private static int findNalStartCode(byte[] data, int index) {
+ int endIndex = data.length - NAL_START_CODE.length;
+ for (int i = index; i <= endIndex; i++) {
+ if (isNalStartCode(data, i)) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Tests whether there exists a NAL start code at a given index.
+ *
+ * @param data The data.
+ * @param index The index to test.
+ * @return Whether there exists a start code that begins at {@code index}.
+ */
+ private static boolean isNalStartCode(byte[] data, int index) {
+ if (data.length - index <= NAL_START_CODE.length) {
+ return false;
+ }
+ for (int j = 0; j < NAL_START_CODE.length; j++) {
+ if (data[index + j] != NAL_START_CODE[j]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the AAC audio object type as specified in 14496-3 (2005) Table 1.14.
+ *
+ * @param bitArray The bit array containing the audio specific configuration.
+ * @return The audio object type.
+ */
+ private static int getAacAudioObjectType(ParsableBitArray bitArray) {
+ int audioObjectType = bitArray.readBits(5);
+ if (audioObjectType == AUDIO_OBJECT_TYPE_ESCAPE) {
+ audioObjectType = 32 + bitArray.readBits(6);
+ }
+ return audioObjectType;
+ }
+
+ /**
+ * Returns the AAC sampling frequency (or extension sampling frequency) as specified in 14496-3
+ * (2005) Table 1.13.
+ *
+ * @param bitArray The bit array containing the audio specific configuration.
+ * @return The sampling frequency.
+ */
+ private static int getAacSamplingFrequency(ParsableBitArray bitArray) {
+ int samplingFrequency;
+ int frequencyIndex = bitArray.readBits(4);
+ if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) {
+ samplingFrequency = bitArray.readBits(24);
+ } else {
+ Assertions.checkArgument(frequencyIndex < 13);
+ samplingFrequency = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
+ }
+ return samplingFrequency;
+ }
+
+ private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType,
+ int channelConfiguration) {
+ bitArray.skipBits(1); // frameLengthFlag.
+ boolean dependsOnCoreDecoder = bitArray.readBit();
+ if (dependsOnCoreDecoder) {
+ bitArray.skipBits(14); // coreCoderDelay.
+ }
+ boolean extensionFlag = bitArray.readBit();
+ if (channelConfiguration == 0) {
+ throw new UnsupportedOperationException(); // TODO: Implement programConfigElement();
+ }
+ if (audioObjectType == 6 || audioObjectType == 20) {
+ bitArray.skipBits(3); // layerNr.
+ }
+ if (extensionFlag) {
+ if (audioObjectType == 22) {
+ bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11).
+ }
+ if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20
+ || audioObjectType == 23) {
+ // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag,
+ // aacSpectralDataResilienceFlag.
+ bitArray.skipBits(3);
+ }
+ bitArray.skipBits(1); // extensionFlag3.
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java
new file mode 100644
index 0000000000..31b81fe16f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ColorParser.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.text.TextUtils;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for color expressions found in styling formats, e.g. TTML and CSS.
+ *
+ * @see <a href="https://w3c.github.io/webvtt/#styling">WebVTT CSS Styling</a>
+ * @see <a href="https://www.w3.org/TR/ttml2/">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>
+ */
+public final class ColorParser {
+
+ private static final String RGB = "rgb";
+ private static final String RGBA = "rgba";
+
+ private static final Pattern RGB_PATTERN = Pattern.compile(
+ "^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
+
+ private static final Pattern RGBA_PATTERN_INT_ALPHA = Pattern.compile(
+ "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$");
+
+ private static final Pattern RGBA_PATTERN_FLOAT_ALPHA = Pattern.compile(
+ "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d*\\.?\\d*?)\\)$");
+
+ private static final Map<String, Integer> COLOR_MAP;
+
+ /**
+ * Parses a TTML color expression.
+ *
+ * @param colorExpression The color expression.
+ * @return The parsed ARGB color.
+ */
+ public static int parseTtmlColor(String colorExpression) {
+ return parseColorInternal(colorExpression, false);
+ }
+
+ /**
+ * Parses a CSS color expression.
+ *
+ * @param colorExpression The color expression.
+ * @return The parsed ARGB color.
+ */
+ public static int parseCssColor(String colorExpression) {
+ return parseColorInternal(colorExpression, true);
+ }
+
+ private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) {
+ Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));
+ colorExpression = colorExpression.replace(" ", "");
+ if (colorExpression.charAt(0) == '#') {
+ // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF.
+ int color = (int) Long.parseLong(colorExpression.substring(1), 16);
+ if (colorExpression.length() == 7) {
+ // Set the alpha value
+ color |= 0xFF000000;
+ } else if (colorExpression.length() == 9) {
+ // We have #RRGGBBAA, but we need #AARRGGBB
+ color = ((color & 0xFF) << 24) | (color >>> 8);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ return color;
+ } else if (colorExpression.startsWith(RGBA)) {
+ Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA)
+ .matcher(colorExpression);
+ if (matcher.matches()) {
+ return argb(
+ alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4)))
+ : Integer.parseInt(matcher.group(4), 10),
+ Integer.parseInt(matcher.group(1), 10),
+ Integer.parseInt(matcher.group(2), 10),
+ Integer.parseInt(matcher.group(3), 10)
+ );
+ }
+ } else if (colorExpression.startsWith(RGB)) {
+ Matcher matcher = RGB_PATTERN.matcher(colorExpression);
+ if (matcher.matches()) {
+ return rgb(
+ Integer.parseInt(matcher.group(1), 10),
+ Integer.parseInt(matcher.group(2), 10),
+ Integer.parseInt(matcher.group(3), 10)
+ );
+ }
+ } else {
+ // we use our own color map
+ Integer color = COLOR_MAP.get(Util.toLowerInvariant(colorExpression));
+ if (color != null) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException();
+ }
+
+ private static int argb(int alpha, int red, int green, int blue) {
+ return (alpha << 24) | (red << 16) | (green << 8) | blue;
+ }
+
+ private static int rgb(int red, int green, int blue) {
+ return argb(0xFF, red, green, blue);
+ }
+
+ static {
+ COLOR_MAP = new HashMap<>();
+ COLOR_MAP.put("aliceblue", 0xFFF0F8FF);
+ COLOR_MAP.put("antiquewhite", 0xFFFAEBD7);
+ COLOR_MAP.put("aqua", 0xFF00FFFF);
+ COLOR_MAP.put("aquamarine", 0xFF7FFFD4);
+ COLOR_MAP.put("azure", 0xFFF0FFFF);
+ COLOR_MAP.put("beige", 0xFFF5F5DC);
+ COLOR_MAP.put("bisque", 0xFFFFE4C4);
+ COLOR_MAP.put("black", 0xFF000000);
+ COLOR_MAP.put("blanchedalmond", 0xFFFFEBCD);
+ COLOR_MAP.put("blue", 0xFF0000FF);
+ COLOR_MAP.put("blueviolet", 0xFF8A2BE2);
+ COLOR_MAP.put("brown", 0xFFA52A2A);
+ COLOR_MAP.put("burlywood", 0xFFDEB887);
+ COLOR_MAP.put("cadetblue", 0xFF5F9EA0);
+ COLOR_MAP.put("chartreuse", 0xFF7FFF00);
+ COLOR_MAP.put("chocolate", 0xFFD2691E);
+ COLOR_MAP.put("coral", 0xFFFF7F50);
+ COLOR_MAP.put("cornflowerblue", 0xFF6495ED);
+ COLOR_MAP.put("cornsilk", 0xFFFFF8DC);
+ COLOR_MAP.put("crimson", 0xFFDC143C);
+ COLOR_MAP.put("cyan", 0xFF00FFFF);
+ COLOR_MAP.put("darkblue", 0xFF00008B);
+ COLOR_MAP.put("darkcyan", 0xFF008B8B);
+ COLOR_MAP.put("darkgoldenrod", 0xFFB8860B);
+ COLOR_MAP.put("darkgray", 0xFFA9A9A9);
+ COLOR_MAP.put("darkgreen", 0xFF006400);
+ COLOR_MAP.put("darkgrey", 0xFFA9A9A9);
+ COLOR_MAP.put("darkkhaki", 0xFFBDB76B);
+ COLOR_MAP.put("darkmagenta", 0xFF8B008B);
+ COLOR_MAP.put("darkolivegreen", 0xFF556B2F);
+ COLOR_MAP.put("darkorange", 0xFFFF8C00);
+ COLOR_MAP.put("darkorchid", 0xFF9932CC);
+ COLOR_MAP.put("darkred", 0xFF8B0000);
+ COLOR_MAP.put("darksalmon", 0xFFE9967A);
+ COLOR_MAP.put("darkseagreen", 0xFF8FBC8F);
+ COLOR_MAP.put("darkslateblue", 0xFF483D8B);
+ COLOR_MAP.put("darkslategray", 0xFF2F4F4F);
+ COLOR_MAP.put("darkslategrey", 0xFF2F4F4F);
+ COLOR_MAP.put("darkturquoise", 0xFF00CED1);
+ COLOR_MAP.put("darkviolet", 0xFF9400D3);
+ COLOR_MAP.put("deeppink", 0xFFFF1493);
+ COLOR_MAP.put("deepskyblue", 0xFF00BFFF);
+ COLOR_MAP.put("dimgray", 0xFF696969);
+ COLOR_MAP.put("dimgrey", 0xFF696969);
+ COLOR_MAP.put("dodgerblue", 0xFF1E90FF);
+ COLOR_MAP.put("firebrick", 0xFFB22222);
+ COLOR_MAP.put("floralwhite", 0xFFFFFAF0);
+ COLOR_MAP.put("forestgreen", 0xFF228B22);
+ COLOR_MAP.put("fuchsia", 0xFFFF00FF);
+ COLOR_MAP.put("gainsboro", 0xFFDCDCDC);
+ COLOR_MAP.put("ghostwhite", 0xFFF8F8FF);
+ COLOR_MAP.put("gold", 0xFFFFD700);
+ COLOR_MAP.put("goldenrod", 0xFFDAA520);
+ COLOR_MAP.put("gray", 0xFF808080);
+ COLOR_MAP.put("green", 0xFF008000);
+ COLOR_MAP.put("greenyellow", 0xFFADFF2F);
+ COLOR_MAP.put("grey", 0xFF808080);
+ COLOR_MAP.put("honeydew", 0xFFF0FFF0);
+ COLOR_MAP.put("hotpink", 0xFFFF69B4);
+ COLOR_MAP.put("indianred", 0xFFCD5C5C);
+ COLOR_MAP.put("indigo", 0xFF4B0082);
+ COLOR_MAP.put("ivory", 0xFFFFFFF0);
+ COLOR_MAP.put("khaki", 0xFFF0E68C);
+ COLOR_MAP.put("lavender", 0xFFE6E6FA);
+ COLOR_MAP.put("lavenderblush", 0xFFFFF0F5);
+ COLOR_MAP.put("lawngreen", 0xFF7CFC00);
+ COLOR_MAP.put("lemonchiffon", 0xFFFFFACD);
+ COLOR_MAP.put("lightblue", 0xFFADD8E6);
+ COLOR_MAP.put("lightcoral", 0xFFF08080);
+ COLOR_MAP.put("lightcyan", 0xFFE0FFFF);
+ COLOR_MAP.put("lightgoldenrodyellow", 0xFFFAFAD2);
+ COLOR_MAP.put("lightgray", 0xFFD3D3D3);
+ COLOR_MAP.put("lightgreen", 0xFF90EE90);
+ COLOR_MAP.put("lightgrey", 0xFFD3D3D3);
+ COLOR_MAP.put("lightpink", 0xFFFFB6C1);
+ COLOR_MAP.put("lightsalmon", 0xFFFFA07A);
+ COLOR_MAP.put("lightseagreen", 0xFF20B2AA);
+ COLOR_MAP.put("lightskyblue", 0xFF87CEFA);
+ COLOR_MAP.put("lightslategray", 0xFF778899);
+ COLOR_MAP.put("lightslategrey", 0xFF778899);
+ COLOR_MAP.put("lightsteelblue", 0xFFB0C4DE);
+ COLOR_MAP.put("lightyellow", 0xFFFFFFE0);
+ COLOR_MAP.put("lime", 0xFF00FF00);
+ COLOR_MAP.put("limegreen", 0xFF32CD32);
+ COLOR_MAP.put("linen", 0xFFFAF0E6);
+ COLOR_MAP.put("magenta", 0xFFFF00FF);
+ COLOR_MAP.put("maroon", 0xFF800000);
+ COLOR_MAP.put("mediumaquamarine", 0xFF66CDAA);
+ COLOR_MAP.put("mediumblue", 0xFF0000CD);
+ COLOR_MAP.put("mediumorchid", 0xFFBA55D3);
+ COLOR_MAP.put("mediumpurple", 0xFF9370DB);
+ COLOR_MAP.put("mediumseagreen", 0xFF3CB371);
+ COLOR_MAP.put("mediumslateblue", 0xFF7B68EE);
+ COLOR_MAP.put("mediumspringgreen", 0xFF00FA9A);
+ COLOR_MAP.put("mediumturquoise", 0xFF48D1CC);
+ COLOR_MAP.put("mediumvioletred", 0xFFC71585);
+ COLOR_MAP.put("midnightblue", 0xFF191970);
+ COLOR_MAP.put("mintcream", 0xFFF5FFFA);
+ COLOR_MAP.put("mistyrose", 0xFFFFE4E1);
+ COLOR_MAP.put("moccasin", 0xFFFFE4B5);
+ COLOR_MAP.put("navajowhite", 0xFFFFDEAD);
+ COLOR_MAP.put("navy", 0xFF000080);
+ COLOR_MAP.put("oldlace", 0xFFFDF5E6);
+ COLOR_MAP.put("olive", 0xFF808000);
+ COLOR_MAP.put("olivedrab", 0xFF6B8E23);
+ COLOR_MAP.put("orange", 0xFFFFA500);
+ COLOR_MAP.put("orangered", 0xFFFF4500);
+ COLOR_MAP.put("orchid", 0xFFDA70D6);
+ COLOR_MAP.put("palegoldenrod", 0xFFEEE8AA);
+ COLOR_MAP.put("palegreen", 0xFF98FB98);
+ COLOR_MAP.put("paleturquoise", 0xFFAFEEEE);
+ COLOR_MAP.put("palevioletred", 0xFFDB7093);
+ COLOR_MAP.put("papayawhip", 0xFFFFEFD5);
+ COLOR_MAP.put("peachpuff", 0xFFFFDAB9);
+ COLOR_MAP.put("peru", 0xFFCD853F);
+ COLOR_MAP.put("pink", 0xFFFFC0CB);
+ COLOR_MAP.put("plum", 0xFFDDA0DD);
+ COLOR_MAP.put("powderblue", 0xFFB0E0E6);
+ COLOR_MAP.put("purple", 0xFF800080);
+ COLOR_MAP.put("rebeccapurple", 0xFF663399);
+ COLOR_MAP.put("red", 0xFFFF0000);
+ COLOR_MAP.put("rosybrown", 0xFFBC8F8F);
+ COLOR_MAP.put("royalblue", 0xFF4169E1);
+ COLOR_MAP.put("saddlebrown", 0xFF8B4513);
+ COLOR_MAP.put("salmon", 0xFFFA8072);
+ COLOR_MAP.put("sandybrown", 0xFFF4A460);
+ COLOR_MAP.put("seagreen", 0xFF2E8B57);
+ COLOR_MAP.put("seashell", 0xFFFFF5EE);
+ COLOR_MAP.put("sienna", 0xFFA0522D);
+ COLOR_MAP.put("silver", 0xFFC0C0C0);
+ COLOR_MAP.put("skyblue", 0xFF87CEEB);
+ COLOR_MAP.put("slateblue", 0xFF6A5ACD);
+ COLOR_MAP.put("slategray", 0xFF708090);
+ COLOR_MAP.put("slategrey", 0xFF708090);
+ COLOR_MAP.put("snow", 0xFFFFFAFA);
+ COLOR_MAP.put("springgreen", 0xFF00FF7F);
+ COLOR_MAP.put("steelblue", 0xFF4682B4);
+ COLOR_MAP.put("tan", 0xFFD2B48C);
+ COLOR_MAP.put("teal", 0xFF008080);
+ COLOR_MAP.put("thistle", 0xFFD8BFD8);
+ COLOR_MAP.put("tomato", 0xFFFF6347);
+ COLOR_MAP.put("transparent", 0x00000000);
+ COLOR_MAP.put("turquoise", 0xFF40E0D0);
+ COLOR_MAP.put("violet", 0xFFEE82EE);
+ COLOR_MAP.put("wheat", 0xFFF5DEB3);
+ COLOR_MAP.put("white", 0xFFFFFFFF);
+ COLOR_MAP.put("whitesmoke", 0xFFF5F5F5);
+ COLOR_MAP.put("yellow", 0xFFFFFF00);
+ COLOR_MAP.put("yellowgreen", 0xFF9ACD32);
+ }
+
+ private ColorParser() {
+ // Prevent instantiation.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java
new file mode 100644
index 0000000000..3866edced1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+/**
+ * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return
+ * whether they resulted in a change of state.
+ */
+public final class ConditionVariable {
+
+ private boolean isOpen;
+
+ /**
+ * Opens the condition and releases all threads that are blocked.
+ *
+ * @return True if the condition variable was opened. False if it was already open.
+ */
+ public synchronized boolean open() {
+ if (isOpen) {
+ return false;
+ }
+ isOpen = true;
+ notifyAll();
+ return true;
+ }
+
+ /**
+ * Closes the condition.
+ *
+ * @return True if the condition variable was closed. False if it was already closed.
+ */
+ public synchronized boolean close() {
+ boolean wasOpen = isOpen;
+ isOpen = false;
+ return wasOpen;
+ }
+
+ /**
+ * Blocks until the condition is opened.
+ *
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public synchronized void block() throws InterruptedException {
+ while (!isOpen) {
+ wait();
+ }
+ }
+
+ /**
+ * Blocks until the condition is opened or until {@code timeout} milliseconds have passed.
+ *
+ * @param timeout The maximum time to wait in milliseconds.
+ * @return True if the condition was opened, false if the call returns because of the timeout.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public synchronized boolean block(long timeout) throws InterruptedException {
+ long now = android.os.SystemClock.elapsedRealtime();
+ long end = now + timeout;
+ while (!isOpen && now < end) {
+ wait(end - now);
+ now = android.os.SystemClock.elapsedRealtime();
+ }
+ return isOpen;
+ }
+
+ /** Returns whether the condition is opened. */
+ public synchronized boolean isOpen() {
+ return isOpen;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
new file mode 100644
index 0000000000..1f48f718b7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EGLSurfaceTexture.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLSurface;
+import android.opengl.GLES20;
+import android.os.Handler;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Generates a {@link SurfaceTexture} using EGL/GLES functions. */
+@TargetApi(17)
+public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableListener, Runnable {
+
+ /** Listener to be called when the texture image on {@link SurfaceTexture} has been updated. */
+ public interface TextureImageListener {
+ /** Called when the {@link SurfaceTexture} receives a new frame from its image producer. */
+ void onFrameAvailable();
+ }
+
+ /**
+ * Secure mode to be used by the EGL surface and context. One of {@link #SECURE_MODE_NONE}, {@link
+ * #SECURE_MODE_SURFACELESS_CONTEXT} or {@link #SECURE_MODE_PROTECTED_PBUFFER}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER})
+ public @interface SecureMode {}
+
+ /** No secure EGL surface and context required. */
+ public static final int SECURE_MODE_NONE = 0;
+ /** Creating a surfaceless, secured EGL context. */
+ public static final int SECURE_MODE_SURFACELESS_CONTEXT = 1;
+ /** Creating a secure surface backed by a pixel buffer. */
+ public static final int SECURE_MODE_PROTECTED_PBUFFER = 2;
+
+ private static final int EGL_SURFACE_WIDTH = 1;
+ private static final int EGL_SURFACE_HEIGHT = 1;
+
+ private static final int[] EGL_CONFIG_ATTRIBUTES =
+ new int[] {
+ EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
+ EGL14.EGL_RED_SIZE, 8,
+ EGL14.EGL_GREEN_SIZE, 8,
+ EGL14.EGL_BLUE_SIZE, 8,
+ EGL14.EGL_ALPHA_SIZE, 8,
+ EGL14.EGL_DEPTH_SIZE, 0,
+ EGL14.EGL_CONFIG_CAVEAT, EGL14.EGL_NONE,
+ EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
+ EGL14.EGL_NONE
+ };
+
+ private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
+
+ /** A runtime exception to be thrown if some EGL operations failed. */
+ public static final class GlException extends RuntimeException {
+ private GlException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final Handler handler;
+ private final int[] textureIdHolder;
+ @Nullable private final TextureImageListener callback;
+
+ @Nullable private EGLDisplay display;
+ @Nullable private EGLContext context;
+ @Nullable private EGLSurface surface;
+ @Nullable private SurfaceTexture texture;
+
+ /**
+ * @param handler The {@link Handler} that will be used to call {@link
+ * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that
+ * {@link #init(int)} has to be called on the same looper thread as the {@link Handler}'s
+ * looper.
+ */
+ public EGLSurfaceTexture(Handler handler) {
+ this(handler, /* callback= */ null);
+ }
+
+ /**
+ * @param handler The {@link Handler} that will be used to call {@link
+ * SurfaceTexture#updateTexImage()} to update images on the {@link SurfaceTexture}. Note that
+ * {@link #init(int)} has to be called on the same looper thread as the looper of the {@link
+ * Handler}.
+ * @param callback The {@link TextureImageListener} to be called when the texture image on {@link
+ * SurfaceTexture} has been updated. This callback will be called on the same handler thread
+ * as the {@code handler}.
+ */
+ public EGLSurfaceTexture(Handler handler, @Nullable TextureImageListener callback) {
+ this.handler = handler;
+ this.callback = callback;
+ textureIdHolder = new int[1];
+ }
+
+ /**
+ * Initializes required EGL parameters and creates the {@link SurfaceTexture}.
+ *
+ * @param secureMode The {@link SecureMode} to be used for EGL surface.
+ */
+ public void init(@SecureMode int secureMode) {
+ display = getDefaultDisplay();
+ EGLConfig config = chooseEGLConfig(display);
+ context = createEGLContext(display, config, secureMode);
+ surface = createEGLSurface(display, config, context, secureMode);
+ generateTextureIds(textureIdHolder);
+ texture = new SurfaceTexture(textureIdHolder[0]);
+ texture.setOnFrameAvailableListener(this);
+ }
+
+ /** Releases all allocated resources. */
+ @SuppressWarnings({"nullness:argument.type.incompatible"})
+ public void release() {
+ handler.removeCallbacks(this);
+ try {
+ if (texture != null) {
+ texture.release();
+ GLES20.glDeleteTextures(1, textureIdHolder, 0);
+ }
+ } finally {
+ if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) {
+ EGL14.eglMakeCurrent(
+ display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
+ }
+ if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) {
+ EGL14.eglDestroySurface(display, surface);
+ }
+ if (context != null) {
+ EGL14.eglDestroyContext(display, context);
+ }
+ // EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]).
+ if (Util.SDK_INT >= 19) {
+ EGL14.eglReleaseThread();
+ }
+ if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) {
+ // Android is unusual in that it uses a reference-counted EGLDisplay. So for
+ // every eglInitialize() we need an eglTerminate().
+ EGL14.eglTerminate(display);
+ }
+ display = null;
+ context = null;
+ surface = null;
+ texture = null;
+ }
+ }
+
+ /**
+ * Returns the wrapped {@link SurfaceTexture}. This can only be called after {@link #init(int)}.
+ */
+ public SurfaceTexture getSurfaceTexture() {
+ return Assertions.checkNotNull(texture);
+ }
+
+ // SurfaceTexture.OnFrameAvailableListener
+
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ handler.post(this);
+ }
+
+ // Runnable
+
+ @Override
+ public void run() {
+ // Run on the provided handler thread when a new image frame is available.
+ dispatchOnFrameAvailable();
+ if (texture != null) {
+ try {
+ texture.updateTexImage();
+ } catch (RuntimeException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private void dispatchOnFrameAvailable() {
+ if (callback != null) {
+ callback.onFrameAvailable();
+ }
+ }
+
+ private static EGLDisplay getDefaultDisplay() {
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ if (display == null) {
+ throw new GlException("eglGetDisplay failed");
+ }
+
+ int[] version = new int[2];
+ boolean eglInitialized =
+ EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);
+ if (!eglInitialized) {
+ throw new GlException("eglInitialize failed");
+ }
+ return display;
+ }
+
+ private static EGLConfig chooseEGLConfig(EGLDisplay display) {
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfigs = new int[1];
+ boolean success =
+ EGL14.eglChooseConfig(
+ display,
+ EGL_CONFIG_ATTRIBUTES,
+ /* attrib_listOffset= */ 0,
+ configs,
+ /* configsOffset= */ 0,
+ /* config_size= */ 1,
+ numConfigs,
+ /* num_configOffset= */ 0);
+ if (!success || numConfigs[0] <= 0 || configs[0] == null) {
+ throw new GlException(
+ Util.formatInvariant(
+ /* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
+ success, numConfigs[0], configs[0]));
+ }
+
+ return configs[0];
+ }
+
+ private static EGLContext createEGLContext(
+ EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
+ int[] glAttributes;
+ if (secureMode == SECURE_MODE_NONE) {
+ glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
+ } else {
+ glAttributes =
+ new int[] {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION,
+ 2,
+ EGL_PROTECTED_CONTENT_EXT,
+ EGL14.EGL_TRUE,
+ EGL14.EGL_NONE
+ };
+ }
+ EGLContext context =
+ EGL14.eglCreateContext(
+ display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
+ if (context == null) {
+ throw new GlException("eglCreateContext failed");
+ }
+ return context;
+ }
+
+ private static EGLSurface createEGLSurface(
+ EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
+ EGLSurface surface;
+ if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
+ surface = EGL14.EGL_NO_SURFACE;
+ } else {
+ int[] pbufferAttributes;
+ if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) {
+ pbufferAttributes =
+ new int[] {
+ EGL14.EGL_WIDTH,
+ EGL_SURFACE_WIDTH,
+ EGL14.EGL_HEIGHT,
+ EGL_SURFACE_HEIGHT,
+ EGL_PROTECTED_CONTENT_EXT,
+ EGL14.EGL_TRUE,
+ EGL14.EGL_NONE
+ };
+ } else {
+ pbufferAttributes =
+ new int[] {
+ EGL14.EGL_WIDTH,
+ EGL_SURFACE_WIDTH,
+ EGL14.EGL_HEIGHT,
+ EGL_SURFACE_HEIGHT,
+ EGL14.EGL_NONE
+ };
+ }
+ surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
+ if (surface == null) {
+ throw new GlException("eglCreatePbufferSurface failed");
+ }
+ }
+
+ boolean eglMadeCurrent =
+ EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
+ if (!eglMadeCurrent) {
+ throw new GlException("eglMakeCurrent failed");
+ }
+ return surface;
+ }
+
+ private static void generateTextureIds(int[] textureIdHolder) {
+ GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
+ GlUtil.checkGlError();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java
new file mode 100644
index 0000000000..0eca418cd8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ErrorMessageProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.util.Pair;
+
+/** Converts throwables into error codes and user readable error messages. */
+public interface ErrorMessageProvider<T extends Throwable> {
+
+ /**
+ * Returns a pair consisting of an error code and a user readable error message for the given
+ * throwable.
+ *
+ * @param throwable The throwable for which an error code and message should be generated.
+ * @return A pair consisting of an error code and a user readable error message.
+ */
+ Pair<Integer, String> getErrorMessage(T throwable);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java
new file mode 100644
index 0000000000..6e9a3798bf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventDispatcher.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.Handler;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Event dispatcher which allows listener registration.
+ *
+ * @param <T> The type of listener.
+ */
+public final class EventDispatcher<T> {
+
+ /** Functional interface to send an event. */
+ public interface Event<T> {
+
+ /**
+ * Sends the event to a listener.
+ *
+ * @param listener The listener to send the event to.
+ */
+ void sendTo(T listener);
+ }
+
+ /** The list of listeners and handlers. */
+ private final CopyOnWriteArrayList<HandlerAndListener<T>> listeners;
+
+ /** Creates an event dispatcher. */
+ public EventDispatcher() {
+ listeners = new CopyOnWriteArrayList<>();
+ }
+
+ /** Adds a listener to the event dispatcher. */
+ public void addListener(Handler handler, T eventListener) {
+ Assertions.checkArgument(handler != null && eventListener != null);
+ removeListener(eventListener);
+ listeners.add(new HandlerAndListener<>(handler, eventListener));
+ }
+
+ /** Removes a listener from the event dispatcher. */
+ public void removeListener(T eventListener) {
+ for (HandlerAndListener<T> handlerAndListener : listeners) {
+ if (handlerAndListener.listener == eventListener) {
+ handlerAndListener.release();
+ listeners.remove(handlerAndListener);
+ }
+ }
+ }
+
+ /**
+ * Dispatches an event to all registered listeners.
+ *
+ * @param event The {@link Event}.
+ */
+ public void dispatch(Event<T> event) {
+ for (HandlerAndListener<T> handlerAndListener : listeners) {
+ handlerAndListener.dispatch(event);
+ }
+ }
+
+ private static final class HandlerAndListener<T> {
+
+ private final Handler handler;
+ private final T listener;
+
+ private boolean released;
+
+ public HandlerAndListener(Handler handler, T eventListener) {
+ this.handler = handler;
+ this.listener = eventListener;
+ }
+
+ public void release() {
+ released = true;
+ }
+
+ public void dispatch(Event<T> event) {
+ handler.post(
+ () -> {
+ if (!released) {
+ event.sendTo(listener);
+ }
+ });
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java
new file mode 100644
index 0000000000..0c2a6abcf1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/EventLogger.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.view.Surface;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import java.io.IOException;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/** Logs events from {@link Player} and other core components using {@link Log}. */
+@SuppressWarnings("UngroupedOverloads")
+public class EventLogger implements AnalyticsListener {
+
+ private static final String DEFAULT_TAG = "EventLogger";
+ private static final int MAX_TIMELINE_ITEM_LINES = 3;
+ private static final NumberFormat TIME_FORMAT;
+ static {
+ TIME_FORMAT = NumberFormat.getInstance(Locale.US);
+ TIME_FORMAT.setMinimumFractionDigits(2);
+ TIME_FORMAT.setMaximumFractionDigits(2);
+ TIME_FORMAT.setGroupingUsed(false);
+ }
+
+ @Nullable private final MappingTrackSelector trackSelector;
+ private final String tag;
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+ private final long startTimeMs;
+
+ /**
+ * Creates event logger.
+ *
+ * @param trackSelector The mapping track selector used by the player. May be null if detailed
+ * logging of track mapping is not required.
+ */
+ public EventLogger(@Nullable MappingTrackSelector trackSelector) {
+ this(trackSelector, DEFAULT_TAG);
+ }
+
+ /**
+ * Creates event logger.
+ *
+ * @param trackSelector The mapping track selector used by the player. May be null if detailed
+ * logging of track mapping is not required.
+ * @param tag The tag used for logging.
+ */
+ public EventLogger(@Nullable MappingTrackSelector trackSelector, String tag) {
+ this.trackSelector = trackSelector;
+ this.tag = tag;
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ startTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ // AnalyticsListener
+
+ @Override
+ public void onLoadingChanged(EventTime eventTime, boolean isLoading) {
+ logd(eventTime, "loading", Boolean.toString(isLoading));
+ }
+
+ @Override
+ public void onPlayerStateChanged(
+ EventTime eventTime, boolean playWhenReady, @Player.State int state) {
+ logd(eventTime, "state", playWhenReady + ", " + getStateString(state));
+ }
+
+ @Override
+ public void onPlaybackSuppressionReasonChanged(
+ EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) {
+ logd(
+ eventTime,
+ "playbackSuppressionReason",
+ getPlaybackSuppressionReasonString(playbackSuppressionReason));
+ }
+
+ @Override
+ public void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {
+ logd(eventTime, "isPlaying", Boolean.toString(isPlaying));
+ }
+
+ @Override
+ public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) {
+ logd(eventTime, "repeatMode", getRepeatModeString(repeatMode));
+ }
+
+ @Override
+ public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {
+ logd(eventTime, "shuffleModeEnabled", Boolean.toString(shuffleModeEnabled));
+ }
+
+ @Override
+ public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {
+ logd(eventTime, "positionDiscontinuity", getDiscontinuityReasonString(reason));
+ }
+
+ @Override
+ public void onSeekStarted(EventTime eventTime) {
+ logd(eventTime, "seekStarted");
+ }
+
+ @Override
+ public void onPlaybackParametersChanged(
+ EventTime eventTime, PlaybackParameters playbackParameters) {
+ logd(
+ eventTime,
+ "playbackParameters",
+ Util.formatInvariant(
+ "speed=%.2f, pitch=%.2f, skipSilence=%s",
+ playbackParameters.speed, playbackParameters.pitch, playbackParameters.skipSilence));
+ }
+
+ @Override
+ public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) {
+ int periodCount = eventTime.timeline.getPeriodCount();
+ int windowCount = eventTime.timeline.getWindowCount();
+ logd(
+ "timeline ["
+ + getEventTimeString(eventTime)
+ + ", periodCount="
+ + periodCount
+ + ", windowCount="
+ + windowCount
+ + ", reason="
+ + getTimelineChangeReasonString(reason));
+ for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {
+ eventTime.timeline.getPeriod(i, period);
+ logd(" " + "period [" + getTimeString(period.getDurationMs()) + "]");
+ }
+ if (periodCount > MAX_TIMELINE_ITEM_LINES) {
+ logd(" ...");
+ }
+ for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {
+ eventTime.timeline.getWindow(i, window);
+ logd(
+ " "
+ + "window ["
+ + getTimeString(window.getDurationMs())
+ + ", "
+ + window.isSeekable
+ + ", "
+ + window.isDynamic
+ + "]");
+ }
+ if (windowCount > MAX_TIMELINE_ITEM_LINES) {
+ logd(" ...");
+ }
+ logd("]");
+ }
+
+ @Override
+ public void onPlayerError(EventTime eventTime, ExoPlaybackException e) {
+ loge(eventTime, "playerFailed", e);
+ }
+
+ @Override
+ public void onTracksChanged(
+ EventTime eventTime, TrackGroupArray ignored, TrackSelectionArray trackSelections) {
+ MappedTrackInfo mappedTrackInfo =
+ trackSelector != null ? trackSelector.getCurrentMappedTrackInfo() : null;
+ if (mappedTrackInfo == null) {
+ logd(eventTime, "tracks", "[]");
+ return;
+ }
+ logd("tracks [" + getEventTimeString(eventTime));
+ // Log tracks associated to renderers.
+ int rendererCount = mappedTrackInfo.getRendererCount();
+ for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
+ TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
+ TrackSelection trackSelection = trackSelections.get(rendererIndex);
+ if (rendererTrackGroups.length > 0) {
+ logd(" Renderer:" + rendererIndex + " [");
+ for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
+ TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
+ String adaptiveSupport =
+ getAdaptiveSupportString(
+ trackGroup.length,
+ mappedTrackInfo.getAdaptiveSupport(
+ rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false));
+ logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
+ String formatSupport =
+ RendererCapabilities.getFormatSupportString(
+ mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex));
+ logd(
+ " "
+ + status
+ + " Track:"
+ + trackIndex
+ + ", "
+ + Format.toLogString(trackGroup.getFormat(trackIndex))
+ + ", supported="
+ + formatSupport);
+ }
+ logd(" ]");
+ }
+ // Log metadata for at most one of the tracks selected for the renderer.
+ if (trackSelection != null) {
+ for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {
+ Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;
+ if (metadata != null) {
+ logd(" Metadata [");
+ printMetadata(metadata, " ");
+ logd(" ]");
+ break;
+ }
+ }
+ }
+ logd(" ]");
+ }
+ }
+ // Log tracks not associated with a renderer.
+ TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups();
+ if (unassociatedTrackGroups.length > 0) {
+ logd(" Renderer:None [");
+ for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
+ logd(" Group:" + groupIndex + " [");
+ TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ String status = getTrackStatusString(false);
+ String formatSupport =
+ RendererCapabilities.getFormatSupportString(
+ RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
+ logd(
+ " "
+ + status
+ + " Track:"
+ + trackIndex
+ + ", "
+ + Format.toLogString(trackGroup.getFormat(trackIndex))
+ + ", supported="
+ + formatSupport);
+ }
+ logd(" ]");
+ }
+ logd(" ]");
+ }
+ logd("]");
+ }
+
+ @Override
+ public void onSeekProcessed(EventTime eventTime) {
+ logd(eventTime, "seekProcessed");
+ }
+
+ @Override
+ public void onMetadata(EventTime eventTime, Metadata metadata) {
+ logd("metadata [" + getEventTimeString(eventTime));
+ printMetadata(metadata, " ");
+ logd("]");
+ }
+
+ @Override
+ public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) {
+ logd(eventTime, "decoderEnabled", Util.getTrackTypeString(trackType));
+ }
+
+ @Override
+ public void onAudioSessionId(EventTime eventTime, int audioSessionId) {
+ logd(eventTime, "audioSessionId", Integer.toString(audioSessionId));
+ }
+
+ @Override
+ public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {
+ logd(
+ eventTime,
+ "audioAttributes",
+ audioAttributes.contentType
+ + ","
+ + audioAttributes.flags
+ + ","
+ + audioAttributes.usage
+ + ","
+ + audioAttributes.allowedCapturePolicy);
+ }
+
+ @Override
+ public void onVolumeChanged(EventTime eventTime, float volume) {
+ logd(eventTime, "volume", Float.toString(volume));
+ }
+
+ @Override
+ public void onDecoderInitialized(
+ EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {
+ logd(eventTime, "decoderInitialized", Util.getTrackTypeString(trackType) + ", " + decoderName);
+ }
+
+ @Override
+ public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {
+ logd(
+ eventTime,
+ "decoderInputFormat",
+ Util.getTrackTypeString(trackType) + ", " + Format.toLogString(format));
+ }
+
+ @Override
+ public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) {
+ logd(eventTime, "decoderDisabled", Util.getTrackTypeString(trackType));
+ }
+
+ @Override
+ public void onAudioUnderrun(
+ EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ loge(
+ eventTime,
+ "audioTrackUnderrun",
+ bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]",
+ null);
+ }
+
+ @Override
+ public void onDroppedVideoFrames(EventTime eventTime, int count, long elapsedMs) {
+ logd(eventTime, "droppedFrames", Integer.toString(count));
+ }
+
+ @Override
+ public void onVideoSizeChanged(
+ EventTime eventTime,
+ int width,
+ int height,
+ int unappliedRotationDegrees,
+ float pixelWidthHeightRatio) {
+ logd(eventTime, "videoSize", width + ", " + height);
+ }
+
+ @Override
+ public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {
+ logd(eventTime, "renderedFirstFrame", String.valueOf(surface));
+ }
+
+ @Override
+ public void onMediaPeriodCreated(EventTime eventTime) {
+ logd(eventTime, "mediaPeriodCreated");
+ }
+
+ @Override
+ public void onMediaPeriodReleased(EventTime eventTime) {
+ logd(eventTime, "mediaPeriodReleased");
+ }
+
+ @Override
+ public void onLoadStarted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLoadError(
+ EventTime eventTime,
+ LoadEventInfo loadEventInfo,
+ MediaLoadData mediaLoadData,
+ IOException error,
+ boolean wasCanceled) {
+ printInternalError(eventTime, "loadError", error);
+ }
+
+ @Override
+ public void onLoadCanceled(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onLoadCompleted(
+ EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onReadingStarted(EventTime eventTime) {
+ logd(eventTime, "mediaPeriodReadingStarted");
+ }
+
+ @Override
+ public void onBandwidthEstimate(
+ EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {
+ logd(eventTime, "surfaceSize", width + ", " + height);
+ }
+
+ @Override
+ public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {
+ logd(eventTime, "upstreamDiscarded", Format.toLogString(mediaLoadData.trackFormat));
+ }
+
+ @Override
+ public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
+ logd(eventTime, "downstreamFormat", Format.toLogString(mediaLoadData.trackFormat));
+ }
+
+ @Override
+ public void onDrmSessionAcquired(EventTime eventTime) {
+ logd(eventTime, "drmSessionAcquired");
+ }
+
+ @Override
+ public void onDrmSessionManagerError(EventTime eventTime, Exception e) {
+ printInternalError(eventTime, "drmSessionManagerError", e);
+ }
+
+ @Override
+ public void onDrmKeysRestored(EventTime eventTime) {
+ logd(eventTime, "drmKeysRestored");
+ }
+
+ @Override
+ public void onDrmKeysRemoved(EventTime eventTime) {
+ logd(eventTime, "drmKeysRemoved");
+ }
+
+ @Override
+ public void onDrmKeysLoaded(EventTime eventTime) {
+ logd(eventTime, "drmKeysLoaded");
+ }
+
+ @Override
+ public void onDrmSessionReleased(EventTime eventTime) {
+ logd(eventTime, "drmSessionReleased");
+ }
+
+ /**
+ * Logs a debug message.
+ *
+ * @param msg The message to log.
+ */
+ protected void logd(String msg) {
+ Log.d(tag, msg);
+ }
+
+ /**
+ * Logs an error message.
+ *
+ * @param msg The message to log.
+ */
+ protected void loge(String msg) {
+ Log.e(tag, msg);
+ }
+
+ // Internal methods
+
+ private void logd(EventTime eventTime, String eventName) {
+ logd(getEventString(eventTime, eventName, /* eventDescription= */ null, /* throwable= */ null));
+ }
+
+ private void logd(EventTime eventTime, String eventName, String eventDescription) {
+ logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null));
+ }
+
+ private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) {
+ loge(getEventString(eventTime, eventName, /* eventDescription= */ null, throwable));
+ }
+
+ private void loge(
+ EventTime eventTime,
+ String eventName,
+ String eventDescription,
+ @Nullable Throwable throwable) {
+ loge(getEventString(eventTime, eventName, eventDescription, throwable));
+ }
+
+ private void printInternalError(EventTime eventTime, String type, Exception e) {
+ loge(eventTime, "internalError", type, e);
+ }
+
+ private void printMetadata(Metadata metadata, String prefix) {
+ for (int i = 0; i < metadata.length(); i++) {
+ logd(prefix + metadata.get(i));
+ }
+ }
+
+ private String getEventString(
+ EventTime eventTime,
+ String eventName,
+ @Nullable String eventDescription,
+ @Nullable Throwable throwable) {
+ String eventString = eventName + " [" + getEventTimeString(eventTime);
+ if (eventDescription != null) {
+ eventString += ", " + eventDescription;
+ }
+ @Nullable String throwableString = Log.getThrowableString(throwable);
+ if (!TextUtils.isEmpty(throwableString)) {
+ eventString += "\n " + throwableString.replace("\n", "\n ") + '\n';
+ }
+ eventString += "]";
+ return eventString;
+ }
+
+ private String getEventTimeString(EventTime eventTime) {
+ String windowPeriodString = "window=" + eventTime.windowIndex;
+ if (eventTime.mediaPeriodId != null) {
+ windowPeriodString +=
+ ", period=" + eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);
+ if (eventTime.mediaPeriodId.isAd()) {
+ windowPeriodString += ", adGroup=" + eventTime.mediaPeriodId.adGroupIndex;
+ windowPeriodString += ", ad=" + eventTime.mediaPeriodId.adIndexInAdGroup;
+ }
+ }
+ return "eventTime="
+ + getTimeString(eventTime.realtimeMs - startTimeMs)
+ + ", mediaPos="
+ + getTimeString(eventTime.currentPlaybackPositionMs)
+ + ", "
+ + windowPeriodString;
+ }
+
+ private static String getTimeString(long timeMs) {
+ return timeMs == C.TIME_UNSET ? "?" : TIME_FORMAT.format((timeMs) / 1000f);
+ }
+
+ private static String getStateString(int state) {
+ switch (state) {
+ case Player.STATE_BUFFERING:
+ return "BUFFERING";
+ case Player.STATE_ENDED:
+ return "ENDED";
+ case Player.STATE_IDLE:
+ return "IDLE";
+ case Player.STATE_READY:
+ return "READY";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getAdaptiveSupportString(
+ int trackCount, @AdaptiveSupport int adaptiveSupport) {
+ if (trackCount < 2) {
+ return "N/A";
+ }
+ switch (adaptiveSupport) {
+ case RendererCapabilities.ADAPTIVE_SEAMLESS:
+ return "YES";
+ case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:
+ return "YES_NOT_SEAMLESS";
+ case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:
+ return "NO";
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ // Suppressing reference equality warning because the track group stored in the track selection
+ // must point to the exact track group object to be considered part of it.
+ @SuppressWarnings("ReferenceEquality")
+ private static String getTrackStatusString(
+ @Nullable TrackSelection selection, TrackGroup group, int trackIndex) {
+ return getTrackStatusString(selection != null && selection.getTrackGroup() == group
+ && selection.indexOf(trackIndex) != C.INDEX_UNSET);
+ }
+
+ private static String getTrackStatusString(boolean enabled) {
+ return enabled ? "[X]" : "[ ]";
+ }
+
+ private static String getRepeatModeString(@Player.RepeatMode int repeatMode) {
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ return "OFF";
+ case Player.REPEAT_MODE_ONE:
+ return "ONE";
+ case Player.REPEAT_MODE_ALL:
+ return "ALL";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getDiscontinuityReasonString(@Player.DiscontinuityReason int reason) {
+ switch (reason) {
+ case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION:
+ return "PERIOD_TRANSITION";
+ case Player.DISCONTINUITY_REASON_SEEK:
+ return "SEEK";
+ case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
+ return "SEEK_ADJUSTMENT";
+ case Player.DISCONTINUITY_REASON_AD_INSERTION:
+ return "AD_INSERTION";
+ case Player.DISCONTINUITY_REASON_INTERNAL:
+ return "INTERNAL";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) {
+ switch (reason) {
+ case Player.TIMELINE_CHANGE_REASON_PREPARED:
+ return "PREPARED";
+ case Player.TIMELINE_CHANGE_REASON_RESET:
+ return "RESET";
+ case Player.TIMELINE_CHANGE_REASON_DYNAMIC:
+ return "DYNAMIC";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getPlaybackSuppressionReasonString(
+ @PlaybackSuppressionReason int playbackSuppressionReason) {
+ switch (playbackSuppressionReason) {
+ case Player.PLAYBACK_SUPPRESSION_REASON_NONE:
+ return "NONE";
+ case Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS:
+ return "TRANSIENT_AUDIO_FOCUS_LOSS";
+ default:
+ return "?";
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java
new file mode 100644
index 0000000000..faa917fab8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacConstants.java
@@ -0,0 +1,42 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+/** Defines constants used by the FLAC extractor. */
+public final class FlacConstants {
+
+ /** Size of the FLAC stream marker in bytes. */
+ public static final int STREAM_MARKER_SIZE = 4;
+ /** Size of the header of a FLAC metadata block in bytes. */
+ public static final int METADATA_BLOCK_HEADER_SIZE = 4;
+ /** Size of the FLAC stream info block (header included) in bytes. */
+ public static final int STREAM_INFO_BLOCK_SIZE = 38;
+ /** Minimum size of a FLAC frame header in bytes. */
+ public static final int MIN_FRAME_HEADER_SIZE = 6;
+ /** Maximum size of a FLAC frame header in bytes. */
+ public static final int MAX_FRAME_HEADER_SIZE = 16;
+
+ /** Stream info metadata block type. */
+ public static final int METADATA_TYPE_STREAM_INFO = 0;
+ /** Seek table metadata block type. */
+ public static final int METADATA_TYPE_SEEK_TABLE = 3;
+ /** Vorbis comment metadata block type. */
+ public static final int METADATA_TYPE_VORBIS_COMMENT = 4;
+ /** Picture metadata block type. */
+ public static final int METADATA_TYPE_PICTURE = 6;
+
+ private FlacConstants() {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java
new file mode 100644
index 0000000000..893481d8da
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/FlacStreamMetadata.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.VorbisComment;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Holder for FLAC metadata.
+ *
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
+ * METADATA_BLOCK_STREAMINFO</a>
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
+ * METADATA_BLOCK_SEEKTABLE</a>
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
+ * METADATA_BLOCK_VORBIS_COMMENT</a>
+ * @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
+ * METADATA_BLOCK_PICTURE</a>
+ */
+public final class FlacStreamMetadata {
+
+ /** A FLAC seek table. */
+ public static class SeekTable {
+ /** Seek points sample numbers. */
+ public final long[] pointSampleNumbers;
+ /** Seek points byte offsets from the first frame. */
+ public final long[] pointOffsets;
+
+ public SeekTable(long[] pointSampleNumbers, long[] pointOffsets) {
+ this.pointSampleNumbers = pointSampleNumbers;
+ this.pointOffsets = pointOffsets;
+ }
+ }
+
+ private static final String TAG = "FlacStreamMetadata";
+
+ /** Indicates that a value is not in the corresponding lookup table. */
+ public static final int NOT_IN_LOOKUP_TABLE = -1;
+ /** Separator between the field name of a Vorbis comment and the corresponding value. */
+ private static final String SEPARATOR = "=";
+
+ /** Minimum number of samples per block. */
+ public final int minBlockSizeSamples;
+ /** Maximum number of samples per block. */
+ public final int maxBlockSizeSamples;
+ /** Minimum frame size in bytes, or 0 if the value is unknown. */
+ public final int minFrameSize;
+ /** Maximum frame size in bytes, or 0 if the value is unknown. */
+ public final int maxFrameSize;
+ /** Sample rate in Hertz. */
+ public final int sampleRate;
+ /**
+ * Lookup key corresponding to the stream sample rate, or {@link #NOT_IN_LOOKUP_TABLE} if it is
+ * not in the lookup table.
+ *
+ * <p>This key is used to indicate the sample rate in the frame header for the most common values.
+ *
+ * <p>The sample rate lookup table is described in https://xiph.org/flac/format.html#frame_header.
+ */
+ public final int sampleRateLookupKey;
+ /** Number of audio channels. */
+ public final int channels;
+ /** Number of bits per sample. */
+ public final int bitsPerSample;
+ /**
+ * Lookup key corresponding to the number of bits per sample of the stream, or {@link
+ * #NOT_IN_LOOKUP_TABLE} if it is not in the lookup table.
+ *
+ * <p>This key is used to indicate the number of bits per sample in the frame header for the most
+ * common values.
+ *
+ * <p>The sample size lookup table is described in https://xiph.org/flac/format.html#frame_header.
+ */
+ public final int bitsPerSampleLookupKey;
+ /** Total number of samples, or 0 if the value is unknown. */
+ public final long totalSamples;
+ /** Seek table, or {@code null} if it is not provided. */
+ @Nullable public final SeekTable seekTable;
+ /** Content metadata, or {@code null} if it is not provided. */
+ @Nullable private final Metadata metadata;
+
+ /**
+ * Parses binary FLAC stream info metadata.
+ *
+ * @param data An array containing binary FLAC stream info block.
+ * @param offset The offset of the stream info block in {@code data}, excluding the header (i.e.
+ * the offset points to the first byte of the minimum block size).
+ */
+ public FlacStreamMetadata(byte[] data, int offset) {
+ ParsableBitArray scratch = new ParsableBitArray(data);
+ scratch.setPosition(offset * 8);
+ minBlockSizeSamples = scratch.readBits(16);
+ maxBlockSizeSamples = scratch.readBits(16);
+ minFrameSize = scratch.readBits(24);
+ maxFrameSize = scratch.readBits(24);
+ sampleRate = scratch.readBits(20);
+ sampleRateLookupKey = getSampleRateLookupKey(sampleRate);
+ channels = scratch.readBits(3) + 1;
+ bitsPerSample = scratch.readBits(5) + 1;
+ bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample);
+ totalSamples = scratch.readBitsToLong(36);
+ seekTable = null;
+ metadata = null;
+ }
+
+ // Used in native code.
+ public FlacStreamMetadata(
+ int minBlockSizeSamples,
+ int maxBlockSizeSamples,
+ int minFrameSize,
+ int maxFrameSize,
+ int sampleRate,
+ int channels,
+ int bitsPerSample,
+ long totalSamples,
+ ArrayList<String> vorbisComments,
+ ArrayList<PictureFrame> pictureFrames) {
+ this(
+ minBlockSizeSamples,
+ maxBlockSizeSamples,
+ minFrameSize,
+ maxFrameSize,
+ sampleRate,
+ channels,
+ bitsPerSample,
+ totalSamples,
+ /* seekTable= */ null,
+ buildMetadata(vorbisComments, pictureFrames));
+ }
+
+ private FlacStreamMetadata(
+ int minBlockSizeSamples,
+ int maxBlockSizeSamples,
+ int minFrameSize,
+ int maxFrameSize,
+ int sampleRate,
+ int channels,
+ int bitsPerSample,
+ long totalSamples,
+ @Nullable SeekTable seekTable,
+ @Nullable Metadata metadata) {
+ this.minBlockSizeSamples = minBlockSizeSamples;
+ this.maxBlockSizeSamples = maxBlockSizeSamples;
+ this.minFrameSize = minFrameSize;
+ this.maxFrameSize = maxFrameSize;
+ this.sampleRate = sampleRate;
+ this.sampleRateLookupKey = getSampleRateLookupKey(sampleRate);
+ this.channels = channels;
+ this.bitsPerSample = bitsPerSample;
+ this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample);
+ this.totalSamples = totalSamples;
+ this.seekTable = seekTable;
+ this.metadata = metadata;
+ }
+
+ /** Returns the maximum size for a decoded frame from the FLAC stream. */
+ public int getMaxDecodedFrameSize() {
+ return maxBlockSizeSamples * channels * (bitsPerSample / 8);
+ }
+
+ /** Returns the bit-rate of the FLAC stream. */
+ public int getBitRate() {
+ return bitsPerSample * sampleRate * channels;
+ }
+
+ /**
+ * Returns the duration of the FLAC stream in microseconds, or {@link C#TIME_UNSET} if the total
+ * number of samples if unknown.
+ */
+ public long getDurationUs() {
+ return totalSamples == 0 ? C.TIME_UNSET : totalSamples * C.MICROS_PER_SECOND / sampleRate;
+ }
+
+ /**
+ * Returns the sample number of the sample at a given time.
+ *
+ * @param timeUs Time position in microseconds in the FLAC stream.
+ * @return The sample number corresponding to the time position.
+ */
+ public long getSampleNumber(long timeUs) {
+ long sampleNumber = (timeUs * sampleRate) / C.MICROS_PER_SECOND;
+ return Util.constrainValue(sampleNumber, /* min= */ 0, totalSamples - 1);
+ }
+
+ /** Returns the approximate number of bytes per frame for the current FLAC stream. */
+ public long getApproxBytesPerFrame() {
+ long approxBytesPerFrame;
+ if (maxFrameSize > 0) {
+ approxBytesPerFrame = ((long) maxFrameSize + minFrameSize) / 2 + 1;
+ } else {
+ // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the
+ // default value for FLAC block-size, which is 4096.
+ long blockSizeSamples =
+ (minBlockSizeSamples == maxBlockSizeSamples && minBlockSizeSamples > 0)
+ ? minBlockSizeSamples
+ : 4096;
+ approxBytesPerFrame = (blockSizeSamples * channels * bitsPerSample) / 8 + 64;
+ }
+ return approxBytesPerFrame;
+ }
+
+ /**
+ * Returns a {@link Format} extracted from the FLAC stream metadata.
+ *
+ * <p>{@code streamMarkerAndInfoBlock} is updated to set the bit corresponding to the stream info
+ * last metadata block flag to true.
+ *
+ * @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the
+ * stream info block.
+ * @param id3Metadata The ID3 metadata of the stream, or {@code null} if there is no such data.
+ * @return The extracted {@link Format}.
+ */
+ public Format getFormat(byte[] streamMarkerAndInfoBlock, @Nullable Metadata id3Metadata) {
+ // Set the last metadata block flag, ignore the other blocks.
+ streamMarkerAndInfoBlock[4] = (byte) 0x80;
+ int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE;
+ @Nullable Metadata metadataWithId3 = getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
+
+ return Format.createAudioSampleFormat(
+ /* id= */ null,
+ MimeTypes.AUDIO_FLAC,
+ /* codecs= */ null,
+ getBitRate(),
+ maxInputSize,
+ channels,
+ sampleRate,
+ /* pcmEncoding= */ Format.NO_VALUE,
+ /* encoderDelay= */ 0,
+ /* encoderPadding= */ 0,
+ /* initializationData= */ Collections.singletonList(streamMarkerAndInfoBlock),
+ /* drmInitData= */ null,
+ /* selectionFlags= */ 0,
+ /* language= */ null,
+ metadataWithId3);
+ }
+
+ /** Returns a copy of the content metadata with entries from {@code other} appended. */
+ @Nullable
+ public Metadata getMetadataCopyWithAppendedEntriesFrom(@Nullable Metadata other) {
+ return metadata == null ? other : metadata.copyWithAppendedEntriesFrom(other);
+ }
+
+ /** Returns a copy of {@code this} with the seek table replaced by the one given. */
+ public FlacStreamMetadata copyWithSeekTable(@Nullable SeekTable seekTable) {
+ return new FlacStreamMetadata(
+ minBlockSizeSamples,
+ maxBlockSizeSamples,
+ minFrameSize,
+ maxFrameSize,
+ sampleRate,
+ channels,
+ bitsPerSample,
+ totalSamples,
+ seekTable,
+ metadata);
+ }
+
+ /** Returns a copy of {@code this} with the given Vorbis comments added to the metadata. */
+ public FlacStreamMetadata copyWithVorbisComments(List<String> vorbisComments) {
+ @Nullable
+ Metadata appendedMetadata =
+ getMetadataCopyWithAppendedEntriesFrom(
+ buildMetadata(vorbisComments, Collections.emptyList()));
+ return new FlacStreamMetadata(
+ minBlockSizeSamples,
+ maxBlockSizeSamples,
+ minFrameSize,
+ maxFrameSize,
+ sampleRate,
+ channels,
+ bitsPerSample,
+ totalSamples,
+ seekTable,
+ appendedMetadata);
+ }
+
+ /** Returns a copy of {@code this} with the given picture frames added to the metadata. */
+ public FlacStreamMetadata copyWithPictureFrames(List<PictureFrame> pictureFrames) {
+ @Nullable
+ Metadata appendedMetadata =
+ getMetadataCopyWithAppendedEntriesFrom(
+ buildMetadata(Collections.emptyList(), pictureFrames));
+ return new FlacStreamMetadata(
+ minBlockSizeSamples,
+ maxBlockSizeSamples,
+ minFrameSize,
+ maxFrameSize,
+ sampleRate,
+ channels,
+ bitsPerSample,
+ totalSamples,
+ seekTable,
+ appendedMetadata);
+ }
+
+ private static int getSampleRateLookupKey(int sampleRate) {
+ switch (sampleRate) {
+ case 88200:
+ return 1;
+ case 176400:
+ return 2;
+ case 192000:
+ return 3;
+ case 8000:
+ return 4;
+ case 16000:
+ return 5;
+ case 22050:
+ return 6;
+ case 24000:
+ return 7;
+ case 32000:
+ return 8;
+ case 44100:
+ return 9;
+ case 48000:
+ return 10;
+ case 96000:
+ return 11;
+ default:
+ return NOT_IN_LOOKUP_TABLE;
+ }
+ }
+
+ private static int getBitsPerSampleLookupKey(int bitsPerSample) {
+ switch (bitsPerSample) {
+ case 8:
+ return 1;
+ case 12:
+ return 2;
+ case 16:
+ return 4;
+ case 20:
+ return 5;
+ case 24:
+ return 6;
+ default:
+ return NOT_IN_LOOKUP_TABLE;
+ }
+ }
+
+ @Nullable
+ private static Metadata buildMetadata(
+ List<String> vorbisComments, List<PictureFrame> pictureFrames) {
+ if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
+ return null;
+ }
+
+ ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
+ for (int i = 0; i < vorbisComments.size(); i++) {
+ String vorbisComment = vorbisComments.get(i);
+ String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
+ if (keyAndValue.length != 2) {
+ Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
+ } else {
+ VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
+ metadataEntries.add(entry);
+ }
+ }
+ metadataEntries.addAll(pictureFrames);
+
+ return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java
new file mode 100644
index 0000000000..a34cee48f9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/GlUtil.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import static android.opengl.GLU.gluErrorString;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.opengl.EGL14;
+import android.opengl.EGLDisplay;
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import javax.microedition.khronos.egl.EGL10;
+
+/** GL utilities. */
+public final class GlUtil {
+
+ /**
+ * GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
+ */
+ public static final class Attribute {
+
+ /** The name of the attribute in the GLSL sources. */
+ public final String name;
+
+ private final int index;
+ private final int location;
+
+ @Nullable private Buffer buffer;
+ private int size;
+
+ /**
+ * Creates a new GL attribute.
+ *
+ * @param program The identifier of a compiled and linked GLSL shader program.
+ * @param index The index of the attribute. After this instance has been constructed, the name
+ * of the attribute is available via the {@link #name} field.
+ */
+ public Attribute(int program, int index) {
+ int[] len = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, len, 0);
+
+ int[] type = new int[1];
+ int[] size = new int[1];
+ byte[] nameBytes = new byte[len[0]];
+ int[] ignore = new int[1];
+
+ GLES20.glGetActiveAttrib(program, index, len[0], ignore, 0, size, 0, type, 0, nameBytes, 0);
+ name = new String(nameBytes, 0, strlen(nameBytes));
+ location = GLES20.glGetAttribLocation(program, name);
+ this.index = index;
+ }
+
+ /**
+ * Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
+ * elements) to this {@link Attribute}.
+ *
+ * @param buffer Buffer to bind to this attribute.
+ * @param size Number of elements per vertex.
+ */
+ public void setBuffer(float[] buffer, int size) {
+ this.buffer = createBuffer(buffer);
+ this.size = size;
+ }
+
+ /**
+ * Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
+ *
+ * <p>Should be called before each drawing call.
+ */
+ public void bind() {
+ Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind");
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+ GLES20.glVertexAttribPointer(
+ location,
+ size, // count
+ GLES20.GL_FLOAT, // type
+ false, // normalize
+ 0, // stride
+ buffer);
+ GLES20.glEnableVertexAttribArray(index);
+ checkGlError();
+ }
+ }
+
+ /**
+ * GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
+ */
+ public static final class Uniform {
+
+ /** The name of the uniform in the GLSL sources. */
+ public final String name;
+
+ private final int location;
+ private final int type;
+ private final float[] value;
+
+ private int texId;
+ private int unit;
+
+ /**
+ * Creates a new GL uniform.
+ *
+ * @param program The identifier of a compiled and linked GLSL shader program.
+ * @param index The index of the uniform. After this instance has been constructed, the name of
+ * the uniform is available via the {@link #name} field.
+ */
+ public Uniform(int program, int index) {
+ int[] len = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0);
+
+ int[] type = new int[1];
+ int[] size = new int[1];
+ byte[] name = new byte[len[0]];
+ int[] ignore = new int[1];
+
+ GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0);
+ this.name = new String(name, 0, strlen(name));
+ location = GLES20.glGetUniformLocation(program, this.name);
+ this.type = type[0];
+
+ value = new float[1];
+ }
+
+ /**
+ * Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
+ *
+ * @param texId The GL texture identifier from which to sample.
+ * @param unit The GL texture unit index.
+ */
+ public void setSamplerTexId(int texId, int unit) {
+ this.texId = texId;
+ this.unit = unit;
+ }
+
+ /** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
+ public void setFloat(float value) {
+ this.value[0] = value;
+ }
+
+ /**
+ * Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)} or
+ * {@link #setFloat(float)}.
+ *
+ * <p>Should be called before each drawing call.
+ */
+ public void bind() {
+ if (type == GLES20.GL_FLOAT) {
+ GLES20.glUniform1fv(location, 1, value, 0);
+ checkGlError();
+ return;
+ }
+
+ if (texId == 0) {
+ throw new IllegalStateException("call setSamplerTexId before bind");
+ }
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
+ if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) {
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
+ } else if (type == GLES20.GL_SAMPLER_2D) {
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
+ } else {
+ throw new IllegalStateException("unexpected uniform type: " + type);
+ }
+ GLES20.glUniform1i(location, unit);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ checkGlError();
+ }
+ }
+
+ private static final String TAG = "GlUtil";
+
+ private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
+ private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
+
+ /** Class only contains static methods. */
+ private GlUtil() {}
+
+ /**
+ * Returns whether creating a GL context with {@value EXTENSION_PROTECTED_CONTENT} is possible. If
+ * {@code true}, the device supports a protected output path for DRM content when using GL.
+ */
+ @TargetApi(24)
+ public static boolean isProtectedContentExtensionSupported(Context context) {
+ if (Util.SDK_INT < 24) {
+ return false;
+ }
+ if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) {
+ // Samsung devices running Nougat are known to be broken. See
+ // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802].
+ // Moto Z XT1650 is also affected. See
+ // https://github.com/google/ExoPlayer/issues/3215.
+ return false;
+ }
+ if (Util.SDK_INT < 26
+ && !context
+ .getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
+ // Pre API level 26 devices were not well tested unless they supported VR mode.
+ return false;
+ }
+
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ return eglExtensions != null && eglExtensions.contains(EXTENSION_PROTECTED_CONTENT);
+ }
+
+ /**
+ * Returns whether creating a GL context with {@value EXTENSION_SURFACELESS_CONTEXT} is possible.
+ */
+ @TargetApi(17)
+ public static boolean isSurfacelessContextExtensionSupported() {
+ if (Util.SDK_INT < 17) {
+ return false;
+ }
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT);
+ }
+
+ /**
+ * If there is an OpenGl error, logs the error and if {@link
+ * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}.
+ */
+ public static void checkGlError() {
+ int lastError = GLES20.GL_NO_ERROR;
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ Log.e(TAG, "glError " + gluErrorString(error));
+ lastError = error;
+ }
+ if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED && lastError != GLES20.GL_NO_ERROR) {
+ throw new RuntimeException("glError " + gluErrorString(lastError));
+ }
+ }
+
+ /**
+ * Builds a GL shader program from vertex and fragment shader code.
+ *
+ * @param vertexCode GLES20 vertex shader program as arrays of strings. Strings are joined by
+ * adding a new line character in between each of them.
+ * @param fragmentCode GLES20 fragment shader program as arrays of strings. Strings are joined by
+ * adding a new line character in between each of them.
+ * @return GLES20 program id.
+ */
+ public static int compileProgram(String[] vertexCode, String[] fragmentCode) {
+ return compileProgram(TextUtils.join("\n", vertexCode), TextUtils.join("\n", fragmentCode));
+ }
+
+ /**
+ * Builds a GL shader program from vertex and fragment shader code.
+ *
+ * @param vertexCode GLES20 vertex shader program.
+ * @param fragmentCode GLES20 fragment shader program.
+ * @return GLES20 program id.
+ */
+ public static int compileProgram(String vertexCode, String fragmentCode) {
+ int program = GLES20.glCreateProgram();
+ checkGlError();
+
+ // Add the vertex and fragment shaders.
+ addShader(GLES20.GL_VERTEX_SHADER, vertexCode, program);
+ addShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode, program);
+
+ // Link and check for errors.
+ GLES20.glLinkProgram(program);
+ int[] linkStatus = new int[] {GLES20.GL_FALSE};
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] != GLES20.GL_TRUE) {
+ throwGlError("Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(program));
+ }
+ checkGlError();
+
+ return program;
+ }
+
+ /** Returns the {@link Attribute}s in the specified {@code program}. */
+ public static Attribute[] getAttributes(int program) {
+ int[] attributeCount = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0);
+ if (attributeCount[0] != 2) {
+ throw new IllegalStateException("expected two attributes");
+ }
+
+ Attribute[] attributes = new Attribute[attributeCount[0]];
+ for (int i = 0; i < attributeCount[0]; i++) {
+ attributes[i] = new Attribute(program, i);
+ }
+ return attributes;
+ }
+
+ /** Returns the {@link Uniform}s in the specified {@code program}. */
+ public static Uniform[] getUniforms(int program) {
+ int[] uniformCount = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0);
+
+ Uniform[] uniforms = new Uniform[uniformCount[0]];
+ for (int i = 0; i < uniformCount[0]; i++) {
+ uniforms[i] = new Uniform(program, i);
+ }
+
+ return uniforms;
+ }
+
+ /**
+ * Allocates a FloatBuffer with the given data.
+ *
+ * @param data Used to initialize the new buffer.
+ */
+ public static FloatBuffer createBuffer(float[] data) {
+ return (FloatBuffer) createBuffer(data.length).put(data).flip();
+ }
+
+ /**
+ * Allocates a FloatBuffer.
+ *
+ * @param capacity The new buffer's capacity, in floats.
+ */
+ public static FloatBuffer createBuffer(int capacity) {
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(capacity * C.BYTES_PER_FLOAT);
+ return byteBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
+ }
+
+ /**
+ * Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and
+ * GL_CLAMP_TO_EDGE wrapping.
+ */
+ public static int createExternalTexture() {
+ int[] texId = new int[1];
+ GLES20.glGenTextures(1, IntBuffer.wrap(texId));
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);
+ GLES20.glTexParameteri(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ checkGlError();
+ return texId[0];
+ }
+
+ private static void addShader(int type, String source, int program) {
+ int shader = GLES20.glCreateShader(type);
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+
+ int[] result = new int[] {GLES20.GL_FALSE};
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);
+ if (result[0] != GLES20.GL_TRUE) {
+ throwGlError(GLES20.glGetShaderInfoLog(shader) + ", source: " + source);
+ }
+
+ GLES20.glAttachShader(program, shader);
+ GLES20.glDeleteShader(shader);
+ checkGlError();
+ }
+
+ private static void throwGlError(String errorMsg) {
+ Log.e(TAG, errorMsg);
+ if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) {
+ throw new RuntimeException(errorMsg);
+ }
+ }
+
+ /** Returns the length of the null-terminated string in {@code strVal}. */
+ private static int strlen(byte[] strVal) {
+ for (int i = 0; i < strVal.length; ++i) {
+ if (strVal[i] == '\0') {
+ return i;
+ }
+ }
+ return strVal.length;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java
new file mode 100644
index 0000000000..2e412fa10f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/HandlerWrapper.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import androidx.annotation.Nullable;
+
+/**
+ * An interface to call through to a {@link Handler}. Instances must be created by calling {@link
+ * Clock#createHandler(Looper, Handler.Callback)} on {@link Clock#DEFAULT} for all non-test cases.
+ */
+public interface HandlerWrapper {
+
+ /** @see Handler#getLooper() */
+ Looper getLooper();
+
+ /** @see Handler#obtainMessage(int) */
+ Message obtainMessage(int what);
+
+ /** @see Handler#obtainMessage(int, Object) */
+ Message obtainMessage(int what, @Nullable Object obj);
+
+ /** @see Handler#obtainMessage(int, int, int) */
+ Message obtainMessage(int what, int arg1, int arg2);
+
+ /** @see Handler#obtainMessage(int, int, int, Object) */
+ Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj);
+
+ /** @see Handler#sendEmptyMessage(int) */
+ boolean sendEmptyMessage(int what);
+
+ /** @see Handler#sendEmptyMessageAtTime(int, long) */
+ boolean sendEmptyMessageAtTime(int what, long uptimeMs);
+
+ /** @see Handler#removeMessages(int) */
+ void removeMessages(int what);
+
+ /** @see Handler#removeCallbacksAndMessages(Object) */
+ void removeCallbacksAndMessages(@Nullable Object token);
+
+ /** @see Handler#post(Runnable) */
+ boolean post(Runnable runnable);
+
+ /** @see Handler#postDelayed(Runnable, long) */
+ boolean postDelayed(Runnable runnable, long delayMs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java
new file mode 100644
index 0000000000..31e582aac5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.util.Arrays;
+
+/**
+ * Configurable loader for native libraries.
+ */
+public final class LibraryLoader {
+
+ private static final String TAG = "LibraryLoader";
+
+ private String[] nativeLibraries;
+ private boolean loadAttempted;
+ private boolean isAvailable;
+
+ /**
+ * @param libraries The names of the libraries to load.
+ */
+ public LibraryLoader(String... libraries) {
+ nativeLibraries = libraries;
+ }
+
+ /**
+ * Overrides the names of the libraries to load. Must be called before any call to
+ * {@link #isAvailable()}.
+ */
+ public synchronized void setLibraries(String... libraries) {
+ Assertions.checkState(!loadAttempted, "Cannot set libraries after loading");
+ nativeLibraries = libraries;
+ }
+
+ /**
+ * Returns whether the underlying libraries are available, loading them if necessary.
+ */
+ public synchronized boolean isAvailable() {
+ if (loadAttempted) {
+ return isAvailable;
+ }
+ loadAttempted = true;
+ try {
+ for (String lib : nativeLibraries) {
+ System.loadLibrary(lib);
+ }
+ isAvailable = true;
+ } catch (UnsatisfiedLinkError exception) {
+ // Log a warning as an attempt to check for the library indicates that the app depends on an
+ // extension and generally would expect its native libraries to be available.
+ Log.w(TAG, "Failed to load " + Arrays.toString(nativeLibraries));
+ }
+ return isAvailable;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java
new file mode 100644
index 0000000000..b6e4a25935
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Log.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.text.TextUtils;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.UnknownHostException;
+
+/** Wrapper around {@link android.util.Log} which allows to set the log level. */
+public final class Log {
+
+ /**
+ * Log level for ExoPlayer logcat logging. One of {@link #LOG_LEVEL_ALL}, {@link #LOG_LEVEL_INFO},
+ * {@link #LOG_LEVEL_WARNING}, {@link #LOG_LEVEL_ERROR} or {@link #LOG_LEVEL_OFF}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({LOG_LEVEL_ALL, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR, LOG_LEVEL_OFF})
+ @interface LogLevel {}
+ /** Log level to log all messages. */
+ public static final int LOG_LEVEL_ALL = 0;
+ /** Log level to only log informative, warning and error messages. */
+ public static final int LOG_LEVEL_INFO = 1;
+ /** Log level to only log warning and error messages. */
+ public static final int LOG_LEVEL_WARNING = 2;
+ /** Log level to only log error messages. */
+ public static final int LOG_LEVEL_ERROR = 3;
+ /** Log level to disable all logging. */
+ public static final int LOG_LEVEL_OFF = Integer.MAX_VALUE;
+
+ private static int logLevel = LOG_LEVEL_ALL;
+ private static boolean logStackTraces = true;
+
+ private Log() {}
+
+ /** Returns current {@link LogLevel} for ExoPlayer logcat logging. */
+ public static @LogLevel int getLogLevel() {
+ return logLevel;
+ }
+
+ /** Returns whether stack traces of {@link Throwable}s will be logged to logcat. */
+ public boolean getLogStackTraces() {
+ return logStackTraces;
+ }
+
+ /**
+ * Sets the {@link LogLevel} for ExoPlayer logcat logging.
+ *
+ * @param logLevel The new {@link LogLevel}.
+ */
+ public static void setLogLevel(@LogLevel int logLevel) {
+ Log.logLevel = logLevel;
+ }
+
+ /**
+ * Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging
+ * is enabled by default.
+ *
+ * @param logStackTraces Whether stack traces will be logged.
+ */
+ public static void setLogStackTraces(boolean logStackTraces) {
+ Log.logStackTraces = logStackTraces;
+ }
+
+ /** @see android.util.Log#d(String, String) */
+ public static void d(String tag, String message) {
+ if (logLevel == LOG_LEVEL_ALL) {
+ android.util.Log.d(tag, message);
+ }
+ }
+
+ /** @see android.util.Log#d(String, String, Throwable) */
+ public static void d(String tag, String message, @Nullable Throwable throwable) {
+ d(tag, appendThrowableString(message, throwable));
+ }
+
+ /** @see android.util.Log#i(String, String) */
+ public static void i(String tag, String message) {
+ if (logLevel <= LOG_LEVEL_INFO) {
+ android.util.Log.i(tag, message);
+ }
+ }
+
+ /** @see android.util.Log#i(String, String, Throwable) */
+ public static void i(String tag, String message, @Nullable Throwable throwable) {
+ i(tag, appendThrowableString(message, throwable));
+ }
+
+ /** @see android.util.Log#w(String, String) */
+ public static void w(String tag, String message) {
+ if (logLevel <= LOG_LEVEL_WARNING) {
+ android.util.Log.w(tag, message);
+ }
+ }
+
+ /** @see android.util.Log#w(String, String, Throwable) */
+ public static void w(String tag, String message, @Nullable Throwable throwable) {
+ w(tag, appendThrowableString(message, throwable));
+ }
+
+ /** @see android.util.Log#e(String, String) */
+ public static void e(String tag, String message) {
+ if (logLevel <= LOG_LEVEL_ERROR) {
+ android.util.Log.e(tag, message);
+ }
+ }
+
+ /** @see android.util.Log#e(String, String, Throwable) */
+ public static void e(String tag, String message, @Nullable Throwable throwable) {
+ e(tag, appendThrowableString(message, throwable));
+ }
+
+ /**
+ * Returns a string representation of a {@link Throwable} suitable for logging, taking into
+ * account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled.
+ *
+ * <p>Stack trace logging may be unconditionally suppressed for some expected failure modes (e.g.,
+ * {@link Throwable Throwables} that are expected if the device doesn't have network connectivity)
+ * to avoid log spam.
+ *
+ * @param throwable The {@link Throwable}.
+ * @return The string representation of the {@link Throwable}.
+ */
+ @Nullable
+ public static String getThrowableString(@Nullable Throwable throwable) {
+ if (throwable == null) {
+ return null;
+ } else if (isCausedByUnknownHostException(throwable)) {
+ // UnknownHostException implies the device doesn't have network connectivity.
+ // UnknownHostException.getMessage() may return a string that's more verbose than desired for
+ // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
+ // special handling to return the empty string, which can result in logging that doesn't
+ // indicate the failure mode at all. Hence we special case this exception to always return a
+ // concise but useful message.
+ return "UnknownHostException (no network)";
+ } else if (!logStackTraces) {
+ return throwable.getMessage();
+ } else {
+ return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
+ }
+ }
+
+ private static String appendThrowableString(String message, @Nullable Throwable throwable) {
+ @Nullable String throwableString = getThrowableString(throwable);
+ if (!TextUtils.isEmpty(throwableString)) {
+ message += "\n " + throwableString.replace("\n", "\n ") + '\n';
+ }
+ return message;
+ }
+
+ private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) {
+ while (throwable != null) {
+ if (throwable instanceof UnknownHostException) {
+ return true;
+ }
+ throwable = throwable.getCause();
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java
new file mode 100644
index 0000000000..ef6f938ca8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/LongArray.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.util.Arrays;
+
+/**
+ * An append-only, auto-growing {@code long[]}.
+ */
+public final class LongArray {
+
+ private static final int DEFAULT_INITIAL_CAPACITY = 32;
+
+ private int size;
+ private long[] values;
+
+ public LongArray() {
+ this(DEFAULT_INITIAL_CAPACITY);
+ }
+
+ /**
+ * @param initialCapacity The initial capacity of the array.
+ */
+ public LongArray(int initialCapacity) {
+ values = new long[initialCapacity];
+ }
+
+ /**
+ * Appends a value.
+ *
+ * @param value The value to append.
+ */
+ public void add(long value) {
+ if (size == values.length) {
+ values = Arrays.copyOf(values, size * 2);
+ }
+ values[size++] = value;
+ }
+
+ /**
+ * Returns the value at a specified index.
+ *
+ * @param index The index.
+ * @return The corresponding value.
+ * @throws IndexOutOfBoundsException If the index is less than zero, or greater than or equal to
+ * {@link #size()}.
+ */
+ public long get(int index) {
+ if (index < 0 || index >= size) {
+ throw new IndexOutOfBoundsException("Invalid index " + index + ", size is " + size);
+ }
+ return values[index];
+ }
+
+ /**
+ * Returns the current size of the array.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Copies the current values into a newly allocated primitive array.
+ *
+ * @return The primitive array containing the copied values.
+ */
+ public long[] toArray() {
+ return Arrays.copyOf(values, size);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java
new file mode 100644
index 0000000000..029f3aa8f5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MediaClock.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+
+/**
+ * Tracks the progression of media time.
+ */
+public interface MediaClock {
+
+ /**
+ * Returns the current media position in microseconds.
+ */
+ long getPositionUs();
+
+ /**
+ * Attempts to set the playback parameters. The media clock may override these parameters if they
+ * are not supported.
+ *
+ * @param playbackParameters The playback parameters to attempt to set.
+ */
+ void setPlaybackParameters(PlaybackParameters playbackParameters);
+
+ /**
+ * Returns the active playback parameters.
+ */
+ PlaybackParameters getPlaybackParameters();
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java
new file mode 100644
index 0000000000..594a62d63a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.util.ArrayList;
+
+/**
+ * Defines common MIME types and helper methods.
+ */
+public final class MimeTypes {
+
+ public static final String BASE_TYPE_VIDEO = "video";
+ public static final String BASE_TYPE_AUDIO = "audio";
+ public static final String BASE_TYPE_TEXT = "text";
+ public static final String BASE_TYPE_APPLICATION = "application";
+
+ public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
+ public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
+ public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp";
+ public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
+ public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc";
+ public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8";
+ public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
+ public static final String VIDEO_AV1 = BASE_TYPE_VIDEO + "/av01";
+ public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es";
+ public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg";
+ public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2";
+ public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1";
+ public static final String VIDEO_DIVX = BASE_TYPE_VIDEO + "/divx";
+ public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision";
+ public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
+
+ public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4";
+ public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";
+ public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm";
+ public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg";
+ public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1";
+ public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2";
+ public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw";
+ public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw";
+ public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw";
+ public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3";
+ public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3";
+ public static final String AUDIO_E_AC3_JOC = BASE_TYPE_AUDIO + "/eac3-joc";
+ public static final String AUDIO_AC4 = BASE_TYPE_AUDIO + "/ac4";
+ public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd";
+ public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts";
+ public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd";
+ public static final String AUDIO_DTS_EXPRESS = BASE_TYPE_AUDIO + "/vnd.dts.hd;profile=lbr";
+ public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis";
+ public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus";
+ public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
+ public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
+ public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
+ public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
+ public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
+ public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
+
+ public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
+ public static final String TEXT_SSA = BASE_TYPE_TEXT + "/x-ssa";
+
+ public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4";
+ public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
+ public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml";
+ public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
+ public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml";
+ public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
+ public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
+ public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708";
+ public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
+ public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
+ public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g";
+ public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4-vtt";
+ public static final String APPLICATION_MP4CEA608 = BASE_TYPE_APPLICATION + "/x-mp4-cea-608";
+ public static final String APPLICATION_RAWCC = BASE_TYPE_APPLICATION + "/x-rawcc";
+ public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub";
+ public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs";
+ public static final String APPLICATION_SCTE35 = BASE_TYPE_APPLICATION + "/x-scte35";
+ public static final String APPLICATION_CAMERA_MOTION = BASE_TYPE_APPLICATION + "/x-camera-motion";
+ public static final String APPLICATION_EMSG = BASE_TYPE_APPLICATION + "/x-emsg";
+ public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
+ public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
+ public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
+
+ private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();
+
+ /**
+ * Registers a custom MIME type. Most applications do not need to call this method, as handling of
+ * standard MIME types is built in. These built-in MIME types take precedence over any registered
+ * via this method. If this method is used, it must be called before creating any player(s).
+ *
+ * @param mimeType The custom MIME type to register.
+ * @param codecPrefix The RFC 6381-style codec string prefix associated with the MIME type.
+ * @param trackType The {@link C}{@code .TRACK_TYPE_*} constant associated with the MIME type.
+ * This value is ignored if the top-level type of {@code mimeType} is audio, video or text.
+ */
+ public static void registerCustomMimeType(String mimeType, String codecPrefix, int trackType) {
+ CustomMimeType customMimeType = new CustomMimeType(mimeType, codecPrefix, trackType);
+ int customMimeTypeCount = customMimeTypes.size();
+ for (int i = 0; i < customMimeTypeCount; i++) {
+ if (mimeType.equals(customMimeTypes.get(i).mimeType)) {
+ customMimeTypes.remove(i);
+ break;
+ }
+ }
+ customMimeTypes.add(customMimeType);
+ }
+
+ /** Returns whether the given string is an audio MIME type. */
+ public static boolean isAudio(@Nullable String mimeType) {
+ return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));
+ }
+
+ /** Returns whether the given string is a video MIME type. */
+ public static boolean isVideo(@Nullable String mimeType) {
+ return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));
+ }
+
+ /** Returns whether the given string is a text MIME type. */
+ public static boolean isText(@Nullable String mimeType) {
+ return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));
+ }
+
+ /** Returns whether the given string is an application MIME type. */
+ public static boolean isApplication(@Nullable String mimeType) {
+ return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));
+ }
+
+ /**
+ * Returns true if it is known that all samples in a stream of the given sample MIME type are
+ * guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on
+ * every sample).
+ *
+ * @param mimeType The sample MIME type.
+ * @return True if it is known that all samples in a stream of the given sample MIME type are
+ * guaranteed to be sync samples. False otherwise, including if {@code null} is passed.
+ */
+ public static boolean allSamplesAreSyncSamples(@Nullable String mimeType) {
+ if (mimeType == null) {
+ return false;
+ }
+ // TODO: Consider adding additional audio MIME types here.
+ switch (mimeType) {
+ case AUDIO_AAC:
+ case AUDIO_MPEG:
+ case AUDIO_MPEG_L1:
+ case AUDIO_MPEG_L2:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Derives a video sample mimeType from a codecs attribute.
+ *
+ * @param codecs The codecs attribute.
+ * @return The derived video mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getVideoMediaMimeType(@Nullable String codecs) {
+ if (codecs == null) {
+ return null;
+ }
+ String[] codecList = Util.splitCodecs(codecs);
+ for (String codec : codecList) {
+ @Nullable String mimeType = getMediaMimeType(codec);
+ if (mimeType != null && isVideo(mimeType)) {
+ return mimeType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Derives a audio sample mimeType from a codecs attribute.
+ *
+ * @param codecs The codecs attribute.
+ * @return The derived audio mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getAudioMediaMimeType(@Nullable String codecs) {
+ if (codecs == null) {
+ return null;
+ }
+ String[] codecList = Util.splitCodecs(codecs);
+ for (String codec : codecList) {
+ @Nullable String mimeType = getMediaMimeType(codec);
+ if (mimeType != null && isAudio(mimeType)) {
+ return mimeType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Derives a mimeType from a codec identifier, as defined in RFC 6381.
+ *
+ * @param codec The codec identifier to derive.
+ * @return The mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getMediaMimeType(@Nullable String codec) {
+ if (codec == null) {
+ return null;
+ }
+ codec = Util.toLowerInvariant(codec.trim());
+ if (codec.startsWith("avc1") || codec.startsWith("avc3")) {
+ return MimeTypes.VIDEO_H264;
+ } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) {
+ return MimeTypes.VIDEO_H265;
+ } else if (codec.startsWith("dvav")
+ || codec.startsWith("dva1")
+ || codec.startsWith("dvhe")
+ || codec.startsWith("dvh1")) {
+ return MimeTypes.VIDEO_DOLBY_VISION;
+ } else if (codec.startsWith("av01")) {
+ return MimeTypes.VIDEO_AV1;
+ } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) {
+ return MimeTypes.VIDEO_VP9;
+ } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) {
+ return MimeTypes.VIDEO_VP8;
+ } else if (codec.startsWith("mp4a")) {
+ @Nullable String mimeType = null;
+ if (codec.startsWith("mp4a.")) {
+ String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix
+ if (objectTypeString.length() >= 2) {
+ try {
+ String objectTypeHexString = Util.toUpperInvariant(objectTypeString.substring(0, 2));
+ int objectTypeInt = Integer.parseInt(objectTypeHexString, 16);
+ mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt);
+ } catch (NumberFormatException ignored) {
+ // Ignored.
+ }
+ }
+ }
+ return mimeType == null ? MimeTypes.AUDIO_AAC : mimeType;
+ } else if (codec.startsWith("ac-3") || codec.startsWith("dac3")) {
+ return MimeTypes.AUDIO_AC3;
+ } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) {
+ return MimeTypes.AUDIO_E_AC3;
+ } else if (codec.startsWith("ec+3")) {
+ return MimeTypes.AUDIO_E_AC3_JOC;
+ } else if (codec.startsWith("ac-4") || codec.startsWith("dac4")) {
+ return MimeTypes.AUDIO_AC4;
+ } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) {
+ return MimeTypes.AUDIO_DTS;
+ } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) {
+ return MimeTypes.AUDIO_DTS_HD;
+ } else if (codec.startsWith("opus")) {
+ return MimeTypes.AUDIO_OPUS;
+ } else if (codec.startsWith("vorbis")) {
+ return MimeTypes.AUDIO_VORBIS;
+ } else if (codec.startsWith("flac")) {
+ return MimeTypes.AUDIO_FLAC;
+ } else if (codec.startsWith("stpp")) {
+ return MimeTypes.APPLICATION_TTML;
+ } else if (codec.startsWith("wvtt")) {
+ return MimeTypes.TEXT_VTT;
+ } else {
+ return getCustomMimeTypeForCodec(codec);
+ }
+ }
+
+ /**
+ * Derives a mimeType from MP4 object type identifier, as defined in RFC 6381 and
+ * https://mp4ra.org/#/object_types.
+ *
+ * @param objectType The objectType identifier to derive.
+ * @return The mimeType, or null if it could not be derived.
+ */
+ @Nullable
+ public static String getMimeTypeFromMp4ObjectType(int objectType) {
+ switch (objectType) {
+ case 0x20:
+ return MimeTypes.VIDEO_MP4V;
+ case 0x21:
+ return MimeTypes.VIDEO_H264;
+ case 0x23:
+ return MimeTypes.VIDEO_H265;
+ case 0x60:
+ case 0x61:
+ case 0x62:
+ case 0x63:
+ case 0x64:
+ case 0x65:
+ return MimeTypes.VIDEO_MPEG2;
+ case 0x6A:
+ return MimeTypes.VIDEO_MPEG;
+ case 0x69:
+ case 0x6B:
+ return MimeTypes.AUDIO_MPEG;
+ case 0xA3:
+ return MimeTypes.VIDEO_VC1;
+ case 0xB1:
+ return MimeTypes.VIDEO_VP9;
+ case 0x40:
+ case 0x66:
+ case 0x67:
+ case 0x68:
+ return MimeTypes.AUDIO_AAC;
+ case 0xA5:
+ return MimeTypes.AUDIO_AC3;
+ case 0xA6:
+ return MimeTypes.AUDIO_E_AC3;
+ case 0xA9:
+ case 0xAC:
+ return MimeTypes.AUDIO_DTS;
+ case 0xAA:
+ case 0xAB:
+ return MimeTypes.AUDIO_DTS_HD;
+ case 0xAD:
+ return MimeTypes.AUDIO_OPUS;
+ case 0xAE:
+ return MimeTypes.AUDIO_AC4;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type.
+ * {@link C#TRACK_TYPE_UNKNOWN} if the MIME type is not known or the mapping cannot be
+ * established.
+ *
+ * @param mimeType The MIME type.
+ * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type.
+ */
+ public static int getTrackType(@Nullable String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return C.TRACK_TYPE_UNKNOWN;
+ } else if (isAudio(mimeType)) {
+ return C.TRACK_TYPE_AUDIO;
+ } else if (isVideo(mimeType)) {
+ return C.TRACK_TYPE_VIDEO;
+ } else if (isText(mimeType) || APPLICATION_CEA608.equals(mimeType)
+ || APPLICATION_CEA708.equals(mimeType) || APPLICATION_MP4CEA608.equals(mimeType)
+ || APPLICATION_SUBRIP.equals(mimeType) || APPLICATION_TTML.equals(mimeType)
+ || APPLICATION_TX3G.equals(mimeType) || APPLICATION_MP4VTT.equals(mimeType)
+ || APPLICATION_RAWCC.equals(mimeType) || APPLICATION_VOBSUB.equals(mimeType)
+ || APPLICATION_PGS.equals(mimeType) || APPLICATION_DVBSUBS.equals(mimeType)) {
+ return C.TRACK_TYPE_TEXT;
+ } else if (APPLICATION_ID3.equals(mimeType)
+ || APPLICATION_EMSG.equals(mimeType)
+ || APPLICATION_SCTE35.equals(mimeType)) {
+ return C.TRACK_TYPE_METADATA;
+ } else if (APPLICATION_CAMERA_MOTION.equals(mimeType)) {
+ return C.TRACK_TYPE_CAMERA_MOTION;
+ } else {
+ return getTrackTypeForCustomMimeType(mimeType);
+ }
+ }
+
+ /**
+ * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to specified MIME type, if
+ * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise.
+ *
+ * @param mimeType The MIME type.
+ * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or
+ * {@link C#ENCODING_INVALID}.
+ */
+ public static @C.Encoding int getEncoding(String mimeType) {
+ switch (mimeType) {
+ case MimeTypes.AUDIO_MPEG:
+ return C.ENCODING_MP3;
+ case MimeTypes.AUDIO_AC3:
+ return C.ENCODING_AC3;
+ case MimeTypes.AUDIO_E_AC3:
+ return C.ENCODING_E_AC3;
+ case MimeTypes.AUDIO_E_AC3_JOC:
+ return C.ENCODING_E_AC3_JOC;
+ case MimeTypes.AUDIO_AC4:
+ return C.ENCODING_AC4;
+ case MimeTypes.AUDIO_DTS:
+ return C.ENCODING_DTS;
+ case MimeTypes.AUDIO_DTS_HD:
+ return C.ENCODING_DTS_HD;
+ case MimeTypes.AUDIO_TRUEHD:
+ return C.ENCODING_DOLBY_TRUEHD;
+ default:
+ return C.ENCODING_INVALID;
+ }
+ }
+
+ /**
+ * Equivalent to {@code getTrackType(getMediaMimeType(codec))}.
+ *
+ * @param codec The codec.
+ * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified codec.
+ */
+ public static int getTrackTypeOfCodec(String codec) {
+ return getTrackType(getMediaMimeType(codec));
+ }
+
+ /**
+ * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not
+ * contain a forward slash character ({@code '/'}).
+ */
+ @Nullable
+ private static String getTopLevelType(@Nullable String mimeType) {
+ if (mimeType == null) {
+ return null;
+ }
+ int indexOfSlash = mimeType.indexOf('/');
+ if (indexOfSlash == -1) {
+ return null;
+ }
+ return mimeType.substring(0, indexOfSlash);
+ }
+
+ @Nullable
+ private static String getCustomMimeTypeForCodec(String codec) {
+ int customMimeTypeCount = customMimeTypes.size();
+ for (int i = 0; i < customMimeTypeCount; i++) {
+ CustomMimeType customMimeType = customMimeTypes.get(i);
+ if (codec.startsWith(customMimeType.codecPrefix)) {
+ return customMimeType.mimeType;
+ }
+ }
+ return null;
+ }
+
+ private static int getTrackTypeForCustomMimeType(String mimeType) {
+ int customMimeTypeCount = customMimeTypes.size();
+ for (int i = 0; i < customMimeTypeCount; i++) {
+ CustomMimeType customMimeType = customMimeTypes.get(i);
+ if (mimeType.equals(customMimeType.mimeType)) {
+ return customMimeType.trackType;
+ }
+ }
+ return C.TRACK_TYPE_UNKNOWN;
+ }
+
+ private MimeTypes() {
+ // Prevent instantiation.
+ }
+
+ private static final class CustomMimeType {
+ public final String mimeType;
+ public final String codecPrefix;
+ public final int trackType;
+
+ public CustomMimeType(String mimeType, String codecPrefix, int trackType) {
+ this.mimeType = mimeType;
+ this.codecPrefix = codecPrefix;
+ this.trackType = trackType;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java
new file mode 100644
index 0000000000..d7409daa66
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Utility methods for handling H.264/AVC and H.265/HEVC NAL units.
+ */
+public final class NalUnitUtil {
+
+ private static final String TAG = "NalUnitUtil";
+
+ /**
+ * Holds data parsed from a sequence parameter set NAL unit.
+ */
+ public static final class SpsData {
+
+ public final int profileIdc;
+ public final int constraintsFlagsAndReservedZero2Bits;
+ public final int levelIdc;
+ public final int seqParameterSetId;
+ public final int width;
+ public final int height;
+ public final float pixelWidthAspectRatio;
+ public final boolean separateColorPlaneFlag;
+ public final boolean frameMbsOnlyFlag;
+ public final int frameNumLength;
+ public final int picOrderCountType;
+ public final int picOrderCntLsbLength;
+ public final boolean deltaPicOrderAlwaysZeroFlag;
+
+ public SpsData(
+ int profileIdc,
+ int constraintsFlagsAndReservedZero2Bits,
+ int levelIdc,
+ int seqParameterSetId,
+ int width,
+ int height,
+ float pixelWidthAspectRatio,
+ boolean separateColorPlaneFlag,
+ boolean frameMbsOnlyFlag,
+ int frameNumLength,
+ int picOrderCountType,
+ int picOrderCntLsbLength,
+ boolean deltaPicOrderAlwaysZeroFlag) {
+ this.profileIdc = profileIdc;
+ this.constraintsFlagsAndReservedZero2Bits = constraintsFlagsAndReservedZero2Bits;
+ this.levelIdc = levelIdc;
+ this.seqParameterSetId = seqParameterSetId;
+ this.width = width;
+ this.height = height;
+ this.pixelWidthAspectRatio = pixelWidthAspectRatio;
+ this.separateColorPlaneFlag = separateColorPlaneFlag;
+ this.frameMbsOnlyFlag = frameMbsOnlyFlag;
+ this.frameNumLength = frameNumLength;
+ this.picOrderCountType = picOrderCountType;
+ this.picOrderCntLsbLength = picOrderCntLsbLength;
+ this.deltaPicOrderAlwaysZeroFlag = deltaPicOrderAlwaysZeroFlag;
+ }
+
+ }
+
+ /**
+ * Holds data parsed from a picture parameter set NAL unit.
+ */
+ public static final class PpsData {
+
+ public final int picParameterSetId;
+ public final int seqParameterSetId;
+ public final boolean bottomFieldPicOrderInFramePresentFlag;
+
+ public PpsData(int picParameterSetId, int seqParameterSetId,
+ boolean bottomFieldPicOrderInFramePresentFlag) {
+ this.picParameterSetId = picParameterSetId;
+ this.seqParameterSetId = seqParameterSetId;
+ this.bottomFieldPicOrderInFramePresentFlag = bottomFieldPicOrderInFramePresentFlag;
+ }
+
+ }
+
+ /** Four initial bytes that must prefix NAL units for decoding. */
+ public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
+
+ /** Value for aspect_ratio_idc indicating an extended aspect ratio, in H.264 and H.265 SPSs. */
+ public static final int EXTENDED_SAR = 0xFF;
+ /** Aspect ratios indexed by aspect_ratio_idc, in H.264 and H.265 SPSs. */
+ public static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {
+ 1f /* Unspecified. Assume square */,
+ 1f,
+ 12f / 11f,
+ 10f / 11f,
+ 16f / 11f,
+ 40f / 33f,
+ 24f / 11f,
+ 20f / 11f,
+ 32f / 11f,
+ 80f / 33f,
+ 18f / 11f,
+ 15f / 11f,
+ 64f / 33f,
+ 160f / 99f,
+ 4f / 3f,
+ 3f / 2f,
+ 2f
+ };
+
+ private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+ private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
+ private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39;
+
+ private static final Object scratchEscapePositionsLock = new Object();
+
+ /**
+ * Temporary store for positions of escape codes in {@link #unescapeStream(byte[], int)}. Guarded
+ * by {@link #scratchEscapePositionsLock}.
+ */
+ private static int[] scratchEscapePositions = new int[10];
+
+ /**
+ * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with
+ * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.
+ * <p>
+ * Executions of this method are mutually exclusive, so it should not be called with very large
+ * buffers.
+ *
+ * @param data The data to unescape.
+ * @param limit The limit (exclusive) of the data to unescape.
+ * @return The length of the unescaped data.
+ */
+ public static int unescapeStream(byte[] data, int limit) {
+ synchronized (scratchEscapePositionsLock) {
+ int position = 0;
+ int scratchEscapeCount = 0;
+ while (position < limit) {
+ position = findNextUnescapeIndex(data, position, limit);
+ if (position < limit) {
+ if (scratchEscapePositions.length <= scratchEscapeCount) {
+ // Grow scratchEscapePositions to hold a larger number of positions.
+ scratchEscapePositions = Arrays.copyOf(scratchEscapePositions,
+ scratchEscapePositions.length * 2);
+ }
+ scratchEscapePositions[scratchEscapeCount++] = position;
+ position += 3;
+ }
+ }
+
+ int unescapedLength = limit - scratchEscapeCount;
+ int escapedPosition = 0; // The position being read from.
+ int unescapedPosition = 0; // The position being written to.
+ for (int i = 0; i < scratchEscapeCount; i++) {
+ int nextEscapePosition = scratchEscapePositions[i];
+ int copyLength = nextEscapePosition - escapedPosition;
+ System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength);
+ unescapedPosition += copyLength;
+ data[unescapedPosition++] = 0;
+ data[unescapedPosition++] = 0;
+ escapedPosition += copyLength + 3;
+ }
+
+ int remainingLength = unescapedLength - unescapedPosition;
+ System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength);
+ return unescapedLength;
+ }
+ }
+
+ /**
+ * Discards data from the buffer up to the first SPS, where {@code data.position()} is interpreted
+ * as the length of the buffer.
+ * <p>
+ * When the method returns, {@code data.position()} will contain the new length of the buffer. If
+ * the buffer is not empty it is guaranteed to start with an SPS.
+ *
+ * @param data Buffer containing start code delimited NAL units.
+ */
+ public static void discardToSps(ByteBuffer data) {
+ int length = data.position();
+ int consecutiveZeros = 0;
+ int offset = 0;
+ while (offset + 1 < length) {
+ int value = data.get(offset) & 0xFF;
+ if (consecutiveZeros == 3) {
+ if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) {
+ // Copy from this NAL unit onwards to the start of the buffer.
+ ByteBuffer offsetData = data.duplicate();
+ offsetData.position(offset - 3);
+ offsetData.limit(length);
+ data.position(0);
+ data.put(offsetData);
+ return;
+ }
+ } else if (value == 0) {
+ consecutiveZeros++;
+ }
+ if (value != 0) {
+ consecutiveZeros = 0;
+ }
+ offset++;
+ }
+ // Empty the buffer if the SPS NAL unit was not found.
+ data.clear();
+ }
+
+ /**
+ * Returns whether the NAL unit with the specified header contains supplemental enhancement
+ * information.
+ *
+ * @param mimeType The sample MIME type.
+ * @param nalUnitHeaderFirstByte The first byte of nal_unit().
+ * @return Whether the NAL unit with the specified header is an SEI NAL unit.
+ */
+ public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) {
+ return (MimeTypes.VIDEO_H264.equals(mimeType)
+ && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI)
+ || (MimeTypes.VIDEO_H265.equals(mimeType)
+ && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI);
+ }
+
+ /**
+ * Returns the type of the NAL unit in {@code data} that starts at {@code offset}.
+ *
+ * @param data The data to search.
+ * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
+ * {@code data.length - 3} (exclusive).
+ * @return The type of the unit.
+ */
+ public static int getNalUnitType(byte[] data, int offset) {
+ return data[offset + 3] & 0x1F;
+ }
+
+ /**
+ * Returns the type of the H.265 NAL unit in {@code data} that starts at {@code offset}.
+ *
+ * @param data The data to search.
+ * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and
+ * {@code data.length - 3} (exclusive).
+ * @return The type of the unit.
+ */
+ public static int getH265NalUnitType(byte[] data, int offset) {
+ return (data[offset + 3] & 0x7E) >> 1;
+ }
+
+ /**
+ * Parses an SPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
+ * 7.3.2.1.1.
+ *
+ * @param nalData A buffer containing escaped SPS data.
+ * @param nalOffset The offset of the NAL unit header in {@code nalData}.
+ * @param nalLimit The limit of the NAL unit in {@code nalData}.
+ * @return A parsed representation of the SPS data.
+ */
+ public static SpsData parseSpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {
+ ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);
+ data.skipBits(8); // nal_unit
+ int profileIdc = data.readBits(8);
+ int constraintsFlagsAndReservedZero2Bits = data.readBits(8);
+ int levelIdc = data.readBits(8);
+ int seqParameterSetId = data.readUnsignedExpGolombCodedInt();
+
+ int chromaFormatIdc = 1; // Default is 4:2:0
+ boolean separateColorPlaneFlag = false;
+ if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244
+ || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118
+ || profileIdc == 128 || profileIdc == 138) {
+ chromaFormatIdc = data.readUnsignedExpGolombCodedInt();
+ if (chromaFormatIdc == 3) {
+ separateColorPlaneFlag = data.readBit();
+ }
+ data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+ data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+ data.skipBit(); // qpprime_y_zero_transform_bypass_flag
+ boolean seqScalingMatrixPresentFlag = data.readBit();
+ if (seqScalingMatrixPresentFlag) {
+ int limit = (chromaFormatIdc != 3) ? 8 : 12;
+ for (int i = 0; i < limit; i++) {
+ boolean seqScalingListPresentFlag = data.readBit();
+ if (seqScalingListPresentFlag) {
+ skipScalingList(data, i < 6 ? 16 : 64);
+ }
+ }
+ }
+ }
+
+ int frameNumLength = data.readUnsignedExpGolombCodedInt() + 4; // log2_max_frame_num_minus4 + 4
+ int picOrderCntType = data.readUnsignedExpGolombCodedInt();
+ int picOrderCntLsbLength = 0;
+ boolean deltaPicOrderAlwaysZeroFlag = false;
+ if (picOrderCntType == 0) {
+ // log2_max_pic_order_cnt_lsb_minus4 + 4
+ picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4;
+ } else if (picOrderCntType == 1) {
+ deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flag
+ data.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic
+ data.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field
+ long numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt();
+ for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
+ data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
+ }
+ }
+ data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
+ data.skipBit(); // gaps_in_frame_num_value_allowed_flag
+
+ int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;
+ int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1;
+ boolean frameMbsOnlyFlag = data.readBit();
+ int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;
+ if (!frameMbsOnlyFlag) {
+ data.skipBit(); // mb_adaptive_frame_field_flag
+ }
+
+ data.skipBit(); // direct_8x8_inference_flag
+ int frameWidth = picWidthInMbs * 16;
+ int frameHeight = frameHeightInMbs * 16;
+ boolean frameCroppingFlag = data.readBit();
+ if (frameCroppingFlag) {
+ int frameCropLeftOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropRightOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropTopOffset = data.readUnsignedExpGolombCodedInt();
+ int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt();
+ int cropUnitX;
+ int cropUnitY;
+ if (chromaFormatIdc == 0) {
+ cropUnitX = 1;
+ cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0);
+ } else {
+ int subWidthC = (chromaFormatIdc == 3) ? 1 : 2;
+ int subHeightC = (chromaFormatIdc == 1) ? 2 : 1;
+ cropUnitX = subWidthC;
+ cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0));
+ }
+ frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;
+ frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
+ }
+
+ float pixelWidthHeightRatio = 1;
+ boolean vuiParametersPresentFlag = data.readBit();
+ if (vuiParametersPresentFlag) {
+ boolean aspectRatioInfoPresentFlag = data.readBit();
+ if (aspectRatioInfoPresentFlag) {
+ int aspectRatioIdc = data.readBits(8);
+ if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) {
+ int sarWidth = data.readBits(16);
+ int sarHeight = data.readBits(16);
+ if (sarWidth != 0 && sarHeight != 0) {
+ pixelWidthHeightRatio = (float) sarWidth / sarHeight;
+ }
+ } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) {
+ pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc];
+ } else {
+ Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
+ }
+ }
+ }
+
+ return new SpsData(
+ profileIdc,
+ constraintsFlagsAndReservedZero2Bits,
+ levelIdc,
+ seqParameterSetId,
+ frameWidth,
+ frameHeight,
+ pixelWidthHeightRatio,
+ separateColorPlaneFlag,
+ frameMbsOnlyFlag,
+ frameNumLength,
+ picOrderCntType,
+ picOrderCntLsbLength,
+ deltaPicOrderAlwaysZeroFlag);
+ }
+
+ /**
+ * Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection
+ * 7.3.2.2.
+ *
+ * @param nalData A buffer containing escaped PPS data.
+ * @param nalOffset The offset of the NAL unit header in {@code nalData}.
+ * @param nalLimit The limit of the NAL unit in {@code nalData}.
+ * @return A parsed representation of the PPS data.
+ */
+ public static PpsData parsePpsNalUnit(byte[] nalData, int nalOffset, int nalLimit) {
+ ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit);
+ data.skipBits(8); // nal_unit
+ int picParameterSetId = data.readUnsignedExpGolombCodedInt();
+ int seqParameterSetId = data.readUnsignedExpGolombCodedInt();
+ data.skipBit(); // entropy_coding_mode_flag
+ boolean bottomFieldPicOrderInFramePresentFlag = data.readBit();
+ return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag);
+ }
+
+ /**
+ * Finds the first NAL unit in {@code data}.
+ * <p>
+ * If {@code prefixFlags} is null then the first three bytes of a NAL unit must be entirely
+ * contained within the part of the array being searched in order for it to be found.
+ * <p>
+ * When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four
+ * bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same
+ * {@code prefixFlags} parameter to successive calls. State maintained in this parameter enables
+ * the detection of such NAL units. Note that when using this feature, the return value may be 3,
+ * 2 or 1 less than {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before
+ * the first byte in the current array.
+ *
+ * @param data The data to search.
+ * @param startOffset The offset (inclusive) in the data to start the search.
+ * @param endOffset The offset (exclusive) in the data to end the search.
+ * @param prefixFlags A boolean array whose first three elements are used to store the state
+ * required to detect NAL units where the NAL unit prefix spans array boundaries. The array
+ * must be at least 3 elements long.
+ * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found.
+ */
+ public static int findNalUnit(byte[] data, int startOffset, int endOffset,
+ boolean[] prefixFlags) {
+ int length = endOffset - startOffset;
+
+ Assertions.checkState(length >= 0);
+ if (length == 0) {
+ return endOffset;
+ }
+
+ if (prefixFlags != null) {
+ if (prefixFlags[0]) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 3;
+ } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 2;
+ } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0
+ && data[startOffset + 1] == 1) {
+ clearPrefixFlags(prefixFlags);
+ return startOffset - 1;
+ }
+ }
+
+ int limit = endOffset - 1;
+ // We're looking for the NAL unit start code prefix 0x000001. The value of i tracks the index of
+ // the third byte.
+ for (int i = startOffset + 2; i < limit; i += 3) {
+ if ((data[i] & 0xFE) != 0) {
+ // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the
+ // loop advance the index by three.
+ } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) {
+ if (prefixFlags != null) {
+ clearPrefixFlags(prefixFlags);
+ }
+ return i - 2;
+ } else {
+ // There isn't a NAL prefix here, but there might be at the next position. We should
+ // only skip forward by one. The loop will skip forward by three, so subtract two here.
+ i -= 2;
+ }
+ }
+
+ if (prefixFlags != null) {
+ // True if the last three bytes in the data seen so far are {0,0,1}.
+ prefixFlags[0] = length > 2
+ ? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
+ : length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1)
+ : (prefixFlags[1] && data[endOffset - 1] == 1);
+ // True if the last two bytes in the data seen so far are {0,0}.
+ prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0
+ : prefixFlags[2] && data[endOffset - 1] == 0;
+ // True if the last byte in the data seen so far is {0}.
+ prefixFlags[2] = data[endOffset - 1] == 0;
+ }
+
+ return endOffset;
+ }
+
+ /**
+ * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}.
+ *
+ * @param prefixFlags The flags to clear.
+ */
+ public static void clearPrefixFlags(boolean[] prefixFlags) {
+ prefixFlags[0] = false;
+ prefixFlags[1] = false;
+ prefixFlags[2] = false;
+ }
+
+ private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) {
+ for (int i = offset; i < limit - 2; i++) {
+ if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) {
+ return i;
+ }
+ }
+ return limit;
+ }
+
+ private static void skipScalingList(ParsableNalUnitBitArray bitArray, int size) {
+ int lastScale = 8;
+ int nextScale = 8;
+ for (int i = 0; i < size; i++) {
+ if (nextScale != 0) {
+ int deltaScale = bitArray.readSignedExpGolombCodedInt();
+ nextScale = (lastScale + deltaScale + 256) % 256;
+ }
+ lastScale = (nextScale == 0) ? lastScale : nextScale;
+ }
+ }
+
+ private NalUnitUtil() {
+ // Prevent instantiation.
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java
new file mode 100644
index 0000000000..0c9b9b2182
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NonNullApi.java
@@ -0,0 +1,34 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import javax.annotation.Nonnull;
+import javax.annotation.meta.TypeQualifierDefault;
+import kotlin.annotations.jvm.MigrationStatus;
+import kotlin.annotations.jvm.UnderMigration;
+
+/**
+ * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless
+ * explicitly marked with a nullable annotation.
+ */
+@Nonnull
+@TypeQualifierDefault(ElementType.TYPE_USE)
+@UnderMigration(status = MigrationStatus.STRICT)
+@Retention(RetentionPolicy.CLASS)
+public @interface NonNullApi {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java
new file mode 100644
index 0000000000..df68c8fe59
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/NotificationUtil.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Utility methods for displaying {@link Notification Notifications}. */
+@SuppressLint("InlinedApi")
+public final class NotificationUtil {
+
+ /**
+ * Notification channel importance levels. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link
+ * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link
+ * #IMPORTANCE_DEFAULT} or {@link #IMPORTANCE_HIGH}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ IMPORTANCE_UNSPECIFIED,
+ IMPORTANCE_NONE,
+ IMPORTANCE_MIN,
+ IMPORTANCE_LOW,
+ IMPORTANCE_DEFAULT,
+ IMPORTANCE_HIGH
+ })
+ public @interface Importance {}
+ /** @see NotificationManager#IMPORTANCE_UNSPECIFIED */
+ public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED;
+ /** @see NotificationManager#IMPORTANCE_NONE */
+ public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE;
+ /** @see NotificationManager#IMPORTANCE_MIN */
+ public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN;
+ /** @see NotificationManager#IMPORTANCE_LOW */
+ public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW;
+ /** @see NotificationManager#IMPORTANCE_DEFAULT */
+ public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT;
+ /** @see NotificationManager#IMPORTANCE_HIGH */
+ public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH;
+
+ /** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */
+ @Deprecated
+ public static void createNotificationChannel(
+ Context context, String id, @StringRes int nameResourceId, @Importance int importance) {
+ createNotificationChannel(
+ context, id, nameResourceId, /* descriptionResourceId= */ 0, importance);
+ }
+
+ /**
+ * Creates a notification channel that notifications can be posted to. See {@link
+ * NotificationChannel} and {@link
+ * NotificationManager#createNotificationChannel(NotificationChannel)} for details.
+ *
+ * @param context A {@link Context}.
+ * @param id The id of the channel. Must be unique per package. The value may be truncated if it's
+ * too long.
+ * @param nameResourceId A string resource identifier for the user visible name of the channel.
+ * The recommended maximum length is 40 characters. The string may be truncated if it's too
+ * long. You can rename the channel when the system locale changes by listening for the {@link
+ * Intent#ACTION_LOCALE_CHANGED} broadcast.
+ * @param descriptionResourceId A string resource identifier for the user visible description of
+ * the channel, or 0 if no description is provided. The recommended maximum length is 300
+ * characters. The value may be truncated if it is too long. You can change the description of
+ * the channel when the system locale changes by listening for the {@link
+ * Intent#ACTION_LOCALE_CHANGED} broadcast.
+ * @param importance The importance of the channel. This controls how interruptive notifications
+ * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link
+ * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link
+ * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}.
+ */
+ public static void createNotificationChannel(
+ Context context,
+ String id,
+ @StringRes int nameResourceId,
+ @StringRes int descriptionResourceId,
+ @Importance int importance) {
+ if (Util.SDK_INT >= 26) {
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationChannel channel =
+ new NotificationChannel(id, context.getString(nameResourceId), importance);
+ if (descriptionResourceId != 0) {
+ channel.setDescription(context.getString(descriptionResourceId));
+ }
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ /**
+ * Post a notification to be shown in the status bar. If a notification with the same id has
+ * already been posted by your application and has not yet been canceled, it will be replaced by
+ * the updated information. If {@code notification} is {@code null} then any notification
+ * previously shown with the specified id will be cancelled.
+ *
+ * @param context A {@link Context}.
+ * @param id The notification id.
+ * @param notification The {@link Notification} to post, or {@code null} to cancel a previously
+ * shown notification.
+ */
+ public static void setNotification(Context context, int id, @Nullable Notification notification) {
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notification != null) {
+ notificationManager.notify(id, notification);
+ } else {
+ notificationManager.cancel(id);
+ }
+ }
+
+ private NotificationUtil() {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java
new file mode 100644
index 0000000000..3d6a702723
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a bitstream.
+ */
+public final class ParsableBitArray {
+
+ public byte[] data;
+
+ // The offset within the data, stored as the current byte offset, and the bit offset within that
+ // byte (from 0 to 7).
+ private int byteOffset;
+ private int bitOffset;
+ private int byteLimit;
+
+ /** Creates a new instance that initially has no backing data. */
+ public ParsableBitArray() {
+ data = Util.EMPTY_BYTE_ARRAY;
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ */
+ public ParsableBitArray(byte[] data) {
+ this(data, data.length);
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ * @param limit The limit in bytes.
+ */
+ public ParsableBitArray(byte[] data, int limit) {
+ this.data = data;
+ byteLimit = limit;
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ */
+ public void reset(byte[] data) {
+ reset(data, data.length);
+ }
+
+ /**
+ * Sets this instance's data, position and limit to match the provided {@code parsableByteArray}.
+ * Any modifications to the underlying data array will be visible in both instances
+ *
+ * @param parsableByteArray The {@link ParsableByteArray}.
+ */
+ public void reset(ParsableByteArray parsableByteArray) {
+ reset(parsableByteArray.data, parsableByteArray.limit());
+ setPosition(parsableByteArray.getPosition() * 8);
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ * @param limit The limit in bytes.
+ */
+ public void reset(byte[] data, int limit) {
+ this.data = data;
+ byteOffset = 0;
+ bitOffset = 0;
+ byteLimit = limit;
+ }
+
+ /**
+ * Returns the number of bits yet to be read.
+ */
+ public int bitsLeft() {
+ return (byteLimit - byteOffset) * 8 - bitOffset;
+ }
+
+ /**
+ * Returns the current bit offset.
+ */
+ public int getPosition() {
+ return byteOffset * 8 + bitOffset;
+ }
+
+ /**
+ * Returns the current byte offset. Must only be called when the position is byte aligned.
+ *
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public int getBytePosition() {
+ Assertions.checkState(bitOffset == 0);
+ return byteOffset;
+ }
+
+ /**
+ * Sets the current bit offset.
+ *
+ * @param position The position to set.
+ */
+ public void setPosition(int position) {
+ byteOffset = position / 8;
+ bitOffset = position - (byteOffset * 8);
+ assertValidOffset();
+ }
+
+ /**
+ * Skips a single bit.
+ */
+ public void skipBit() {
+ if (++bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset++;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Skips bits and moves current reading position forward.
+ *
+ * @param numBits The number of bits to skip.
+ */
+ public void skipBits(int numBits) {
+ int numBytes = numBits / 8;
+ byteOffset += numBytes;
+ bitOffset += numBits - (numBytes * 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return Whether the bit is set.
+ */
+ public boolean readBit() {
+ boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0;
+ skipBit();
+ return returnValue;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom {@code numBits} bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ if (numBits == 0) {
+ return 0;
+ }
+ int returnValue = 0;
+ bitOffset += numBits;
+ while (bitOffset > 8) {
+ bitOffset -= 8;
+ returnValue |= (data[byteOffset++] & 0xFF) << bitOffset;
+ }
+ returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
+ returnValue &= 0xFFFFFFFF >>> (32 - numBits);
+ if (bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset++;
+ }
+ assertValidOffset();
+ return returnValue;
+ }
+
+ /**
+ * Reads up to 64 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return A long whose bottom {@code numBits} bits hold the read data.
+ */
+ public long readBitsToLong(int numBits) {
+ if (numBits <= 32) {
+ return Util.toUnsignedLong(readBits(numBits));
+ }
+ return Util.toLong(readBits(numBits - 32), readBits(32));
+ }
+
+ /**
+ * Reads {@code numBits} bits into {@code buffer}.
+ *
+ * @param buffer The array into which the read data should be written. The trailing {@code numBits
+ * % 8} bits are written into the most significant bits of the last modified {@code buffer}
+ * byte. The remaining ones are unmodified.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param numBits The number of bits to read.
+ */
+ public void readBits(byte[] buffer, int offset, int numBits) {
+ // Whole bytes.
+ int to = offset + (numBits >> 3) /* numBits / 8 */;
+ for (int i = offset; i < to; i++) {
+ buffer[i] = (byte) (data[byteOffset++] << bitOffset);
+ buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset)));
+ }
+ // Trailing bits.
+ int bitsLeft = numBits & 7 /* numBits % 8 */;
+ if (bitsLeft == 0) {
+ return;
+ }
+ // Set bits that are going to be overwritten to 0.
+ buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft));
+ if (bitOffset + bitsLeft > 8) {
+ // We read the rest of data[byteOffset] and increase byteOffset.
+ buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset));
+ bitOffset -= 8;
+ }
+ bitOffset += bitsLeft;
+ int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset);
+ buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft));
+ if (bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset++;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
+ */
+ public void byteAlign() {
+ if (bitOffset == 0) {
+ return;
+ }
+ bitOffset = 0;
+ byteOffset++;
+ assertValidOffset();
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer}. Must only be called when the position
+ * is byte aligned.
+ *
+ * @see System#arraycopy(Object, int, Object, int, int)
+ * @param buffer The array into which the read data should be written.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param length The number of bytes to read.
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public void readBytes(byte[] buffer, int offset, int length) {
+ Assertions.checkState(bitOffset == 0);
+ System.arraycopy(data, byteOffset, buffer, offset, length);
+ byteOffset += length;
+ assertValidOffset();
+ }
+
+ /**
+ * Skips the next {@code length} bytes. Must only be called when the position is byte aligned.
+ *
+ * @param length The number of bytes to read.
+ * @throws IllegalStateException If the position isn't byte aligned.
+ */
+ public void skipBytes(int length) {
+ Assertions.checkState(bitOffset == 0);
+ byteOffset += length;
+ assertValidOffset();
+ }
+
+ /**
+ * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits
+ * from {@code value}. Bits are written in order from most significant to least significant. The
+ * read position is advanced by {@code numBits}.
+ *
+ * @param value The integer whose {@code numBits} least significant bits are written into {@link
+ * #data}.
+ * @param numBits The number of bits to write.
+ */
+ public void putInt(int value, int numBits) {
+ int remainingBitsToRead = numBits;
+ if (numBits < 32) {
+ value &= (1 << numBits) - 1;
+ }
+ int firstByteReadSize = Math.min(8 - bitOffset, numBits);
+ int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize;
+ int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1);
+ data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask);
+ int firstByteInputBits = value >>> (numBits - firstByteReadSize);
+ data[byteOffset] =
+ (byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize));
+ remainingBitsToRead -= firstByteReadSize;
+ int currentByteIndex = byteOffset + 1;
+ while (remainingBitsToRead > 8) {
+ data[currentByteIndex++] = (byte) (value >>> (remainingBitsToRead - 8));
+ remainingBitsToRead -= 8;
+ }
+ int lastByteRightPaddingSize = 8 - remainingBitsToRead;
+ data[currentByteIndex] =
+ (byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1));
+ int lastByteInput = value & ((1 << remainingBitsToRead) - 1);
+ data[currentByteIndex] =
+ (byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize));
+ skipBits(numBits);
+ assertValidOffset();
+ }
+
+ private void assertValidOffset() {
+ // It is fine for position to be at the end of the array, but no further.
+ Assertions.checkState(byteOffset >= 0
+ && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java
new file mode 100644
index 0000000000..9ad9dd1aa7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are
+ * parsed with the assumption that their constituent bytes are in big endian order.
+ */
+public final class ParsableByteArray {
+
+ public byte[] data;
+
+ private int position;
+ private int limit;
+
+ /** Creates a new instance that initially has no backing data. */
+ public ParsableByteArray() {
+ data = Util.EMPTY_BYTE_ARRAY;
+ }
+
+ /**
+ * Creates a new instance with {@code limit} bytes and sets the limit.
+ *
+ * @param limit The limit to set.
+ */
+ public ParsableByteArray(int limit) {
+ this.data = new byte[limit];
+ this.limit = limit;
+ }
+
+ /**
+ * Creates a new instance wrapping {@code data}, and sets the limit to {@code data.length}.
+ *
+ * @param data The array to wrap.
+ */
+ public ParsableByteArray(byte[] data) {
+ this.data = data;
+ limit = data.length;
+ }
+
+ /**
+ * Creates a new instance that wraps an existing array.
+ *
+ * @param data The data to wrap.
+ * @param limit The limit to set.
+ */
+ public ParsableByteArray(byte[] data, int limit) {
+ this.data = data;
+ this.limit = limit;
+ }
+
+ /** Sets the position and limit to zero. */
+ public void reset() {
+ position = 0;
+ limit = 0;
+ }
+
+ /**
+ * Resets the position to zero and the limit to the specified value. If the limit exceeds the
+ * capacity, {@code data} is replaced with a new array of sufficient size.
+ *
+ * @param limit The limit to set.
+ */
+ public void reset(int limit) {
+ reset(capacity() < limit ? new byte[limit] : data, limit);
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero and the limit to
+ * {@code data.length}.
+ *
+ * @param data The array to wrap.
+ */
+ public void reset(byte[] data) {
+ reset(data, data.length);
+ }
+
+ /**
+ * Updates the instance to wrap {@code data}, and resets the position to zero.
+ *
+ * @param data The array to wrap.
+ * @param limit The limit to set.
+ */
+ public void reset(byte[] data, int limit) {
+ this.data = data;
+ this.limit = limit;
+ position = 0;
+ }
+
+ /**
+ * Returns the number of bytes yet to be read.
+ */
+ public int bytesLeft() {
+ return limit - position;
+ }
+
+ /**
+ * Returns the limit.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Sets the limit.
+ *
+ * @param limit The limit to set.
+ */
+ public void setLimit(int limit) {
+ Assertions.checkArgument(limit >= 0 && limit <= data.length);
+ this.limit = limit;
+ }
+
+ /**
+ * Returns the current offset in the array, in bytes.
+ */
+ public int getPosition() {
+ return position;
+ }
+
+ /**
+ * Returns the capacity of the array, which may be larger than the limit.
+ */
+ public int capacity() {
+ return data.length;
+ }
+
+ /**
+ * Sets the reading offset in the array.
+ *
+ * @param position Byte offset in the array from which to read.
+ * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
+ * array.
+ */
+ public void setPosition(int position) {
+ // It is fine for position to be at the end of the array.
+ Assertions.checkArgument(position >= 0 && position <= limit);
+ this.position = position;
+ }
+
+ /**
+ * Moves the reading offset by {@code bytes}.
+ *
+ * @param bytes The number of bytes to skip.
+ * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the
+ * array.
+ */
+ public void skipBytes(int bytes) {
+ setPosition(position + bytes);
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of
+ * {@code bitArray} to zero.
+ *
+ * @param bitArray The {@link ParsableBitArray} into which the bytes should be read.
+ * @param length The number of bytes to write.
+ */
+ public void readBytes(ParsableBitArray bitArray, int length) {
+ readBytes(bitArray.data, 0, length);
+ bitArray.setPosition(0);
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer} at {@code offset}.
+ *
+ * @see System#arraycopy(Object, int, Object, int, int)
+ * @param buffer The array into which the read data should be written.
+ * @param offset The offset in {@code buffer} at which the read data should be written.
+ * @param length The number of bytes to read.
+ */
+ public void readBytes(byte[] buffer, int offset, int length) {
+ System.arraycopy(data, position, buffer, offset, length);
+ position += length;
+ }
+
+ /**
+ * Reads the next {@code length} bytes into {@code buffer}.
+ *
+ * @see ByteBuffer#put(byte[], int, int)
+ * @param buffer The {@link ByteBuffer} into which the read data should be written.
+ * @param length The number of bytes to read.
+ */
+ public void readBytes(ByteBuffer buffer, int length) {
+ buffer.put(data, position, length);
+ position += length;
+ }
+
+ /**
+ * Peeks at the next byte as an unsigned value.
+ */
+ public int peekUnsignedByte() {
+ return (data[position] & 0xFF);
+ }
+
+ /**
+ * Peeks at the next char.
+ */
+ public char peekChar() {
+ return (char) ((data[position] & 0xFF) << 8
+ | (data[position + 1] & 0xFF));
+ }
+
+ /**
+ * Reads the next byte as an unsigned value.
+ */
+ public int readUnsignedByte() {
+ return (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next two bytes as an unsigned value.
+ */
+ public int readUnsignedShort() {
+ return (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next two bytes as an unsigned value.
+ */
+ public int readLittleEndianUnsignedShort() {
+ return (data[position++] & 0xFF) | (data[position++] & 0xFF) << 8;
+ }
+
+ /**
+ * Reads the next two bytes as a signed value.
+ */
+ public short readShort() {
+ return (short) ((data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF));
+ }
+
+ /**
+ * Reads the next two bytes as a signed value.
+ */
+ public short readLittleEndianShort() {
+ return (short) ((data[position++] & 0xFF) | (data[position++] & 0xFF) << 8);
+ }
+
+ /**
+ * Reads the next three bytes as an unsigned value.
+ */
+ public int readUnsignedInt24() {
+ return (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next three bytes as a signed value.
+ */
+ public int readInt24() {
+ return ((data[position++] & 0xFF) << 24) >> 8
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next three bytes as a signed value in little endian order.
+ */
+ public int readLittleEndianInt24() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16;
+ }
+
+ /**
+ * Reads the next three bytes as an unsigned value in little endian order.
+ */
+ public int readLittleEndianUnsignedInt24() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16;
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned value.
+ */
+ public long readUnsignedInt() {
+ return (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL);
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned value in little endian order.
+ */
+ public long readLittleEndianUnsignedInt() {
+ return (data[position++] & 0xFFL)
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 24;
+ }
+
+ /**
+ * Reads the next four bytes as a signed value
+ */
+ public int readInt() {
+ return (data[position++] & 0xFF) << 24
+ | (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ }
+
+ /**
+ * Reads the next four bytes as a signed value in little endian order.
+ */
+ public int readLittleEndianInt() {
+ return (data[position++] & 0xFF)
+ | (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF) << 16
+ | (data[position++] & 0xFF) << 24;
+ }
+
+ /**
+ * Reads the next eight bytes as a signed value.
+ */
+ public long readLong() {
+ return (data[position++] & 0xFFL) << 56
+ | (data[position++] & 0xFFL) << 48
+ | (data[position++] & 0xFFL) << 40
+ | (data[position++] & 0xFFL) << 32
+ | (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL);
+ }
+
+ /**
+ * Reads the next eight bytes as a signed value in little endian order.
+ */
+ public long readLittleEndianLong() {
+ return (data[position++] & 0xFFL)
+ | (data[position++] & 0xFFL) << 8
+ | (data[position++] & 0xFFL) << 16
+ | (data[position++] & 0xFFL) << 24
+ | (data[position++] & 0xFFL) << 32
+ | (data[position++] & 0xFFL) << 40
+ | (data[position++] & 0xFFL) << 48
+ | (data[position++] & 0xFFL) << 56;
+ }
+
+ /**
+ * Reads the next four bytes, returning the integer portion of the fixed point 16.16 integer.
+ */
+ public int readUnsignedFixedPoint1616() {
+ int result = (data[position++] & 0xFF) << 8
+ | (data[position++] & 0xFF);
+ position += 2; // Skip the non-integer portion.
+ return result;
+ }
+
+ /**
+ * Reads a Synchsafe integer.
+ * <p>
+ * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can
+ * store 28 bits of information.
+ *
+ * @return The parsed value.
+ */
+ public int readSynchSafeInt() {
+ int b1 = readUnsignedByte();
+ int b2 = readUnsignedByte();
+ int b3 = readUnsignedByte();
+ int b4 = readUnsignedByte();
+ return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
+ }
+
+ /**
+ * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public int readUnsignedIntToInt() {
+ int result = readInt();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next four bytes as a little endian unsigned integer into an integer, if the top bit
+ * is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public int readLittleEndianUnsignedIntToInt() {
+ int result = readLittleEndianInt();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next eight bytes as an unsigned long into a long, if the top bit is a zero.
+ *
+ * @throws IllegalStateException Thrown if the top bit of the input data is set.
+ */
+ public long readUnsignedLongToLong() {
+ long result = readLong();
+ if (result < 0) {
+ throw new IllegalStateException("Top bit not zero: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Reads the next four bytes as a 32-bit floating point value.
+ */
+ public float readFloat() {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ /**
+ * Reads the next eight bytes as a 64-bit floating point value.
+ */
+ public double readDouble() {
+ return Double.longBitsToDouble(readLong());
+ }
+
+ /**
+ * Reads the next {@code length} bytes as UTF-8 characters.
+ *
+ * @param length The number of bytes to read.
+ * @return The string encoded by the bytes.
+ */
+ public String readString(int length) {
+ return readString(length, Charset.forName(C.UTF8_NAME));
+ }
+
+ /**
+ * Reads the next {@code length} bytes as characters in the specified {@link Charset}.
+ *
+ * @param length The number of bytes to read.
+ * @param charset The character set of the encoded characters.
+ * @return The string encoded by the bytes in the specified character set.
+ */
+ public String readString(int length, Charset charset) {
+ String result = new String(data, position, length, charset);
+ position += length;
+ return result;
+ }
+
+ /**
+ * Reads the next {@code length} bytes as UTF-8 characters. A terminating NUL byte is discarded,
+ * if present.
+ *
+ * @param length The number of bytes to read.
+ * @return The string, not including any terminating NUL byte.
+ */
+ public String readNullTerminatedString(int length) {
+ if (length == 0) {
+ return "";
+ }
+ int stringLength = length;
+ int lastIndex = position + length - 1;
+ if (lastIndex < limit && data[lastIndex] == 0) {
+ stringLength--;
+ }
+ String result = Util.fromUtf8Bytes(data, position, stringLength);
+ position += length;
+ return result;
+ }
+
+ /**
+ * Reads up to the next NUL byte (or the limit) as UTF-8 characters.
+ *
+ * @return The string not including any terminating NUL byte, or null if the end of the data has
+ * already been reached.
+ */
+ @Nullable
+ public String readNullTerminatedString() {
+ if (bytesLeft() == 0) {
+ return null;
+ }
+ int stringLimit = position;
+ while (stringLimit < limit && data[stringLimit] != 0) {
+ stringLimit++;
+ }
+ String string = Util.fromUtf8Bytes(data, position, stringLimit - position);
+ position = stringLimit;
+ if (position < limit) {
+ position++;
+ }
+ return string;
+ }
+
+ /**
+ * Reads a line of text.
+ *
+ * <p>A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
+ * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default
+ * charset (UTF-8) is used. This method discards leading UTF-8 byte order marks, if present.
+ *
+ * @return The line not including any line-termination characters, or null if the end of the data
+ * has already been reached.
+ */
+ @Nullable
+ public String readLine() {
+ if (bytesLeft() == 0) {
+ return null;
+ }
+ int lineLimit = position;
+ while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {
+ lineLimit++;
+ }
+ if (lineLimit - position >= 3 && data[position] == (byte) 0xEF
+ && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) {
+ // There's a UTF-8 byte order mark at the start of the line. Discard it.
+ position += 3;
+ }
+ String line = Util.fromUtf8Bytes(data, position, lineLimit - position);
+ position = lineLimit;
+ if (position == limit) {
+ return line;
+ }
+ if (data[position] == '\r') {
+ position++;
+ if (position == limit) {
+ return line;
+ }
+ }
+ if (data[position] == '\n') {
+ position++;
+ }
+ return line;
+ }
+
+ /**
+ * Reads a long value encoded by UTF-8 encoding
+ *
+ * @throws NumberFormatException if there is a problem with decoding
+ * @return Decoded long value
+ */
+ public long readUtf8EncodedLong() {
+ int length = 0;
+ long value = data[position];
+ // find the high most 0 bit
+ for (int j = 7; j >= 0; j--) {
+ if ((value & (1 << j)) == 0) {
+ if (j < 6) {
+ value &= (1 << j) - 1;
+ length = 7 - j;
+ } else if (j == 7) {
+ length = 1;
+ }
+ break;
+ }
+ }
+ if (length == 0) {
+ throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value);
+ }
+ for (int i = 1; i < length; i++) {
+ int x = data[position + i];
+ if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th
+ throw new NumberFormatException("Invalid UTF-8 sequence continuation byte: " + value);
+ }
+ value = (value << 6) | (x & 0x3F);
+ }
+ position += length;
+ return value;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java
new file mode 100644
index 0000000000..e73404fd91
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+/**
+ * Wraps a byte array, providing methods that allow it to be read as a NAL unit bitstream.
+ * <p>
+ * Whenever the byte sequence [0, 0, 3] appears in the wrapped byte array, it is treated as [0, 0]
+ * for all reading/skipping operations, which makes the bitstream appear to be unescaped.
+ */
+public final class ParsableNalUnitBitArray {
+
+ private byte[] data;
+ private int byteLimit;
+
+ // The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3].
+ private int byteOffset;
+ private int bitOffset;
+
+ /**
+ * @param data The data to wrap.
+ * @param offset The byte offset in {@code data} to start reading from.
+ * @param limit The byte offset of the end of the bitstream in {@code data}.
+ */
+ @SuppressWarnings({"initialization.fields.uninitialized", "method.invocation.invalid"})
+ public ParsableNalUnitBitArray(byte[] data, int offset, int limit) {
+ reset(data, offset, limit);
+ }
+
+ /**
+ * Resets the wrapped data, limit and offset.
+ *
+ * @param data The data to wrap.
+ * @param offset The byte offset in {@code data} to start reading from.
+ * @param limit The byte offset of the end of the bitstream in {@code data}.
+ */
+ public void reset(byte[] data, int offset, int limit) {
+ this.data = data;
+ byteOffset = offset;
+ byteLimit = limit;
+ bitOffset = 0;
+ assertValidOffset();
+ }
+
+ /**
+ * Skips a single bit.
+ */
+ public void skipBit() {
+ if (++bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Skips bits and moves current reading position forward.
+ *
+ * @param numBits The number of bits to skip.
+ */
+ public void skipBits(int numBits) {
+ int oldByteOffset = byteOffset;
+ int numBytes = numBits / 8;
+ byteOffset += numBytes;
+ bitOffset += numBits - (numBytes * 8);
+ if (bitOffset > 7) {
+ byteOffset++;
+ bitOffset -= 8;
+ }
+ for (int i = oldByteOffset + 1; i <= byteOffset; i++) {
+ if (shouldSkipByte(i)) {
+ // Skip the byte and move forward to check three bytes ahead.
+ byteOffset++;
+ i += 2;
+ }
+ }
+ assertValidOffset();
+ }
+
+ /**
+ * Returns whether it's possible to read {@code n} bits starting from the current offset. The
+ * offset is not modified.
+ *
+ * @param numBits The number of bits.
+ * @return Whether it is possible to read {@code n} bits.
+ */
+ public boolean canReadBits(int numBits) {
+ int oldByteOffset = byteOffset;
+ int numBytes = numBits / 8;
+ int newByteOffset = byteOffset + numBytes;
+ int newBitOffset = bitOffset + numBits - (numBytes * 8);
+ if (newBitOffset > 7) {
+ newByteOffset++;
+ newBitOffset -= 8;
+ }
+ for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset < byteLimit; i++) {
+ if (shouldSkipByte(i)) {
+ // Skip the byte and move forward to check three bytes ahead.
+ newByteOffset++;
+ i += 2;
+ }
+ }
+ return newByteOffset < byteLimit || (newByteOffset == byteLimit && newBitOffset == 0);
+ }
+
+ /**
+ * Reads a single bit.
+ *
+ * @return Whether the bit is set.
+ */
+ public boolean readBit() {
+ boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0;
+ skipBit();
+ return returnValue;
+ }
+
+ /**
+ * Reads up to 32 bits.
+ *
+ * @param numBits The number of bits to read.
+ * @return An integer whose bottom n bits hold the read data.
+ */
+ public int readBits(int numBits) {
+ int returnValue = 0;
+ bitOffset += numBits;
+ while (bitOffset > 8) {
+ bitOffset -= 8;
+ returnValue |= (data[byteOffset] & 0xFF) << bitOffset;
+ byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
+ }
+ returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
+ returnValue &= 0xFFFFFFFF >>> (32 - numBits);
+ if (bitOffset == 8) {
+ bitOffset = 0;
+ byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
+ }
+ assertValidOffset();
+ return returnValue;
+ }
+
+ /**
+ * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current
+ * offset. The offset is not modified.
+ *
+ * @return Whether it is possible to read an Exp-Golomb-coded integer.
+ */
+ public boolean canReadExpGolombCodedNum() {
+ int initialByteOffset = byteOffset;
+ int initialBitOffset = bitOffset;
+ int leadingZeros = 0;
+ while (byteOffset < byteLimit && !readBit()) {
+ leadingZeros++;
+ }
+ boolean hitLimit = byteOffset == byteLimit;
+ byteOffset = initialByteOffset;
+ bitOffset = initialBitOffset;
+ return !hitLimit && canReadBits(leadingZeros * 2 + 1);
+ }
+
+ /**
+ * Reads an unsigned Exp-Golomb-coded format integer.
+ *
+ * @return The value of the parsed Exp-Golomb-coded integer.
+ */
+ public int readUnsignedExpGolombCodedInt() {
+ return readExpGolombCodeNum();
+ }
+
+ /**
+ * Reads an signed Exp-Golomb-coded format integer.
+ *
+ * @return The value of the parsed Exp-Golomb-coded integer.
+ */
+ public int readSignedExpGolombCodedInt() {
+ int codeNum = readExpGolombCodeNum();
+ return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
+ }
+
+ private int readExpGolombCodeNum() {
+ int leadingZeros = 0;
+ while (!readBit()) {
+ leadingZeros++;
+ }
+ return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
+ }
+
+ private boolean shouldSkipByte(int offset) {
+ return 2 <= offset && offset < byteLimit && data[offset] == (byte) 0x03
+ && data[offset - 2] == (byte) 0x00 && data[offset - 1] == (byte) 0x00;
+ }
+
+ private void assertValidOffset() {
+ // It is fine for position to be at the end of the array, but no further.
+ Assertions.checkState(byteOffset >= 0
+ && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java
new file mode 100644
index 0000000000..d91d9f7254
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Predicate.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+/**
+ * Determines a true or false value for a given input.
+ *
+ * @param <T> The input type of the predicate.
+ */
+public interface Predicate<T> {
+
+ /**
+ * Evaluates an input.
+ *
+ * @param input The input to evaluate.
+ * @return The evaluated result.
+ */
+ boolean evaluate(T input);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java
new file mode 100644
index 0000000000..1067014b40
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.PriorityQueue;
+
+/**
+ * Allows tasks with associated priorities to control how they proceed relative to one another.
+ * <p>
+ * A task should call {@link #add(int)} to register with the manager and {@link #remove(int)} to
+ * unregister. A registered task will prevent tasks of lower priority from proceeding, and should
+ * call {@link #proceed(int)}, {@link #proceedNonBlocking(int)} or {@link #proceedOrThrow(int)} each
+ * time it wishes to check whether it is itself allowed to proceed.
+ */
+public final class PriorityTaskManager {
+
+ /**
+ * Thrown when task attempts to proceed when another registered task has a higher priority.
+ */
+ public static class PriorityTooLowException extends IOException {
+
+ public PriorityTooLowException(int priority, int highestPriority) {
+ super("Priority too low [priority=" + priority + ", highest=" + highestPriority + "]");
+ }
+
+ }
+
+ private final Object lock = new Object();
+
+ // Guarded by lock.
+ private final PriorityQueue<Integer> queue;
+ private int highestPriority;
+
+ public PriorityTaskManager() {
+ queue = new PriorityQueue<>(10, Collections.reverseOrder());
+ highestPriority = Integer.MIN_VALUE;
+ }
+
+ /**
+ * Register a new task. The task must call {@link #remove(int)} when done.
+ *
+ * @param priority The priority of the task. Larger values indicate higher priorities.
+ */
+ public void add(int priority) {
+ synchronized (lock) {
+ queue.add(priority);
+ highestPriority = Math.max(highestPriority, priority);
+ }
+ }
+
+ /**
+ * Blocks until the task is allowed to proceed.
+ *
+ * @param priority The priority of the task.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public void proceed(int priority) throws InterruptedException {
+ synchronized (lock) {
+ while (highestPriority != priority) {
+ lock.wait();
+ }
+ }
+ }
+
+ /**
+ * A non-blocking variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task.
+ * @return Whether the task is allowed to proceed.
+ */
+ public boolean proceedNonBlocking(int priority) {
+ synchronized (lock) {
+ return highestPriority == priority;
+ }
+ }
+
+ /**
+ * A throwing variant of {@link #proceed(int)}.
+ *
+ * @param priority The priority of the task.
+ * @throws PriorityTooLowException If the task is not allowed to proceed.
+ */
+ public void proceedOrThrow(int priority) throws PriorityTooLowException {
+ synchronized (lock) {
+ if (highestPriority != priority) {
+ throw new PriorityTooLowException(priority, highestPriority);
+ }
+ }
+ }
+
+ /**
+ * Unregister a task.
+ *
+ * @param priority The priority of the task.
+ */
+ public void remove(int priority) {
+ synchronized (lock) {
+ queue.remove(priority);
+ highestPriority = queue.isEmpty() ? Integer.MIN_VALUE : Util.castNonNull(queue.peek());
+ lock.notifyAll();
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java
new file mode 100644
index 0000000000..c4964e6848
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/RepeatModeUtil.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Util class for repeat mode handling.
+ */
+public final class RepeatModeUtil {
+
+ // LINT.IfChange
+ /**
+ * Set of repeat toggle modes. Can be combined using bit-wise operations. Possible flag values are
+ * {@link #REPEAT_TOGGLE_MODE_NONE}, {@link #REPEAT_TOGGLE_MODE_ONE} and {@link
+ * #REPEAT_TOGGLE_MODE_ALL}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE, REPEAT_TOGGLE_MODE_ALL})
+ public @interface RepeatToggleModes {}
+ /**
+ * All repeat mode buttons disabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_NONE = 0;
+ /**
+ * "Repeat One" button enabled.
+ */
+ public static final int REPEAT_TOGGLE_MODE_ONE = 1;
+ /** "Repeat All" button enabled. */
+ public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2
+ // LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml)
+
+ private RepeatModeUtil() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Gets the next repeat mode out of {@code enabledModes} starting from {@code currentMode}.
+ *
+ * @param currentMode The current repeat mode.
+ * @param enabledModes Bitmask of enabled modes.
+ * @return The next repeat mode.
+ */
+ public static @Player.RepeatMode int getNextRepeatMode(@Player.RepeatMode int currentMode,
+ int enabledModes) {
+ for (int offset = 1; offset <= 2; offset++) {
+ @Player.RepeatMode int proposedMode = (currentMode + offset) % 3;
+ if (isRepeatModeEnabled(proposedMode, enabledModes)) {
+ return proposedMode;
+ }
+ }
+ return currentMode;
+ }
+
+ /**
+ * Verifies whether a given {@code repeatMode} is enabled in the bitmask {@code enabledModes}.
+ *
+ * @param repeatMode The mode to check.
+ * @param enabledModes The bitmask representing the enabled modes.
+ * @return {@code true} if enabled.
+ */
+ public static boolean isRepeatModeEnabled(@Player.RepeatMode int repeatMode, int enabledModes) {
+ switch (repeatMode) {
+ case Player.REPEAT_MODE_OFF:
+ return true;
+ case Player.REPEAT_MODE_ONE:
+ return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0;
+ case Player.REPEAT_MODE_ALL:
+ return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0;
+ default:
+ return false;
+ }
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
new file mode 100644
index 0000000000..cd38892be0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This is a subclass of {@link BufferedOutputStream} with a {@link #reset(OutputStream)} method
+ * that allows an instance to be re-used with another underlying output stream.
+ */
+public final class ReusableBufferedOutputStream extends BufferedOutputStream {
+
+ private boolean closed;
+
+ public ReusableBufferedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public ReusableBufferedOutputStream(OutputStream out, int size) {
+ super(out, size);
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+
+ Throwable thrown = null;
+ try {
+ flush();
+ } catch (Throwable e) {
+ thrown = e;
+ }
+ try {
+ out.close();
+ } catch (Throwable e) {
+ if (thrown == null) {
+ thrown = e;
+ }
+ }
+ if (thrown != null) {
+ Util.sneakyThrow(thrown);
+ }
+ }
+
+ /**
+ * Resets this stream and uses the given output stream for writing. This stream must be closed
+ * before resetting.
+ *
+ * @param out New output stream to be used for writing.
+ * @throws IllegalStateException If the stream isn't closed.
+ */
+ public void reset(OutputStream out) {
+ Assertions.checkState(closed);
+ this.out = out;
+ count = 0;
+ closed = false;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java
new file mode 100644
index 0000000000..9048de2f34
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Calculate any percentile over a sliding window of weighted values. A maximum weight is
+ * configured. Once the total weight of the values reaches the maximum weight, the oldest value is
+ * reduced in weight until it reaches zero and is removed. This maintains a constant total weight,
+ * equal to the maximum allowed, at the steady state.
+ * <p>
+ * This class can be used for bandwidth estimation based on a sliding window of past transfer rate
+ * observations. This is an alternative to sliding mean and exponential averaging which suffer from
+ * susceptibility to outliers and slow adaptation to step functions.
+ *
+ * @see <a href="http://en.wikipedia.org/wiki/Moving_average">Wiki: Moving average</a>
+ * @see <a href="http://en.wikipedia.org/wiki/Selection_algorithm">Wiki: Selection algorithm</a>
+ */
+public class SlidingPercentile {
+
+ // Orderings.
+ private static final Comparator<Sample> INDEX_COMPARATOR = (a, b) -> a.index - b.index;
+ private static final Comparator<Sample> VALUE_COMPARATOR =
+ (a, b) -> Float.compare(a.value, b.value);
+
+ private static final int SORT_ORDER_NONE = -1;
+ private static final int SORT_ORDER_BY_VALUE = 0;
+ private static final int SORT_ORDER_BY_INDEX = 1;
+
+ private static final int MAX_RECYCLED_SAMPLES = 5;
+
+ private final int maxWeight;
+ private final ArrayList<Sample> samples;
+
+ private final Sample[] recycledSamples;
+
+ private int currentSortOrder;
+ private int nextSampleIndex;
+ private int totalWeight;
+ private int recycledSampleCount;
+
+ /**
+ * @param maxWeight The maximum weight.
+ */
+ public SlidingPercentile(int maxWeight) {
+ this.maxWeight = maxWeight;
+ recycledSamples = new Sample[MAX_RECYCLED_SAMPLES];
+ samples = new ArrayList<>();
+ currentSortOrder = SORT_ORDER_NONE;
+ }
+
+ /** Resets the sliding percentile. */
+ public void reset() {
+ samples.clear();
+ currentSortOrder = SORT_ORDER_NONE;
+ nextSampleIndex = 0;
+ totalWeight = 0;
+ }
+
+ /**
+ * Adds a new weighted value.
+ *
+ * @param weight The weight of the new observation.
+ * @param value The value of the new observation.
+ */
+ public void addSample(int weight, float value) {
+ ensureSortedByIndex();
+
+ Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
+ : new Sample();
+ newSample.index = nextSampleIndex++;
+ newSample.weight = weight;
+ newSample.value = value;
+ samples.add(newSample);
+ totalWeight += weight;
+
+ while (totalWeight > maxWeight) {
+ int excessWeight = totalWeight - maxWeight;
+ Sample oldestSample = samples.get(0);
+ if (oldestSample.weight <= excessWeight) {
+ totalWeight -= oldestSample.weight;
+ samples.remove(0);
+ if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {
+ recycledSamples[recycledSampleCount++] = oldestSample;
+ }
+ } else {
+ oldestSample.weight -= excessWeight;
+ totalWeight -= excessWeight;
+ }
+ }
+ }
+
+ /**
+ * Computes a percentile by integration.
+ *
+ * @param percentile The desired percentile, expressed as a fraction in the range (0,1].
+ * @return The requested percentile value or {@link Float#NaN} if no samples have been added.
+ */
+ public float getPercentile(float percentile) {
+ ensureSortedByValue();
+ float desiredWeight = percentile * totalWeight;
+ int accumulatedWeight = 0;
+ for (int i = 0; i < samples.size(); i++) {
+ Sample currentSample = samples.get(i);
+ accumulatedWeight += currentSample.weight;
+ if (accumulatedWeight >= desiredWeight) {
+ return currentSample.value;
+ }
+ }
+ // Clamp to maximum value or NaN if no values.
+ return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value;
+ }
+
+ /**
+ * Sorts the samples by index.
+ */
+ private void ensureSortedByIndex() {
+ if (currentSortOrder != SORT_ORDER_BY_INDEX) {
+ Collections.sort(samples, INDEX_COMPARATOR);
+ currentSortOrder = SORT_ORDER_BY_INDEX;
+ }
+ }
+
+ /**
+ * Sorts the samples by value.
+ */
+ private void ensureSortedByValue() {
+ if (currentSortOrder != SORT_ORDER_BY_VALUE) {
+ Collections.sort(samples, VALUE_COMPARATOR);
+ currentSortOrder = SORT_ORDER_BY_VALUE;
+ }
+ }
+
+ private static class Sample {
+
+ public int index;
+ public int weight;
+ public float value;
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java
new file mode 100644
index 0000000000..f72867694d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+
+/**
+ * A {@link MediaClock} whose position advances with real time based on the playback parameters when
+ * started.
+ */
+public final class StandaloneMediaClock implements MediaClock {
+
+ private final Clock clock;
+
+ private boolean started;
+ private long baseUs;
+ private long baseElapsedMs;
+ private PlaybackParameters playbackParameters;
+
+ /**
+ * Creates a new standalone media clock using the given {@link Clock} implementation.
+ *
+ * @param clock A {@link Clock}.
+ */
+ public StandaloneMediaClock(Clock clock) {
+ this.clock = clock;
+ this.playbackParameters = PlaybackParameters.DEFAULT;
+ }
+
+ /**
+ * Starts the clock. Does nothing if the clock is already started.
+ */
+ public void start() {
+ if (!started) {
+ baseElapsedMs = clock.elapsedRealtime();
+ started = true;
+ }
+ }
+
+ /**
+ * Stops the clock. Does nothing if the clock is already stopped.
+ */
+ public void stop() {
+ if (started) {
+ resetPosition(getPositionUs());
+ started = false;
+ }
+ }
+
+ /**
+ * Resets the clock's position.
+ *
+ * @param positionUs The position to set in microseconds.
+ */
+ public void resetPosition(long positionUs) {
+ baseUs = positionUs;
+ if (started) {
+ baseElapsedMs = clock.elapsedRealtime();
+ }
+ }
+
+ @Override
+ public long getPositionUs() {
+ long positionUs = baseUs;
+ if (started) {
+ long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs;
+ if (playbackParameters.speed == 1f) {
+ positionUs += C.msToUs(elapsedSinceBaseMs);
+ } else {
+ positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs);
+ }
+ }
+ return positionUs;
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ // Store the current position as the new base, in case the playback speed has changed.
+ if (started) {
+ resetPosition(getPositionUs());
+ }
+ this.playbackParameters = playbackParameters;
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java
new file mode 100644
index 0000000000..a2f915866d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemClock.java
@@ -0,0 +1,47 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import androidx.annotation.Nullable;
+
+/**
+ * The standard implementation of {@link Clock}.
+ */
+/* package */ final class SystemClock implements Clock {
+
+ @Override
+ public long elapsedRealtime() {
+ return android.os.SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public long uptimeMillis() {
+ return android.os.SystemClock.uptimeMillis();
+ }
+
+ @Override
+ public void sleep(long sleepTimeMs) {
+ android.os.SystemClock.sleep(sleepTimeMs);
+ }
+
+ @Override
+ public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) {
+ return new SystemHandlerWrapper(new Handler(looper, callback));
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
new file mode 100644
index 0000000000..e69a24cc10
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.os.Looper;
+import android.os.Message;
+import androidx.annotation.Nullable;
+
+/** The standard implementation of {@link HandlerWrapper}. */
+/* package */ final class SystemHandlerWrapper implements HandlerWrapper {
+
+ private final android.os.Handler handler;
+
+ public SystemHandlerWrapper(android.os.Handler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public Looper getLooper() {
+ return handler.getLooper();
+ }
+
+ @Override
+ public Message obtainMessage(int what) {
+ return handler.obtainMessage(what);
+ }
+
+ @Override
+ public Message obtainMessage(int what, @Nullable Object obj) {
+ return handler.obtainMessage(what, obj);
+ }
+
+ @Override
+ public Message obtainMessage(int what, int arg1, int arg2) {
+ return handler.obtainMessage(what, arg1, arg2);
+ }
+
+ @Override
+ public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) {
+ return handler.obtainMessage(what, arg1, arg2, obj);
+ }
+
+ @Override
+ public boolean sendEmptyMessage(int what) {
+ return handler.sendEmptyMessage(what);
+ }
+
+ @Override
+ public boolean sendEmptyMessageAtTime(int what, long uptimeMs) {
+ return handler.sendEmptyMessageAtTime(what, uptimeMs);
+ }
+
+ @Override
+ public void removeMessages(int what) {
+ handler.removeMessages(what);
+ }
+
+ @Override
+ public void removeCallbacksAndMessages(@Nullable Object token) {
+ handler.removeCallbacksAndMessages(token);
+ }
+
+ @Override
+ public boolean post(Runnable runnable) {
+ return handler.post(runnable);
+ }
+
+ @Override
+ public boolean postDelayed(Runnable runnable, long delayMs) {
+ return handler.postDelayed(runnable, delayMs);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java
new file mode 100644
index 0000000000..396e50dcff
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimedValueQueue.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import androidx.annotation.Nullable;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** A utility class to keep a queue of values with timestamps. This class is thread safe. */
+public final class TimedValueQueue<V> {
+ private static final int INITIAL_BUFFER_SIZE = 10;
+
+ // Looping buffer for timestamps and values
+ private long[] timestamps;
+ private @NullableType V[] values;
+ private int first;
+ private int size;
+
+ public TimedValueQueue() {
+ this(INITIAL_BUFFER_SIZE);
+ }
+
+ /** Creates a TimedValueBuffer with the given initial buffer size. */
+ public TimedValueQueue(int initialBufferSize) {
+ timestamps = new long[initialBufferSize];
+ values = newArray(initialBufferSize);
+ }
+
+ /**
+ * Associates the specified value with the specified timestamp. All new values should have a
+ * greater timestamp than the previously added values. Otherwise all values are removed before
+ * adding the new one.
+ */
+ public synchronized void add(long timestamp, V value) {
+ clearBufferOnTimeDiscontinuity(timestamp);
+ doubleCapacityIfFull();
+ addUnchecked(timestamp, value);
+ }
+
+ /** Removes all of the values. */
+ public synchronized void clear() {
+ first = 0;
+ size = 0;
+ Arrays.fill(values, null);
+ }
+
+ /** Returns number of the values buffered. */
+ public synchronized int size() {
+ return size;
+ }
+
+ /**
+ * Returns the value with the greatest timestamp which is less than or equal to the given
+ * timestamp. Removes all older values and the returned one from the buffer.
+ *
+ * @param timestamp The timestamp value.
+ * @return The value with the greatest timestamp which is less than or equal to the given
+ * timestamp or null if there is no such value.
+ * @see #poll(long)
+ */
+ public synchronized @Nullable V pollFloor(long timestamp) {
+ return poll(timestamp, /* onlyOlder= */ true);
+ }
+
+ /**
+ * Returns the value with the closest timestamp to the given timestamp. Removes all older values
+ * including the returned one from the buffer.
+ *
+ * @param timestamp The timestamp value.
+ * @return The value with the closest timestamp or null if the buffer is empty.
+ * @see #pollFloor(long)
+ */
+ public synchronized @Nullable V poll(long timestamp) {
+ return poll(timestamp, /* onlyOlder= */ false);
+ }
+
+ /**
+ * Returns the value with the closest timestamp to the given timestamp. Removes all older values
+ * including the returned one from the buffer.
+ *
+ * @param timestamp The timestamp value.
+ * @param onlyOlder Whether this method can return a new value in case its timestamp value is
+ * closest to {@code timestamp}.
+ * @return The value with the closest timestamp or null if the buffer is empty or there is no
+ * older value and {@code onlyOlder} is true.
+ */
+ @Nullable
+ private V poll(long timestamp, boolean onlyOlder) {
+ V value = null;
+ long previousTimeDiff = Long.MAX_VALUE;
+ while (size > 0) {
+ long timeDiff = timestamp - timestamps[first];
+ if (timeDiff < 0 && (onlyOlder || -timeDiff >= previousTimeDiff)) {
+ break;
+ }
+ previousTimeDiff = timeDiff;
+ value = values[first];
+ values[first] = null;
+ first = (first + 1) % values.length;
+ size--;
+ }
+ return value;
+ }
+
+ private void clearBufferOnTimeDiscontinuity(long timestamp) {
+ if (size > 0) {
+ int last = (first + size - 1) % values.length;
+ if (timestamp <= timestamps[last]) {
+ clear();
+ }
+ }
+ }
+
+ private void doubleCapacityIfFull() {
+ int capacity = values.length;
+ if (size < capacity) {
+ return;
+ }
+ int newCapacity = capacity * 2;
+ long[] newTimestamps = new long[newCapacity];
+ V[] newValues = newArray(newCapacity);
+ // Reset the loop starting index to 0 while coping to the new buffer.
+ // First copy the values from 'first' index to the end of original array.
+ int length = capacity - first;
+ System.arraycopy(timestamps, first, newTimestamps, 0, length);
+ System.arraycopy(values, first, newValues, 0, length);
+ // Then the values from index 0 to 'first' index.
+ if (first > 0) {
+ System.arraycopy(timestamps, 0, newTimestamps, length, first);
+ System.arraycopy(values, 0, newValues, length, first);
+ }
+ timestamps = newTimestamps;
+ values = newValues;
+ first = 0;
+ }
+
+ private void addUnchecked(long timestamp, V value) {
+ int next = (first + size) % values.length;
+ timestamps[next] = timestamp;
+ values[next] = value;
+ size++;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <V> V[] newArray(int length) {
+ return (V[]) new Object[length];
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java
new file mode 100644
index 0000000000..e824251282
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+
+/**
+ * Offsets timestamps according to an initial sample timestamp offset. MPEG-2 TS timestamps scaling
+ * and adjustment is supported, taking into account timestamp rollover.
+ */
+public final class TimestampAdjuster {
+
+ /**
+ * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should
+ * not be offset.
+ */
+ public static final long DO_NOT_OFFSET = Long.MAX_VALUE;
+
+ /**
+ * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock
+ * presentation timestamp.
+ */
+ private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
+
+ private long firstSampleTimestampUs;
+ private long timestampOffsetUs;
+
+ // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
+ private volatile long lastSampleTimestampUs;
+
+ /**
+ * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.
+ */
+ public TimestampAdjuster(long firstSampleTimestampUs) {
+ lastSampleTimestampUs = C.TIME_UNSET;
+ setFirstSampleTimestampUs(firstSampleTimestampUs);
+ }
+
+ /**
+ * Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be
+ * called before any timestamps have been adjusted.
+ *
+ * @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or
+ * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
+ */
+ public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
+ Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
+ this.firstSampleTimestampUs = firstSampleTimestampUs;
+ }
+
+ /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
+ public long getFirstSampleTimestampUs() {
+ return firstSampleTimestampUs;
+ }
+
+ /**
+ * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link
+ * #adjustSampleTimestamp} has not been called, returns the result of calling {@link
+ * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
+ * C#TIME_UNSET}.
+ */
+ public long getLastAdjustedTimestampUs() {
+ return lastSampleTimestampUs != C.TIME_UNSET
+ ? (lastSampleTimestampUs + timestampOffsetUs)
+ : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
+ }
+
+ /**
+ * Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
+ * If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
+ * adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
+ *
+ * @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
+ * {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
+ * be offset.
+ */
+ public long getTimestampOffsetUs() {
+ return firstSampleTimestampUs == DO_NOT_OFFSET
+ ? 0
+ : lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
+ }
+
+ /**
+ * Resets the instance to its initial state.
+ */
+ public void reset() {
+ lastSampleTimestampUs = C.TIME_UNSET;
+ }
+
+ /**
+ * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.
+ *
+ * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
+ * @return The adjusted timestamp in microseconds.
+ */
+ public long adjustTsTimestamp(long pts90Khz) {
+ if (pts90Khz == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ if (lastSampleTimestampUs != C.TIME_UNSET) {
+ // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
+ // and we need to snap to the one closest to lastSampleTimestampUs.
+ long lastPts = usToPts(lastSampleTimestampUs);
+ long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
+ long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
+ long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount);
+ pts90Khz =
+ Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
+ ? ptsWrapBelow
+ : ptsWrapAbove;
+ }
+ return adjustSampleTimestamp(ptsToUs(pts90Khz));
+ }
+
+ /**
+ * Offsets a timestamp in microseconds.
+ *
+ * @param timeUs The timestamp to adjust in microseconds.
+ * @return The adjusted timestamp in microseconds.
+ */
+ public long adjustSampleTimestamp(long timeUs) {
+ if (timeUs == C.TIME_UNSET) {
+ return C.TIME_UNSET;
+ }
+ // Record the adjusted PTS to adjust for wraparound next time.
+ if (lastSampleTimestampUs != C.TIME_UNSET) {
+ lastSampleTimestampUs = timeUs;
+ } else {
+ if (firstSampleTimestampUs != DO_NOT_OFFSET) {
+ // Calculate the timestamp offset.
+ timestampOffsetUs = firstSampleTimestampUs - timeUs;
+ }
+ synchronized (this) {
+ lastSampleTimestampUs = timeUs;
+ // Notify threads waiting for this adjuster to be initialized.
+ notifyAll();
+ }
+ }
+ return timeUs + timestampOffsetUs;
+ }
+
+ /**
+ * Blocks the calling thread until this adjuster is initialized.
+ *
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ public synchronized void waitUntilInitialized() throws InterruptedException {
+ while (lastSampleTimestampUs == C.TIME_UNSET) {
+ wait();
+ }
+ }
+
+ /**
+ * Converts a 90 kHz clock timestamp to a timestamp in microseconds.
+ *
+ * @param pts A 90 kHz clock timestamp.
+ * @return The corresponding value in microseconds.
+ */
+ public static long ptsToUs(long pts) {
+ return (pts * C.MICROS_PER_SECOND) / 90000;
+ }
+
+ /**
+ * Converts a timestamp in microseconds to a 90 kHz clock timestamp.
+ *
+ * @param us A value in microseconds.
+ * @return The corresponding value as a 90 kHz clock timestamp.
+ */
+ public static long usToPts(long us) {
+ return (us * 90000) / C.MICROS_PER_SECOND;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java
new file mode 100644
index 0000000000..5f53c3130d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.annotation.TargetApi;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+
+/**
+ * Calls through to {@link android.os.Trace} methods on supported API levels.
+ */
+public final class TraceUtil {
+
+ private TraceUtil() {}
+
+ /**
+ * Writes a trace message to indicate that a given section of code has begun.
+ *
+ * @see android.os.Trace#beginSection(String)
+ * @param sectionName The name of the code section to appear in the trace. This may be at most 127
+ * Unicode code units long.
+ */
+ public static void beginSection(String sectionName) {
+ if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {
+ beginSectionV18(sectionName);
+ }
+ }
+
+ /**
+ * Writes a trace message to indicate that a given section of code has ended.
+ *
+ * @see android.os.Trace#endSection()
+ */
+ public static void endSection() {
+ if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) {
+ endSectionV18();
+ }
+ }
+
+ @TargetApi(18)
+ private static void beginSectionV18(String sectionName) {
+ android.os.Trace.beginSection(sectionName);
+ }
+
+ @TargetApi(18)
+ private static void endSectionV18() {
+ android.os.Trace.endSection();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java
new file mode 100644
index 0000000000..03b5d26a51
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/UriUtil.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+
+/**
+ * Utility methods for manipulating URIs.
+ */
+public final class UriUtil {
+
+ /**
+ * The length of arrays returned by {@link #getUriIndices(String)}.
+ */
+ private static final int INDEX_COUNT = 4;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if
+ * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
+ * including when the URI has no scheme.
+ */
+ private static final int SCHEME_COLON = 0;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the path part. Equals (schemeColon + 1)
+ * if no authority part, (schemeColon + 3) if the authority part consists of just "//", and
+ * (query) if no path part. The characters starting at this index can be "//" only if the
+ * authority part is non-empty (in this case the double-slash means the first segment is empty).
+ */
+ private static final int PATH = 1;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the query part, including the '?'
+ * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a
+ * single '?' with no data.
+ */
+ private static final int QUERY = 2;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ * <p>
+ * The value at this position in the array is the index of the fragment part, including the '#'
+ * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if
+ * the fragment part is a single '#' with no data.
+ */
+ private static final int FRAGMENT = 3;
+
+ private UriUtil() {}
+
+ /**
+ * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ public static Uri resolveToUri(@Nullable String baseUri, @Nullable String referenceUri) {
+ return Uri.parse(resolve(baseUri, referenceUri));
+ }
+
+ /**
+ * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}.
+ *
+ * <p>The resolution is performed as specified by RFC-3986.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ public static String resolve(@Nullable String baseUri, @Nullable String referenceUri) {
+ StringBuilder uri = new StringBuilder();
+
+ // Map null onto empty string, to make the following logic simpler.
+ baseUri = baseUri == null ? "" : baseUri;
+ referenceUri = referenceUri == null ? "" : referenceUri;
+
+ int[] refIndices = getUriIndices(referenceUri);
+ if (refIndices[SCHEME_COLON] != -1) {
+ // The reference is absolute. The target Uri is the reference.
+ uri.append(referenceUri);
+ removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]);
+ return uri.toString();
+ }
+
+ int[] baseIndices = getUriIndices(baseUri);
+ if (refIndices[FRAGMENT] == 0) {
+ // The reference is empty or contains just the fragment part, then the target Uri is the
+ // concatenation of the base Uri without its fragment, and the reference.
+ return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString();
+ }
+
+ if (refIndices[QUERY] == 0) {
+ // The reference starts with the query part. The target is the base up to (but excluding) the
+ // query, plus the reference.
+ return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString();
+ }
+
+ if (refIndices[PATH] != 0) {
+ // The reference has authority. The target is the base scheme plus the reference.
+ int baseLimit = baseIndices[SCHEME_COLON] + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+
+ if (referenceUri.charAt(refIndices[PATH]) == '/') {
+ // The reference path is rooted. The target is the base scheme and authority (if any), plus
+ // the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]);
+ }
+
+ // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
+ // and the reference. This can be split into 2 cases:
+ if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]
+ && baseIndices[PATH] == baseIndices[QUERY]) {
+ // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is
+ // needed after the authority, before appending the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1);
+ } else {
+ // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after
+ // it. If base hier-part has no '/', it could only mean that it is completely empty or
+ // contains only one segment, in which case the whole hier-part is excluded and the reference
+ // is appended right after the base scheme colon without an added '/'.
+ int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1);
+ int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+ }
+
+ /**
+ * Removes query parameter from an Uri, if present.
+ *
+ * @param uri The uri.
+ * @param queryParameterName The name of the query parameter.
+ * @return The uri without the query parameter.
+ */
+ public static Uri removeQueryParameter(Uri uri, String queryParameterName) {
+ Uri.Builder builder = uri.buildUpon();
+ builder.clearQuery();
+ for (String key : uri.getQueryParameterNames()) {
+ if (!key.equals(queryParameterName)) {
+ for (String value : uri.getQueryParameters(key)) {
+ builder.appendQueryParameter(key, value);
+ }
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Removes dot segments from the path of a URI.
+ *
+ * @param uri A {@link StringBuilder} containing the URI.
+ * @param offset The index of the start of the path in {@code uri}.
+ * @param limit The limit (exclusive) of the path in {@code uri}.
+ */
+ private static String removeDotSegments(StringBuilder uri, int offset, int limit) {
+ if (offset >= limit) {
+ // Nothing to do.
+ return uri.toString();
+ }
+ if (uri.charAt(offset) == '/') {
+ // If the path starts with a /, always retain it.
+ offset++;
+ }
+ // The first character of the current path segment.
+ int segmentStart = offset;
+ int i = offset;
+ while (i <= limit) {
+ int nextSegmentStart;
+ if (i == limit) {
+ nextSegmentStart = i;
+ } else if (uri.charAt(i) == '/') {
+ nextSegmentStart = i + 1;
+ } else {
+ i++;
+ continue;
+ }
+ // We've encountered the end of a segment or the end of the path. If the final segment was
+ // "." or "..", remove the appropriate segments of the path.
+ if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') {
+ // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
+ uri.delete(segmentStart, nextSegmentStart);
+ limit -= nextSegmentStart - segmentStart;
+ i = segmentStart;
+ } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.'
+ && uri.charAt(segmentStart + 1) == '.') {
+ // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
+ int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1;
+ int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset;
+ uri.delete(removeFrom, nextSegmentStart);
+ limit -= nextSegmentStart - removeFrom;
+ segmentStart = prevSegmentStart;
+ i = prevSegmentStart;
+ } else {
+ i++;
+ segmentStart = i;
+ }
+ }
+ return uri.toString();
+ }
+
+ /**
+ * Calculates indices of the constituent components of a URI.
+ *
+ * @param uriString The URI as a string.
+ * @return The corresponding indices.
+ */
+ private static int[] getUriIndices(String uriString) {
+ int[] indices = new int[INDEX_COUNT];
+ if (TextUtils.isEmpty(uriString)) {
+ indices[SCHEME_COLON] = -1;
+ return indices;
+ }
+
+ // Determine outer structure from right to left.
+ // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+ int length = uriString.length();
+ int fragmentIndex = uriString.indexOf('#');
+ if (fragmentIndex == -1) {
+ fragmentIndex = length;
+ }
+ int queryIndex = uriString.indexOf('?');
+ if (queryIndex == -1 || queryIndex > fragmentIndex) {
+ // '#' before '?': '?' is within the fragment.
+ queryIndex = fragmentIndex;
+ }
+ // Slashes are allowed only in hier-part so any colon after the first slash is part of the
+ // hier-part, not the scheme colon separator.
+ int schemeIndexLimit = uriString.indexOf('/');
+ if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
+ schemeIndexLimit = queryIndex;
+ }
+ int schemeIndex = uriString.indexOf(':');
+ if (schemeIndex > schemeIndexLimit) {
+ // '/' before ':'
+ schemeIndex = -1;
+ }
+
+ // Determine hier-part structure: hier-part = "//" authority path / path
+ // This block can also cope with schemeIndex == -1.
+ boolean hasAuthority = schemeIndex + 2 < queryIndex
+ && uriString.charAt(schemeIndex + 1) == '/'
+ && uriString.charAt(schemeIndex + 2) == '/';
+ int pathIndex;
+ if (hasAuthority) {
+ pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://"
+ if (pathIndex == -1 || pathIndex > queryIndex) {
+ pathIndex = queryIndex;
+ }
+ } else {
+ pathIndex = schemeIndex + 1;
+ }
+
+ indices[SCHEME_COLON] = schemeIndex;
+ indices[PATH] = pathIndex;
+ indices[QUERY] = queryIndex;
+ indices[FRAGMENT] = fragmentIndex;
+ return indices;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java
new file mode 100644
index 0000000000..4d7d8014dd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/Util.java
@@ -0,0 +1,2298 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import static android.content.Context.UI_MODE_SERVICE;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.UiModeManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.media.AudioFormat;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.security.NetworkSecurityPolicy;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.view.Display;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RenderersFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SeekParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Formatter;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.TimeZone;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import org.checkerframework.checker.initialization.qual.UnknownInitialization;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.PolyNull;
+
+/**
+ * Miscellaneous utility methods.
+ */
+public final class Util {
+
+ /**
+ * Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently
+ * overridden for local testing.
+ */
+ public static final int SDK_INT = Build.VERSION.SDK_INT;
+
+ /**
+ * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local
+ * testing.
+ */
+ public static final String DEVICE = Build.DEVICE;
+
+ /**
+ * Like {@link Build#MANUFACTURER}, but in a place where it can be conveniently overridden for
+ * local testing.
+ */
+ public static final String MANUFACTURER = Build.MANUFACTURER;
+
+ /**
+ * Like {@link Build#MODEL}, but in a place where it can be conveniently overridden for local
+ * testing.
+ */
+ public static final String MODEL = Build.MODEL;
+
+ /**
+ * A concise description of the device that it can be useful to log for debugging purposes.
+ */
+ public static final String DEVICE_DEBUG_INFO = DEVICE + ", " + MODEL + ", " + MANUFACTURER + ", "
+ + SDK_INT;
+
+ /** An empty byte array. */
+ public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ private static final String TAG = "Util";
+ private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(
+ "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?"
+ + "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?");
+ private static final Pattern XS_DURATION_PATTERN =
+ Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
+ + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
+ private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
+
+ // Replacement map of ISO language codes used for normalization.
+ @Nullable private static HashMap<String, String> languageTagReplacementMap;
+
+ private Util() {}
+
+ /**
+ * Converts the entirety of an {@link InputStream} to a byte array.
+ *
+ * @param inputStream the {@link InputStream} to be read. The input stream is not closed by this
+ * method.
+ * @return a byte array containing all of the inputStream's bytes.
+ * @throws IOException if an error occurs reading from the stream.
+ */
+ public static byte[] toByteArray(InputStream inputStream) throws IOException {
+ byte[] buffer = new byte[1024 * 4];
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ return outputStream.toByteArray();
+ }
+
+ /**
+ * Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or
+ * {@link Context#startService(Intent)} otherwise.
+ *
+ * @param context The context to call.
+ * @param intent The intent to pass to the called method.
+ * @return The result of the called method.
+ */
+ @Nullable
+ public static ComponentName startForegroundService(Context context, Intent intent) {
+ if (Util.SDK_INT >= 26) {
+ return context.startForegroundService(intent);
+ } else {
+ return context.startService(intent);
+ }
+ }
+
+ /**
+ * Checks whether it's necessary to request the {@link permission#READ_EXTERNAL_STORAGE}
+ * permission read the specified {@link Uri}s, requesting the permission if necessary.
+ *
+ * @param activity The host activity for checking and requesting the permission.
+ * @param uris {@link Uri}s that may require {@link permission#READ_EXTERNAL_STORAGE} to read.
+ * @return Whether a permission request was made.
+ */
+ @TargetApi(23)
+ public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
+ if (Util.SDK_INT < 23) {
+ return false;
+ }
+ for (Uri uri : uris) {
+ if (isLocalFileUri(uri)) {
+ if (activity.checkSelfPermission(permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ activity.requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0);
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether it may be possible to load the given URIs based on the network security
+ * policy's cleartext traffic permissions.
+ *
+ * @param uris A list of URIs that will be loaded.
+ * @return Whether it may be possible to load the given URIs.
+ */
+ @TargetApi(24)
+ public static boolean checkCleartextTrafficPermitted(Uri... uris) {
+ if (Util.SDK_INT < 24) {
+ // We assume cleartext traffic is permitted.
+ return true;
+ }
+ for (Uri uri : uris) {
+ if ("http".equals(uri.getScheme())
+ && !NetworkSecurityPolicy.getInstance()
+ .isCleartextTrafficPermitted(Assertions.checkNotNull(uri.getHost()))) {
+ // The security policy prevents cleartext traffic.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if the URI is a path to a local file or a reference to a local file.
+ *
+ * @param uri The uri to test.
+ */
+ public static boolean isLocalFileUri(Uri uri) {
+ String scheme = uri.getScheme();
+ return TextUtils.isEmpty(scheme) || "file".equals(scheme);
+ }
+
+ /**
+ * Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or
+ * both may be null.
+ *
+ * @param o1 The first object.
+ * @param o2 The second object.
+ * @return {@code o1 == null ? o2 == null : o1.equals(o2)}.
+ */
+ public static boolean areEqual(@Nullable Object o1, @Nullable Object o2) {
+ return o1 == null ? o2 == null : o1.equals(o2);
+ }
+
+ /**
+ * Tests whether an {@code items} array contains an object equal to {@code item}, according to
+ * {@link Object#equals(Object)}.
+ *
+ * <p>If {@code item} is null then true is returned if and only if {@code items} contains null.
+ *
+ * @param items The array of items to search.
+ * @param item The item to search for.
+ * @return True if the array contains an object equal to the item being searched for.
+ */
+ public static boolean contains(@NullableType Object[] items, @Nullable Object item) {
+ for (Object arrayItem : items) {
+ if (areEqual(arrayItem, item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes an indexed range from a List.
+ *
+ * <p>Does nothing if the provided range is valid and {@code fromIndex == toIndex}.
+ *
+ * @param list The List to remove the range from.
+ * @param fromIndex The first index to be removed (inclusive).
+ * @param toIndex The last index to be removed (exclusive).
+ * @throws IllegalArgumentException If {@code fromIndex} &lt; 0, {@code toIndex} &gt; {@code
+ * list.size()}, or {@code fromIndex} &gt; {@code toIndex}.
+ */
+ public static <T> void removeRange(List<T> list, int fromIndex, int toIndex) {
+ if (fromIndex < 0 || toIndex > list.size() || fromIndex > toIndex) {
+ throw new IllegalArgumentException();
+ } else if (fromIndex != toIndex) {
+ // Checking index inequality prevents an unnecessary allocation.
+ list.subList(fromIndex, toIndex).clear();
+ }
+ }
+
+ /**
+ * Casts a nullable variable to a non-null variable without runtime null check.
+ *
+ * <p>Use {@link Assertions#checkNotNull(Object)} to throw if the value is null.
+ */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull("#1")
+ public static <T> T castNonNull(@Nullable T value) {
+ return value;
+ }
+
+ /** Casts a nullable type array to a non-null type array without runtime null check. */
+ @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"})
+ @EnsuresNonNull("#1")
+ public static <T> T[] castNonNullTypeArray(@NullableType T[] value) {
+ return value;
+ }
+
+ /**
+ * Copies and optionally truncates an array. Prevents null array elements created by {@link
+ * Arrays#copyOf(Object[], int)} by ensuring the new length does not exceed the current length.
+ *
+ * @param input The input array.
+ * @param length The output array length. Must be less or equal to the length of the input array.
+ * @return The copied array.
+ */
+ @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"})
+ public static <T> T[] nullSafeArrayCopy(T[] input, int length) {
+ Assertions.checkArgument(length <= input.length);
+ return Arrays.copyOf(input, length);
+ }
+
+ /**
+ * Copies a subset of an array.
+ *
+ * @param input The input array.
+ * @param from The start the range to be copied, inclusive
+ * @param to The end of the range to be copied, exclusive.
+ * @return The copied array.
+ */
+ @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"})
+ public static <T> T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) {
+ Assertions.checkArgument(0 <= from);
+ Assertions.checkArgument(to <= input.length);
+ return Arrays.copyOfRange(input, from, to);
+ }
+
+ /**
+ * Creates a new array containing {@code original} with {@code newElement} appended.
+ *
+ * @param original The input array.
+ * @param newElement The element to append.
+ * @return The new array.
+ */
+ public static <T> T[] nullSafeArrayAppend(T[] original, T newElement) {
+ @NullableType T[] result = Arrays.copyOf(original, original.length + 1);
+ result[original.length] = newElement;
+ return castNonNullTypeArray(result);
+ }
+
+ /**
+ * Creates a new array containing the concatenation of two non-null type arrays.
+ *
+ * @param first The first array.
+ * @param second The second array.
+ * @return The concatenated result.
+ */
+ @SuppressWarnings({"nullness:assignment.type.incompatible"})
+ public static <T> T[] nullSafeArrayConcatenation(T[] first, T[] second) {
+ T[] concatenation = Arrays.copyOf(first, first.length + second.length);
+ System.arraycopy(
+ /* src= */ second,
+ /* srcPos= */ 0,
+ /* dest= */ concatenation,
+ /* destPos= */ first.length,
+ /* length= */ second.length);
+ return concatenation;
+ }
+ /**
+ * Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link
+ * Looper} thread. The method accepts partially initialized objects as callback under the
+ * assumption that the Handler won't be used to send messages until the callback is fully
+ * initialized.
+ *
+ * <p>If the current thread doesn't have a {@link Looper}, the application's main thread {@link
+ * Looper} is used.
+ *
+ * @param callback A {@link Handler.Callback}. May be a partially initialized class.
+ * @return A {@link Handler} with the specified callback on the current {@link Looper} thread.
+ */
+ public static Handler createHandler(Handler.@UnknownInitialization Callback callback) {
+ return createHandler(getLooper(), callback);
+ }
+
+ /**
+ * Creates a {@link Handler} with the specified {@link Handler.Callback} on the specified {@link
+ * Looper} thread. The method accepts partially initialized objects as callback under the
+ * assumption that the Handler won't be used to send messages until the callback is fully
+ * initialized.
+ *
+ * @param looper A {@link Looper} to run the callback on.
+ * @param callback A {@link Handler.Callback}. May be a partially initialized class.
+ * @return A {@link Handler} with the specified callback on the current {@link Looper} thread.
+ */
+ @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"})
+ public static Handler createHandler(
+ Looper looper, Handler.@UnknownInitialization Callback callback) {
+ return new Handler(looper, callback);
+ }
+
+ /**
+ * Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the
+ * application's main thread if the current thread doesn't have a {@link Looper}.
+ */
+ public static Looper getLooper() {
+ Looper myLooper = Looper.myLooper();
+ return myLooper != null ? myLooper : Looper.getMainLooper();
+ }
+
+ /**
+ * Instantiates a new single threaded executor whose thread has the specified name.
+ *
+ * @param threadName The name of the thread.
+ * @return The executor.
+ */
+ public static ExecutorService newSingleThreadExecutor(final String threadName) {
+ return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
+ }
+
+ /**
+ * Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
+ *
+ * @param dataSource The {@link DataSource} to close.
+ */
+ public static void closeQuietly(@Nullable DataSource dataSource) {
+ try {
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Closes a {@link Closeable}, suppressing any {@link IOException} that may occur. Both {@link
+ * java.io.OutputStream} and {@link InputStream} are {@code Closeable}.
+ *
+ * @param closeable The {@link Closeable} to close.
+ */
+ public static void closeQuietly(@Nullable Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Reads an integer from a {@link Parcel} and interprets it as a boolean, with 0 mapping to false
+ * and all other values mapping to true.
+ *
+ * @param parcel The {@link Parcel} to read from.
+ * @return The read value.
+ */
+ public static boolean readBoolean(Parcel parcel) {
+ return parcel.readInt() != 0;
+ }
+
+ /**
+ * Writes a boolean to a {@link Parcel}. The boolean is written as an integer with value 1 (true)
+ * or 0 (false).
+ *
+ * @param parcel The {@link Parcel} to write to.
+ * @param value The value to write.
+ */
+ public static void writeBoolean(Parcel parcel, boolean value) {
+ parcel.writeInt(value ? 1 : 0);
+ }
+
+ /**
+ * Returns the language tag for a {@link Locale}.
+ *
+ * <p>For API levels &ge; 21, this tag is IETF BCP 47 compliant. Use {@link
+ * #normalizeLanguageCode(String)} to retrieve a normalized IETF BCP 47 language tag for all API
+ * levels if needed.
+ *
+ * @param locale A {@link Locale}.
+ * @return The language tag.
+ */
+ public static String getLocaleLanguageTag(Locale locale) {
+ return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString();
+ }
+
+ /**
+ * Returns a normalized IETF BCP 47 language tag for {@code language}.
+ *
+ * @param language A case-insensitive language code supported by {@link
+ * Locale#forLanguageTag(String)}.
+ * @return The all-lowercase normalized code, or null if the input was null, or {@code
+ * language.toLowerCase()} if the language could not be normalized.
+ */
+ public static @PolyNull String normalizeLanguageCode(@PolyNull String language) {
+ if (language == null) {
+ return null;
+ }
+ // Locale data (especially for API < 21) may produce tags with '_' instead of the
+ // standard-conformant '-'.
+ String normalizedTag = language.replace('_', '-');
+ if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) {
+ // Tag isn't valid, keep using the original.
+ normalizedTag = language;
+ }
+ normalizedTag = Util.toLowerInvariant(normalizedTag);
+ String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0];
+ if (languageTagReplacementMap == null) {
+ languageTagReplacementMap = createIsoLanguageReplacementMap();
+ }
+ @Nullable String replacedLanguage = languageTagReplacementMap.get(mainLanguage);
+ if (replacedLanguage != null) {
+ normalizedTag =
+ replacedLanguage + normalizedTag.substring(/* beginIndex= */ mainLanguage.length());
+ mainLanguage = replacedLanguage;
+ }
+ if ("no".equals(mainLanguage) || "i".equals(mainLanguage) || "zh".equals(mainLanguage)) {
+ normalizedTag = maybeReplaceGrandfatheredLanguageTags(normalizedTag);
+ }
+ return normalizedTag;
+ }
+
+ /**
+ * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes.
+ *
+ * @param bytes The UTF-8 encoded bytes to decode.
+ * @return The string.
+ */
+ public static String fromUtf8Bytes(byte[] bytes) {
+ return new String(bytes, Charset.forName(C.UTF8_NAME));
+ }
+
+ /**
+ * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray.
+ *
+ * @param bytes The UTF-8 encoded bytes to decode.
+ * @param offset The index of the first byte to decode.
+ * @param length The number of bytes to decode.
+ * @return The string.
+ */
+ public static String fromUtf8Bytes(byte[] bytes, int offset, int length) {
+ return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME));
+ }
+
+ /**
+ * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
+ *
+ * @param value The {@link String} whose bytes should be obtained.
+ * @return The code points encoding using UTF-8.
+ */
+ public static byte[] getUtf8Bytes(String value) {
+ return value.getBytes(Charset.forName(C.UTF8_NAME));
+ }
+
+ /**
+ * Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link
+ * String#split(String)} but empty matches at the end of the string will not be omitted from the
+ * returned array.
+ *
+ * @param value The string to split.
+ * @param regex A delimiting regular expression.
+ * @return The array of strings resulting from splitting the string.
+ */
+ public static String[] split(String value, String regex) {
+ return value.split(regex, /* limit= */ -1);
+ }
+
+ /**
+ * Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does
+ * not match, returns an array with one element which is the input string. If the delimiter does
+ * match, returns an array with the portion of the string before the delimiter and the rest of the
+ * string.
+ *
+ * @param value The string.
+ * @param regex A delimiting regular expression.
+ * @return The string split by the first occurrence of the delimiter.
+ */
+ public static String[] splitAtFirst(String value, String regex) {
+ return value.split(regex, /* limit= */ 2);
+ }
+
+ /**
+ * Returns whether the given character is a carriage return ('\r') or a line feed ('\n').
+ *
+ * @param c The character.
+ * @return Whether the given character is a linebreak.
+ */
+ public static boolean isLinebreak(int c) {
+ return c == '\n' || c == '\r';
+ }
+
+ /**
+ * Converts text to lower case using {@link Locale#US}.
+ *
+ * @param text The text to convert.
+ * @return The lower case text, or null if {@code text} is null.
+ */
+ public static @PolyNull String toLowerInvariant(@PolyNull String text) {
+ return text == null ? text : text.toLowerCase(Locale.US);
+ }
+
+ /**
+ * Converts text to upper case using {@link Locale#US}.
+ *
+ * @param text The text to convert.
+ * @return The upper case text, or null if {@code text} is null.
+ */
+ public static @PolyNull String toUpperInvariant(@PolyNull String text) {
+ return text == null ? text : text.toUpperCase(Locale.US);
+ }
+
+ /**
+ * Formats a string using {@link Locale#US}.
+ *
+ * @see String#format(String, Object...)
+ */
+ public static String formatInvariant(String format, Object... args) {
+ return String.format(Locale.US, format, args);
+ }
+
+ /**
+ * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
+ *
+ * @param numerator The numerator to divide.
+ * @param denominator The denominator to divide by.
+ * @return The ceiled result of the division.
+ */
+ public static int ceilDivide(int numerator, int denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ /**
+ * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result.
+ *
+ * @param numerator The numerator to divide.
+ * @param denominator The denominator to divide by.
+ * @return The ceiled result of the division.
+ */
+ public static long ceilDivide(long numerator, long denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static int constrainValue(int value, int min, int max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static long constrainValue(long value, long min, long max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Constrains a value to the specified bounds.
+ *
+ * @param value The value to constrain.
+ * @param min The lower bound.
+ * @param max The upper bound.
+ * @return The constrained value {@code Math.max(min, Math.min(value, max))}.
+ */
+ public static float constrainValue(float value, float min, float max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ /**
+ * Returns the sum of two arguments, or a third argument if the result overflows.
+ *
+ * @param x The first value.
+ * @param y The second value.
+ * @param overflowResult The return value if {@code x + y} overflows.
+ * @return {@code x + y}, or {@code overflowResult} if the result overflows.
+ */
+ public static long addWithOverflowDefault(long x, long y, long overflowResult) {
+ long result = x + y;
+ // See Hacker's Delight 2-13 (H. Warren Jr).
+ if (((x ^ result) & (y ^ result)) < 0) {
+ return overflowResult;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the difference between two arguments, or a third argument if the result overflows.
+ *
+ * @param x The first value.
+ * @param y The second value.
+ * @param overflowResult The return value if {@code x - y} overflows.
+ * @return {@code x - y}, or {@code overflowResult} if the result overflows.
+ */
+ public static long subtractWithOverflowDefault(long x, long y, long overflowResult) {
+ long result = x - y;
+ // See Hacker's Delight 2-13 (H. Warren Jr).
+ if (((x ^ y) & (x ^ result)) < 0) {
+ return overflowResult;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link
+ * C#INDEX_UNSET} if {@code value} is not contained in {@code array}.
+ *
+ * @param array The array to search.
+ * @param value The value to search for.
+ * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET}
+ * if {@code value} is not contained in {@code array}.
+ */
+ public static int linearSearch(int[] array, int value) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i] == value) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ /**
+ * Returns the index of the largest element in {@code array} that is less than (or optionally
+ * equal to) a specified {@code value}.
+ *
+ * <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
+ * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the first one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the largest element strictly less
+ * than the value.
+ * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
+ * the smallest element in the array. If false then -1 will be returned.
+ * @return The index of the largest element in {@code array} that is less than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchFloor(
+ int[] array, int value, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ while (--index >= 0 && array[index] == value) {}
+ if (inclusive) {
+ index++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the largest element in {@code array} that is less than (or optionally
+ * equal to) a specified {@code value}.
+ * <p>
+ * The search is performed using a binary search algorithm, so the array must be sorted. If the
+ * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the first one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the largest element strictly less
+ * than the value.
+ * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
+ * the smallest element in the array. If false then -1 will be returned.
+ * @return The index of the largest element in {@code array} that is less than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchFloor(long[] array, long value, boolean inclusive,
+ boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ while (--index >= 0 && array[index] == value) {}
+ if (inclusive) {
+ index++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the largest element in {@code list} that is less than (or optionally equal
+ * to) a specified {@code value}.
+ *
+ * <p>The search is performed using a binary search algorithm, so the list must be sorted. If the
+ * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index
+ * of the first one will be returned.
+ *
+ * @param <T> The type of values being searched.
+ * @param list The list to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the list, whether to return the corresponding
+ * index. If false then the returned index corresponds to the largest element strictly less
+ * than the value.
+ * @param stayInBounds If true, then 0 will be returned in the case that the value is smaller than
+ * the smallest element in the list. If false then -1 will be returned.
+ * @return The index of the largest element in {@code list} that is less than (or optionally equal
+ * to) {@code value}.
+ */
+ public static <T extends Comparable<? super T>> int binarySearchFloor(
+ List<? extends Comparable<? super T>> list,
+ T value,
+ boolean inclusive,
+ boolean stayInBounds) {
+ int index = Collections.binarySearch(list, value);
+ if (index < 0) {
+ index = -(index + 2);
+ } else {
+ while (--index >= 0 && list.get(index).compareTo(value) == 0) {}
+ if (inclusive) {
+ index++;
+ }
+ }
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) a specified {@code value}.
+ *
+ * <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
+ * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the last one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the smallest element strictly
+ * greater than the value.
+ * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
+ * value is greater than the largest element in the array. If false then {@code a.length} will
+ * be returned.
+ * @return The index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchCeil(
+ int[] array, int value, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = ~index;
+ } else {
+ while (++index < array.length && array[index] == value) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ return stayInBounds ? Math.min(array.length - 1, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) a specified {@code value}.
+ *
+ * <p>The search is performed using a binary search algorithm, so the array must be sorted. If the
+ * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the
+ * index of the last one will be returned.
+ *
+ * @param array The array to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the array, whether to return the corresponding
+ * index. If false then the returned index corresponds to the smallest element strictly
+ * greater than the value.
+ * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
+ * value is greater than the largest element in the array. If false then {@code a.length} will
+ * be returned.
+ * @return The index of the smallest element in {@code array} that is greater than (or optionally
+ * equal to) {@code value}.
+ */
+ public static int binarySearchCeil(
+ long[] array, long value, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(array, value);
+ if (index < 0) {
+ index = ~index;
+ } else {
+ while (++index < array.length && array[index] == value) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ return stayInBounds ? Math.min(array.length - 1, index) : index;
+ }
+
+ /**
+ * Returns the index of the smallest element in {@code list} that is greater than (or optionally
+ * equal to) a specified value.
+ *
+ * <p>The search is performed using a binary search algorithm, so the list must be sorted. If the
+ * list contains multiple elements equal to {@code value} and {@code inclusive} is true, the index
+ * of the last one will be returned.
+ *
+ * @param <T> The type of values being searched.
+ * @param list The list to search.
+ * @param value The value being searched for.
+ * @param inclusive If the value is present in the list, whether to return the corresponding
+ * index. If false then the returned index corresponds to the smallest element strictly
+ * greater than the value.
+ * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
+ * the value is greater than the largest element in the list. If false then {@code
+ * list.size()} will be returned.
+ * @return The index of the smallest element in {@code list} that is greater than (or optionally
+ * equal to) {@code value}.
+ */
+ public static <T extends Comparable<? super T>> int binarySearchCeil(
+ List<? extends Comparable<? super T>> list,
+ T value,
+ boolean inclusive,
+ boolean stayInBounds) {
+ int index = Collections.binarySearch(list, value);
+ if (index < 0) {
+ index = ~index;
+ } else {
+ int listSize = list.size();
+ while (++index < listSize && list.get(index).compareTo(value) == 0) {}
+ if (inclusive) {
+ index--;
+ }
+ }
+ return stayInBounds ? Math.min(list.size() - 1, index) : index;
+ }
+
+ /**
+ * Compares two long values and returns the same value as {@code Long.compare(long, long)}.
+ *
+ * @param left The left operand.
+ * @param right The right operand.
+ * @return 0, if left == right, a negative value if left &lt; right, or a positive value if left
+ * &gt; right.
+ */
+ public static int compareLong(long left, long right) {
+ return left < right ? -1 : left == right ? 0 : 1;
+ }
+
+ /**
+ * Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
+ *
+ * @param value The attribute value to decode.
+ * @return The parsed duration in milliseconds.
+ */
+ public static long parseXsDuration(String value) {
+ Matcher matcher = XS_DURATION_PATTERN.matcher(value);
+ if (matcher.matches()) {
+ boolean negated = !TextUtils.isEmpty(matcher.group(1));
+ // Durations containing years and months aren't completely defined. We assume there are
+ // 30.4368 days in a month, and 365.242 days in a year.
+ String years = matcher.group(3);
+ double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0;
+ String months = matcher.group(5);
+ durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0;
+ String days = matcher.group(7);
+ durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0;
+ String hours = matcher.group(10);
+ durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
+ String minutes = matcher.group(12);
+ durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
+ String seconds = matcher.group(14);
+ durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
+ long durationMillis = (long) (durationSeconds * 1000);
+ return negated ? -durationMillis : durationMillis;
+ } else {
+ return (long) (Double.parseDouble(value) * 3600 * 1000);
+ }
+ }
+
+ /**
+ * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since
+ * the epoch.
+ *
+ * @param value The attribute value to decode.
+ * @return The parsed timestamp in milliseconds since the epoch.
+ * @throws ParserException if an error occurs parsing the dateTime attribute value.
+ */
+ public static long parseXsDateTime(String value) throws ParserException {
+ Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);
+ if (!matcher.matches()) {
+ throw new ParserException("Invalid date/time format: " + value);
+ }
+
+ int timezoneShift;
+ if (matcher.group(9) == null) {
+ // No time zone specified.
+ timezoneShift = 0;
+ } else if (matcher.group(9).equalsIgnoreCase("Z")) {
+ timezoneShift = 0;
+ } else {
+ timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60
+ + Integer.parseInt(matcher.group(13))));
+ if ("-".equals(matcher.group(11))) {
+ timezoneShift *= -1;
+ }
+ }
+
+ Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+
+ dateTime.clear();
+ // Note: The month value is 0-based, hence the -1 on group(2)
+ dateTime.set(Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2)) - 1,
+ Integer.parseInt(matcher.group(3)),
+ Integer.parseInt(matcher.group(4)),
+ Integer.parseInt(matcher.group(5)),
+ Integer.parseInt(matcher.group(6)));
+ if (!TextUtils.isEmpty(matcher.group(8))) {
+ final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
+ // we care only for milliseconds, so movePointRight(3)
+ dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
+ }
+
+ long time = dateTime.getTimeInMillis();
+ if (timezoneShift != 0) {
+ time -= timezoneShift * 60000;
+ }
+
+ return time;
+ }
+
+ /**
+ * Scales a large timestamp.
+ * <p>
+ * Logically, scaling consists of a multiplication followed by a division. The actual operations
+ * performed are designed to minimize the probability of overflow.
+ *
+ * @param timestamp The timestamp to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ * @return The scaled timestamp.
+ */
+ public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ return timestamp / divisionFactor;
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ return timestamp * multiplicationFactor;
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ return (long) (timestamp * multiplicationFactor);
+ }
+ }
+
+ /**
+ * Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps.
+ *
+ * @param timestamps The timestamps to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ * @return The scaled timestamps.
+ */
+ public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {
+ long[] scaledTimestamps = new long[timestamps.size()];
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = timestamps.get(i) / divisionFactor;
+ }
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;
+ }
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ for (int i = 0; i < scaledTimestamps.length; i++) {
+ scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);
+ }
+ }
+ return scaledTimestamps;
+ }
+
+ /**
+ * Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps.
+ *
+ * @param timestamps The timestamps to scale.
+ * @param multiplier The multiplier.
+ * @param divisor The divisor.
+ */
+ public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {
+ if (divisor >= multiplier && (divisor % multiplier) == 0) {
+ long divisionFactor = divisor / multiplier;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] /= divisionFactor;
+ }
+ } else if (divisor < multiplier && (multiplier % divisor) == 0) {
+ long multiplicationFactor = multiplier / divisor;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] *= multiplicationFactor;
+ }
+ } else {
+ double multiplicationFactor = (double) multiplier / divisor;
+ for (int i = 0; i < timestamps.length; i++) {
+ timestamps[i] = (long) (timestamps[i] * multiplicationFactor);
+ }
+ }
+ }
+
+ /**
+ * Returns the duration of media that will elapse in {@code playoutDuration}.
+ *
+ * @param playoutDuration The duration to scale.
+ * @param speed The playback speed.
+ * @return The scaled duration, in the same units as {@code playoutDuration}.
+ */
+ public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) {
+ if (speed == 1f) {
+ return playoutDuration;
+ }
+ return Math.round((double) playoutDuration * speed);
+ }
+
+ /**
+ * Returns the playout duration of {@code mediaDuration} of media.
+ *
+ * @param mediaDuration The duration to scale.
+ * @return The scaled duration, in the same units as {@code mediaDuration}.
+ */
+ public static long getPlayoutDurationForMediaDuration(long mediaDuration, float speed) {
+ if (speed == 1f) {
+ return mediaDuration;
+ }
+ return Math.round((double) mediaDuration / speed);
+ }
+
+ /**
+ * Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate
+ * sync points.
+ *
+ * @param positionUs The requested seek position, in microseocnds.
+ * @param seekParameters The {@link SeekParameters}.
+ * @param firstSyncUs The first candidate seek point, in micrseconds.
+ * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code
+ * firstSyncUs} if there's only one candidate.
+ * @return The resolved seek position, in microseconds.
+ */
+ public static long resolveSeekPositionUs(
+ long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) {
+ if (SeekParameters.EXACT.equals(seekParameters)) {
+ return positionUs;
+ }
+ long minPositionUs =
+ subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE);
+ long maxPositionUs =
+ addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE);
+ boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs;
+ boolean secondSyncPositionValid =
+ minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs;
+ if (firstSyncPositionValid && secondSyncPositionValid) {
+ if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) {
+ return firstSyncUs;
+ } else {
+ return secondSyncUs;
+ }
+ } else if (firstSyncPositionValid) {
+ return firstSyncUs;
+ } else if (secondSyncPositionValid) {
+ return secondSyncUs;
+ } else {
+ return minPositionUs;
+ }
+ }
+
+ /**
+ * Converts a list of integers to a primitive array.
+ *
+ * @param list A list of integers.
+ * @return The list in array form, or null if the input list was null.
+ */
+ public static int @PolyNull [] toArray(@PolyNull List<Integer> list) {
+ if (list == null) {
+ return null;
+ }
+ int length = list.size();
+ int[] intArray = new int[length];
+ for (int i = 0; i < length; i++) {
+ intArray[i] = list.get(i);
+ }
+ return intArray;
+ }
+
+ /**
+ * Returns the integer equal to the big-endian concatenation of the characters in {@code string}
+ * as bytes. The string must be no more than four characters long.
+ *
+ * @param string A string no more than four characters long.
+ */
+ public static int getIntegerCodeForString(String string) {
+ int length = string.length();
+ Assertions.checkArgument(length <= 4);
+ int result = 0;
+ for (int i = 0; i < length; i++) {
+ result <<= 8;
+ result |= string.charAt(i);
+ }
+ return result;
+ }
+
+ /**
+ * Converts an integer to a long by unsigned conversion.
+ *
+ * <p>This method is equivalent to {@link Integer#toUnsignedLong(int)} for API 26+.
+ */
+ public static long toUnsignedLong(int x) {
+ // x is implicitly casted to a long before the bit operation is executed but this does not
+ // impact the method correctness.
+ return x & 0xFFFFFFFFL;
+ }
+
+ /**
+ * Return the long that is composed of the bits of the 2 specified integers.
+ *
+ * @param mostSignificantBits The 32 most significant bits of the long to return.
+ * @param leastSignificantBits The 32 least significant bits of the long to return.
+ * @return a long where its 32 most significant bits are {@code mostSignificantBits} bits and its
+ * 32 least significant bits are {@code leastSignificantBits}.
+ */
+ public static long toLong(int mostSignificantBits, int leastSignificantBits) {
+ return (toUnsignedLong(mostSignificantBits) << 32) | toUnsignedLong(leastSignificantBits);
+ }
+
+ /**
+ * Returns a byte array containing values parsed from the hex string provided.
+ *
+ * @param hexString The hex string to convert to bytes.
+ * @return A byte array containing values parsed from the hex string provided.
+ */
+ public static byte[] getBytesFromHexString(String hexString) {
+ byte[] data = new byte[hexString.length() / 2];
+ for (int i = 0; i < data.length; i++) {
+ int stringOffset = i * 2;
+ data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
+ + Character.digit(hexString.charAt(stringOffset + 1), 16));
+ }
+ return data;
+ }
+
+ /**
+ * Returns a string with comma delimited simple names of each object's class.
+ *
+ * @param objects The objects whose simple class names should be comma delimited and returned.
+ * @return A string with comma delimited simple names of each object's class.
+ */
+ public static String getCommaDelimitedSimpleClassNames(Object[] objects) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < objects.length; i++) {
+ stringBuilder.append(objects[i].getClass().getSimpleName());
+ if (i < objects.length - 1) {
+ stringBuilder.append(", ");
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Returns a user agent string based on the given application name and the library version.
+ *
+ * @param context A valid context of the calling application.
+ * @param applicationName String that will be prefix'ed to the generated user agent.
+ * @return A user agent string generated using the applicationName and the library version.
+ */
+ public static String getUserAgent(Context context, String applicationName) {
+ String versionName;
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ versionName = info.versionName;
+ } catch (NameNotFoundException e) {
+ versionName = "?";
+ }
+ return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE
+ + ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
+ }
+
+ /**
+ * Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code
+ * trackType}.
+ *
+ * @param codecs A codec sequence string, as defined in RFC 6381.
+ * @param trackType One of {@link C}{@code .TRACK_TYPE_*}.
+ * @return A copy of {@code codecs} without the codecs whose track type doesn't match {@code
+ * trackType}. If this ends up empty, or {@code codecs} is null, return null.
+ */
+ public static @Nullable String getCodecsOfType(@Nullable String codecs, int trackType) {
+ String[] codecArray = splitCodecs(codecs);
+ if (codecArray.length == 0) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (String codec : codecArray) {
+ if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
+ if (builder.length() > 0) {
+ builder.append(",");
+ }
+ builder.append(codec);
+ }
+ }
+ return builder.length() > 0 ? builder.toString() : null;
+ }
+
+ /**
+ * Splits a codecs sequence string, as defined in RFC 6381, into individual codec strings.
+ *
+ * @param codecs A codec sequence string, as defined in RFC 6381.
+ * @return The split codecs, or an array of length zero if the input was empty or null.
+ */
+ public static String[] splitCodecs(@Nullable String codecs) {
+ if (TextUtils.isEmpty(codecs)) {
+ return new String[0];
+ }
+ return split(codecs.trim(), "(\\s*,\\s*)");
+ }
+
+ /**
+ * Converts a sample bit depth to a corresponding PCM encoding constant.
+ *
+ * @param bitDepth The bit depth. Supported values are 8, 16, 24 and 32.
+ * @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT},
+ * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
+ * {@link C#ENCODING_PCM_32BIT}. If the bit depth is unsupported then
+ * {@link C#ENCODING_INVALID} is returned.
+ */
+ @C.PcmEncoding
+ public static int getPcmEncoding(int bitDepth) {
+ switch (bitDepth) {
+ case 8:
+ return C.ENCODING_PCM_8BIT;
+ case 16:
+ return C.ENCODING_PCM_16BIT;
+ case 24:
+ return C.ENCODING_PCM_24BIT;
+ case 32:
+ return C.ENCODING_PCM_32BIT;
+ default:
+ return C.ENCODING_INVALID;
+ }
+ }
+
+ /**
+ * Returns whether {@code encoding} is one of the linear PCM encodings.
+ *
+ * @param encoding The encoding of the audio data.
+ * @return Whether the encoding is one of the PCM encodings.
+ */
+ public static boolean isEncodingLinearPcm(@C.Encoding int encoding) {
+ return encoding == C.ENCODING_PCM_8BIT
+ || encoding == C.ENCODING_PCM_16BIT
+ || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN
+ || encoding == C.ENCODING_PCM_24BIT
+ || encoding == C.ENCODING_PCM_32BIT
+ || encoding == C.ENCODING_PCM_FLOAT;
+ }
+
+ /**
+ * Returns whether {@code encoding} is high resolution (&gt; 16-bit) PCM.
+ *
+ * @param encoding The encoding of the audio data.
+ * @return Whether the encoding is high resolution PCM.
+ */
+ public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) {
+ return encoding == C.ENCODING_PCM_24BIT
+ || encoding == C.ENCODING_PCM_32BIT
+ || encoding == C.ENCODING_PCM_FLOAT;
+ }
+
+ /**
+ * Returns the audio track channel configuration for the given channel count, or {@link
+ * AudioFormat#CHANNEL_INVALID} if output is not poossible.
+ *
+ * @param channelCount The number of channels in the input audio.
+ * @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not
+ * possible.
+ */
+ public static int getAudioTrackChannelConfig(int channelCount) {
+ switch (channelCount) {
+ case 1:
+ return AudioFormat.CHANNEL_OUT_MONO;
+ case 2:
+ return AudioFormat.CHANNEL_OUT_STEREO;
+ case 3:
+ return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+ case 4:
+ return AudioFormat.CHANNEL_OUT_QUAD;
+ case 5:
+ return AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+ case 6:
+ return AudioFormat.CHANNEL_OUT_5POINT1;
+ case 7:
+ return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
+ case 8:
+ if (Util.SDK_INT >= 23) {
+ return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
+ } else if (Util.SDK_INT >= 21) {
+ // Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.
+ return AudioFormat.CHANNEL_OUT_5POINT1
+ | AudioFormat.CHANNEL_OUT_SIDE_LEFT
+ | AudioFormat.CHANNEL_OUT_SIDE_RIGHT;
+ } else {
+ // 8 ch output is not supported before Android L.
+ return AudioFormat.CHANNEL_INVALID;
+ }
+ default:
+ return AudioFormat.CHANNEL_INVALID;
+ }
+ }
+
+ /**
+ * Returns the frame size for audio with {@code channelCount} channels in the specified encoding.
+ *
+ * @param pcmEncoding The encoding of the audio data.
+ * @param channelCount The channel count.
+ * @return The size of one audio frame in bytes.
+ */
+ public static int getPcmFrameSize(@C.PcmEncoding int pcmEncoding, int channelCount) {
+ switch (pcmEncoding) {
+ case C.ENCODING_PCM_8BIT:
+ return channelCount;
+ case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
+ return channelCount * 2;
+ case C.ENCODING_PCM_24BIT:
+ return channelCount * 3;
+ case C.ENCODING_PCM_32BIT:
+ case C.ENCODING_PCM_FLOAT:
+ return channelCount * 4;
+ case C.ENCODING_INVALID:
+ case Format.NO_VALUE:
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}.
+ */
+ @C.AudioUsage
+ public static int getAudioUsageForStreamType(@C.StreamType int streamType) {
+ switch (streamType) {
+ case C.STREAM_TYPE_ALARM:
+ return C.USAGE_ALARM;
+ case C.STREAM_TYPE_DTMF:
+ return C.USAGE_VOICE_COMMUNICATION_SIGNALLING;
+ case C.STREAM_TYPE_NOTIFICATION:
+ return C.USAGE_NOTIFICATION;
+ case C.STREAM_TYPE_RING:
+ return C.USAGE_NOTIFICATION_RINGTONE;
+ case C.STREAM_TYPE_SYSTEM:
+ return C.USAGE_ASSISTANCE_SONIFICATION;
+ case C.STREAM_TYPE_VOICE_CALL:
+ return C.USAGE_VOICE_COMMUNICATION;
+ case C.STREAM_TYPE_USE_DEFAULT:
+ case C.STREAM_TYPE_MUSIC:
+ default:
+ return C.USAGE_MEDIA;
+ }
+ }
+
+ /**
+ * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}.
+ */
+ @C.AudioContentType
+ public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) {
+ switch (streamType) {
+ case C.STREAM_TYPE_ALARM:
+ case C.STREAM_TYPE_DTMF:
+ case C.STREAM_TYPE_NOTIFICATION:
+ case C.STREAM_TYPE_RING:
+ case C.STREAM_TYPE_SYSTEM:
+ return C.CONTENT_TYPE_SONIFICATION;
+ case C.STREAM_TYPE_VOICE_CALL:
+ return C.CONTENT_TYPE_SPEECH;
+ case C.STREAM_TYPE_USE_DEFAULT:
+ case C.STREAM_TYPE_MUSIC:
+ default:
+ return C.CONTENT_TYPE_MUSIC;
+ }
+ }
+
+ /**
+ * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}.
+ */
+ @C.StreamType
+ public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) {
+ switch (usage) {
+ case C.USAGE_MEDIA:
+ case C.USAGE_GAME:
+ case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
+ return C.STREAM_TYPE_MUSIC;
+ case C.USAGE_ASSISTANCE_SONIFICATION:
+ return C.STREAM_TYPE_SYSTEM;
+ case C.USAGE_VOICE_COMMUNICATION:
+ return C.STREAM_TYPE_VOICE_CALL;
+ case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
+ return C.STREAM_TYPE_DTMF;
+ case C.USAGE_ALARM:
+ return C.STREAM_TYPE_ALARM;
+ case C.USAGE_NOTIFICATION_RINGTONE:
+ return C.STREAM_TYPE_RING;
+ case C.USAGE_NOTIFICATION:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
+ case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
+ case C.USAGE_NOTIFICATION_EVENT:
+ return C.STREAM_TYPE_NOTIFICATION;
+ case C.USAGE_ASSISTANCE_ACCESSIBILITY:
+ case C.USAGE_ASSISTANT:
+ case C.USAGE_UNKNOWN:
+ default:
+ return C.STREAM_TYPE_DEFAULT;
+ }
+ }
+
+ /**
+ * Derives a DRM {@link UUID} from {@code drmScheme}.
+ *
+ * @param drmScheme A UUID string, or {@code "widevine"}, {@code "playready"} or {@code
+ * "clearkey"}.
+ * @return The derived {@link UUID}, or {@code null} if one could not be derived.
+ */
+ public static @Nullable UUID getDrmUuid(String drmScheme) {
+ switch (toLowerInvariant(drmScheme)) {
+ case "widevine":
+ return C.WIDEVINE_UUID;
+ case "playready":
+ return C.PLAYREADY_UUID;
+ case "clearkey":
+ return C.CLEARKEY_UUID;
+ default:
+ try {
+ return UUID.fromString(drmScheme);
+ } catch (RuntimeException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Makes a best guess to infer the type from a {@link Uri}.
+ *
+ * @param uri The {@link Uri}.
+ * @param overrideExtension If not null, used to infer the type.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(Uri uri, @Nullable String overrideExtension) {
+ return TextUtils.isEmpty(overrideExtension)
+ ? inferContentType(uri)
+ : inferContentType("." + overrideExtension);
+ }
+
+ /**
+ * Makes a best guess to infer the type from a {@link Uri}.
+ *
+ * @param uri The {@link Uri}.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(Uri uri) {
+ String path = uri.getPath();
+ return path == null ? C.TYPE_OTHER : inferContentType(path);
+ }
+
+ /**
+ * Makes a best guess to infer the type from a file name.
+ *
+ * @param fileName Name of the file. It can include the path of the file.
+ * @return The content type.
+ */
+ @C.ContentType
+ public static int inferContentType(String fileName) {
+ fileName = toLowerInvariant(fileName);
+ if (fileName.endsWith(".mpd")) {
+ return C.TYPE_DASH;
+ } else if (fileName.endsWith(".m3u8")) {
+ return C.TYPE_HLS;
+ } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {
+ return C.TYPE_SS;
+ } else {
+ return C.TYPE_OTHER;
+ }
+ }
+
+ /**
+ * Returns the specified millisecond time formatted as a string.
+ *
+ * @param builder The builder that {@code formatter} will write to.
+ * @param formatter The formatter.
+ * @param timeMs The time to format as a string, in milliseconds.
+ * @return The time formatted as a string.
+ */
+ public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) {
+ if (timeMs == C.TIME_UNSET) {
+ timeMs = 0;
+ }
+ long totalSeconds = (timeMs + 500) / 1000;
+ long seconds = totalSeconds % 60;
+ long minutes = (totalSeconds / 60) % 60;
+ long hours = totalSeconds / 3600;
+ builder.setLength(0);
+ return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
+ : formatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+
+ /**
+ * Escapes a string so that it's safe for use as a file or directory name on at least FAT32
+ * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
+ *
+ * <p>For simplicity, this only handles common characters known to be illegal on FAT32:
+ * &lt;, &gt;, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape
+ * character. Escaping is performed in a consistent way so that no collisions occur and
+ * {@link #unescapeFileName(String)} can be used to retrieve the original file name.
+ *
+ * @param fileName File name to be escaped.
+ * @return An escaped file name which will be safe for use on at least FAT32 filesystems.
+ */
+ public static String escapeFileName(String fileName) {
+ int length = fileName.length();
+ int charactersToEscapeCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (shouldEscapeCharacter(fileName.charAt(i))) {
+ charactersToEscapeCount++;
+ }
+ }
+ if (charactersToEscapeCount == 0) {
+ return fileName;
+ }
+
+ int i = 0;
+ StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2);
+ while (charactersToEscapeCount > 0) {
+ char c = fileName.charAt(i++);
+ if (shouldEscapeCharacter(c)) {
+ builder.append('%').append(Integer.toHexString(c));
+ charactersToEscapeCount--;
+ } else {
+ builder.append(c);
+ }
+ }
+ if (i < length) {
+ builder.append(fileName, i, length);
+ }
+ return builder.toString();
+ }
+
+ private static boolean shouldEscapeCharacter(char c) {
+ switch (c) {
+ case '<':
+ case '>':
+ case ':':
+ case '"':
+ case '/':
+ case '\\':
+ case '|':
+ case '?':
+ case '*':
+ case '%':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Unescapes an escaped file or directory name back to its original value.
+ *
+ * <p>See {@link #escapeFileName(String)} for more information.
+ *
+ * @param fileName File name to be unescaped.
+ * @return The original value of the file name before it was escaped, or null if the escaped
+ * fileName seems invalid.
+ */
+ public static @Nullable String unescapeFileName(String fileName) {
+ int length = fileName.length();
+ int percentCharacterCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (fileName.charAt(i) == '%') {
+ percentCharacterCount++;
+ }
+ }
+ if (percentCharacterCount == 0) {
+ return fileName;
+ }
+
+ int expectedLength = length - percentCharacterCount * 2;
+ StringBuilder builder = new StringBuilder(expectedLength);
+ Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName);
+ int startOfNotEscaped = 0;
+ while (percentCharacterCount > 0 && matcher.find()) {
+ char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
+ builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter);
+ startOfNotEscaped = matcher.end();
+ percentCharacterCount--;
+ }
+ if (startOfNotEscaped < length) {
+ builder.append(fileName, startOfNotEscaped, length);
+ }
+ if (builder.length() != expectedLength) {
+ return null;
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A hacky method that always throws {@code t} even if {@code t} is a checked exception,
+ * and is not declared to be thrown.
+ */
+ public static void sneakyThrow(Throwable t) {
+ sneakyThrowInternal(t);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /** Recursively deletes a directory and its content. */
+ public static void recursiveDelete(File fileOrDirectory) {
+ File[] directoryFiles = fileOrDirectory.listFiles();
+ if (directoryFiles != null) {
+ for (File child : directoryFiles) {
+ recursiveDelete(child);
+ }
+ }
+ fileOrDirectory.delete();
+ }
+
+ /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */
+ public static File createTempDirectory(Context context, String prefix) throws IOException {
+ File tempFile = createTempFile(context, prefix);
+ tempFile.delete(); // Delete the temp file.
+ tempFile.mkdir(); // Create a directory with the same name.
+ return tempFile;
+ }
+
+ /** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */
+ public static File createTempFile(Context context, String prefix) throws IOException {
+ return File.createTempFile(prefix, null, context.getCacheDir());
+ }
+
+ /**
+ * Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit
+ * first" order.
+ *
+ * @param bytes Array containing the bytes to update the crc value with.
+ * @param start The index to the first byte in the byte range to update the crc with.
+ * @param end The index after the last byte in the byte range to update the crc with.
+ * @param initialValue The initial value for the crc calculation.
+ * @return The result of updating the initial value with the specified bytes.
+ */
+ public static int crc32(byte[] bytes, int start, int end, int initialValue) {
+ for (int i = start; i < end; i++) {
+ initialValue = (initialValue << 8)
+ ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF];
+ }
+ return initialValue;
+ }
+
+ /**
+ * Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit
+ * first" order.
+ *
+ * @param bytes Array containing the bytes to update the crc value with.
+ * @param start The index to the first byte in the byte range to update the crc with.
+ * @param end The index after the last byte in the byte range to update the crc with.
+ * @param initialValue The initial value for the crc calculation.
+ * @return The result of updating the initial value with the specified bytes.
+ */
+ public static int crc8(byte[] bytes, int start, int end, int initialValue) {
+ for (int i = start; i < end; i++) {
+ initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)];
+ }
+ return initialValue;
+ }
+
+ /**
+ * Returns the {@link C.NetworkType} of the current network connection.
+ *
+ * @param context A context to access the connectivity manager.
+ * @return The {@link C.NetworkType} of the current network connection.
+ */
+ @C.NetworkType
+ public static int getNetworkType(Context context) {
+ if (context == null) {
+ // Note: This is for backward compatibility only (context used to be @Nullable).
+ return C.NETWORK_TYPE_UNKNOWN;
+ }
+ NetworkInfo networkInfo;
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivityManager == null) {
+ return C.NETWORK_TYPE_UNKNOWN;
+ }
+ try {
+ networkInfo = connectivityManager.getActiveNetworkInfo();
+ } catch (SecurityException e) {
+ // Expected if permission was revoked.
+ return C.NETWORK_TYPE_UNKNOWN;
+ }
+ if (networkInfo == null || !networkInfo.isConnected()) {
+ return C.NETWORK_TYPE_OFFLINE;
+ }
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ return C.NETWORK_TYPE_WIFI;
+ case ConnectivityManager.TYPE_WIMAX:
+ return C.NETWORK_TYPE_4G;
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_MOBILE_DUN:
+ case ConnectivityManager.TYPE_MOBILE_HIPRI:
+ return getMobileNetworkType(networkInfo);
+ case ConnectivityManager.TYPE_ETHERNET:
+ return C.NETWORK_TYPE_ETHERNET;
+ default: // VPN, Bluetooth, Dummy.
+ return C.NETWORK_TYPE_OTHER;
+ }
+ }
+
+ /**
+ * Returns the upper-case ISO 3166-1 alpha-2 country code of the current registered operator's MCC
+ * (Mobile Country Code), or the country code of the default Locale if not available.
+ *
+ * @param context A context to access the telephony service. If null, only the Locale can be used.
+ * @return The upper-case ISO 3166-1 alpha-2 country code, or an empty String if unavailable.
+ */
+ public static String getCountryCode(@Nullable Context context) {
+ if (context != null) {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager != null) {
+ String countryCode = telephonyManager.getNetworkCountryIso();
+ if (!TextUtils.isEmpty(countryCode)) {
+ return toUpperInvariant(countryCode);
+ }
+ }
+ }
+ return toUpperInvariant(Locale.getDefault().getCountry());
+ }
+
+ /**
+ * Returns a non-empty array of normalized IETF BCP 47 language tags for the system languages
+ * ordered by preference.
+ */
+ public static String[] getSystemLanguageCodes() {
+ String[] systemLocales = getSystemLocales();
+ for (int i = 0; i < systemLocales.length; i++) {
+ systemLocales[i] = normalizeLanguageCode(systemLocales[i]);
+ }
+ return systemLocales;
+ }
+
+ /**
+ * Uncompresses the data in {@code input}.
+ *
+ * @param input Wraps the compressed input data.
+ * @param output Wraps an output buffer to be used to store the uncompressed data. If {@code
+ * output.data} isn't big enough to hold the uncompressed data, a new array is created. If
+ * {@code true} is returned then the output's position will be set to 0 and its limit will be
+ * set to the length of the uncompressed data.
+ * @param inflater If not null, used to uncompressed the input. Otherwise a new {@link Inflater}
+ * is created.
+ * @return Whether the input is uncompressed successfully.
+ */
+ public static boolean inflate(
+ ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) {
+ if (input.bytesLeft() <= 0) {
+ return false;
+ }
+ byte[] outputData = output.data;
+ if (outputData.length < input.bytesLeft()) {
+ outputData = new byte[2 * input.bytesLeft()];
+ }
+ if (inflater == null) {
+ inflater = new Inflater();
+ }
+ inflater.setInput(input.data, input.getPosition(), input.bytesLeft());
+ try {
+ int outputSize = 0;
+ while (true) {
+ outputSize += inflater.inflate(outputData, outputSize, outputData.length - outputSize);
+ if (inflater.finished()) {
+ output.reset(outputData, outputSize);
+ return true;
+ }
+ if (inflater.needsDictionary() || inflater.needsInput()) {
+ return false;
+ }
+ if (outputSize == outputData.length) {
+ outputData = Arrays.copyOf(outputData, outputData.length * 2);
+ }
+ }
+ } catch (DataFormatException e) {
+ return false;
+ } finally {
+ inflater.reset();
+ }
+ }
+
+ /**
+ * Returns whether the app is running on a TV device.
+ *
+ * @param context Any context.
+ * @return Whether the app is running on a TV device.
+ */
+ public static boolean isTv(Context context) {
+ // See https://developer.android.com/training/tv/start/hardware.html#runtime-check.
+ UiModeManager uiModeManager =
+ (UiModeManager) context.getApplicationContext().getSystemService(UI_MODE_SERVICE);
+ return uiModeManager != null
+ && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+ }
+
+ /**
+ * Gets the size of the current mode of the default display, in pixels.
+ *
+ * <p>Note that due to application UI scaling, the number of pixels made available to applications
+ * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as
+ * reported by this function). For example, applications running on a display configured with a 4K
+ * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take
+ * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers.
+ *
+ * @param context Any context.
+ * @return The size of the current mode, in pixels.
+ */
+ public static Point getCurrentDisplayModeSize(Context context) {
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ return getCurrentDisplayModeSize(context, windowManager.getDefaultDisplay());
+ }
+
+ /**
+ * Gets the size of the current mode of the specified display, in pixels.
+ *
+ * <p>Note that due to application UI scaling, the number of pixels made available to applications
+ * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as
+ * reported by this function). For example, applications running on a display configured with a 4K
+ * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take
+ * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers.
+ *
+ * @param context Any context.
+ * @param display The display whose size is to be returned.
+ * @return The size of the current mode, in pixels.
+ */
+ public static Point getCurrentDisplayModeSize(Context context, Display display) {
+ if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) {
+ // On Android TVs it is common for the UI to be configured for a lower resolution than
+ // SurfaceViews can output. Before API 26 the Display object does not provide a way to
+ // identify this case, and up to and including API 28 many devices still do not correctly set
+ // their hardware compositor output size.
+
+ // Sony Android TVs advertise support for 4k output via a system feature.
+ if ("Sony".equals(Util.MANUFACTURER)
+ && Util.MODEL.startsWith("BRAVIA")
+ && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
+ return new Point(3840, 2160);
+ }
+
+ // Otherwise check the system property for display size. From API 28 treble may prevent the
+ // system from writing sys.display-size so we check vendor.display-size instead.
+ String displaySize =
+ Util.SDK_INT < 28
+ ? getSystemProperty("sys.display-size")
+ : getSystemProperty("vendor.display-size");
+ // If we managed to read the display size, attempt to parse it.
+ if (!TextUtils.isEmpty(displaySize)) {
+ try {
+ String[] displaySizeParts = split(displaySize.trim(), "x");
+ if (displaySizeParts.length == 2) {
+ int width = Integer.parseInt(displaySizeParts[0]);
+ int height = Integer.parseInt(displaySizeParts[1]);
+ if (width > 0 && height > 0) {
+ return new Point(width, height);
+ }
+ }
+ } catch (NumberFormatException e) {
+ // Do nothing.
+ }
+ Log.e(TAG, "Invalid display size: " + displaySize);
+ }
+ }
+
+ Point displaySize = new Point();
+ if (Util.SDK_INT >= 23) {
+ getDisplaySizeV23(display, displaySize);
+ } else if (Util.SDK_INT >= 17) {
+ getDisplaySizeV17(display, displaySize);
+ } else {
+ getDisplaySizeV16(display, displaySize);
+ }
+ return displaySize;
+ }
+
+ /**
+ * Extract renderer capabilities for the renderers created by the provided renderers factory.
+ *
+ * @param renderersFactory A {@link RenderersFactory}.
+ * @return The {@link RendererCapabilities} for each renderer created by the {@code
+ * renderersFactory}.
+ */
+ public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) {
+ Renderer[] renderers =
+ renderersFactory.createRenderers(
+ new Handler(),
+ new VideoRendererEventListener() {},
+ new AudioRendererEventListener() {},
+ (cues) -> {},
+ (metadata) -> {},
+ /* drmSessionManager= */ null);
+ RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length];
+ for (int i = 0; i < renderers.length; i++) {
+ capabilities[i] = renderers[i].getCapabilities();
+ }
+ return capabilities;
+ }
+
+ /**
+ * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}.
+ *
+ * @param trackType A {@code TRACK_TYPE_*} constant,
+ * @return A string representation of this constant.
+ */
+ public static String getTrackTypeString(int trackType) {
+ switch (trackType) {
+ case C.TRACK_TYPE_AUDIO:
+ return "audio";
+ case C.TRACK_TYPE_DEFAULT:
+ return "default";
+ case C.TRACK_TYPE_METADATA:
+ return "metadata";
+ case C.TRACK_TYPE_CAMERA_MOTION:
+ return "camera motion";
+ case C.TRACK_TYPE_NONE:
+ return "none";
+ case C.TRACK_TYPE_TEXT:
+ return "text";
+ case C.TRACK_TYPE_VIDEO:
+ return "video";
+ default:
+ return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?";
+ }
+ }
+
+ @Nullable
+ private static String getSystemProperty(String name) {
+ try {
+ @SuppressLint("PrivateApi")
+ Class<?> systemProperties = Class.forName("android.os.SystemProperties");
+ Method getMethod = systemProperties.getMethod("get", String.class);
+ return (String) getMethod.invoke(systemProperties, name);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to read system property " + name, e);
+ return null;
+ }
+ }
+
+ @TargetApi(23)
+ private static void getDisplaySizeV23(Display display, Point outSize) {
+ Display.Mode mode = display.getMode();
+ outSize.x = mode.getPhysicalWidth();
+ outSize.y = mode.getPhysicalHeight();
+ }
+
+ @TargetApi(17)
+ private static void getDisplaySizeV17(Display display, Point outSize) {
+ display.getRealSize(outSize);
+ }
+
+ private static void getDisplaySizeV16(Display display, Point outSize) {
+ display.getSize(outSize);
+ }
+
+ private static String[] getSystemLocales() {
+ Configuration config = Resources.getSystem().getConfiguration();
+ return SDK_INT >= 24
+ ? getSystemLocalesV24(config)
+ : new String[] {getLocaleLanguageTag(config.locale)};
+ }
+
+ @TargetApi(24)
+ private static String[] getSystemLocalesV24(Configuration config) {
+ return Util.split(config.getLocales().toLanguageTags(), ",");
+ }
+
+ @TargetApi(21)
+ private static String getLocaleLanguageTagV21(Locale locale) {
+ return locale.toLanguageTag();
+ }
+
+ private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) {
+ switch (networkInfo.getSubtype()) {
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ return C.NETWORK_TYPE_2G;
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
+ return C.NETWORK_TYPE_3G;
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return C.NETWORK_TYPE_4G;
+ case TelephonyManager.NETWORK_TYPE_NR:
+ return C.NETWORK_TYPE_5G;
+ case TelephonyManager.NETWORK_TYPE_IWLAN:
+ return C.NETWORK_TYPE_WIFI;
+ case TelephonyManager.NETWORK_TYPE_GSM:
+ case TelephonyManager.NETWORK_TYPE_UNKNOWN:
+ default: // Future mobile network types.
+ return C.NETWORK_TYPE_CELLULAR_UNKNOWN;
+ }
+ }
+
+ private static HashMap<String, String> createIsoLanguageReplacementMap() {
+ String[] iso2Languages = Locale.getISOLanguages();
+ HashMap<String, String> replacedLanguages =
+ new HashMap<>(
+ /* initialCapacity= */ iso2Languages.length + additionalIsoLanguageReplacements.length);
+ for (String iso2 : iso2Languages) {
+ try {
+ // This returns the ISO 639-2/T code for the language.
+ String iso3 = new Locale(iso2).getISO3Language();
+ if (!TextUtils.isEmpty(iso3)) {
+ replacedLanguages.put(iso3, iso2);
+ }
+ } catch (MissingResourceException e) {
+ // Shouldn't happen for list of known languages, but we don't want to throw either.
+ }
+ }
+ // Add additional replacement mappings.
+ for (int i = 0; i < additionalIsoLanguageReplacements.length; i += 2) {
+ replacedLanguages.put(
+ additionalIsoLanguageReplacements[i], additionalIsoLanguageReplacements[i + 1]);
+ }
+ return replacedLanguages;
+ }
+
+ private static String maybeReplaceGrandfatheredLanguageTags(String languageTag) {
+ for (int i = 0; i < isoGrandfatheredTagReplacements.length; i += 2) {
+ if (languageTag.startsWith(isoGrandfatheredTagReplacements[i])) {
+ return isoGrandfatheredTagReplacements[i + 1]
+ + languageTag.substring(/* beginIndex= */ isoGrandfatheredTagReplacements[i].length());
+ }
+ }
+ return languageTag;
+ }
+
+ // Additional mapping from ISO3 to ISO2 language codes.
+ private static final String[] additionalIsoLanguageReplacements =
+ new String[] {
+ // Bibliographical codes defined in ISO 639-2/B, replaced by terminological code defined in
+ // ISO 639-2/T. See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes.
+ "alb", "sq",
+ "arm", "hy",
+ "baq", "eu",
+ "bur", "my",
+ "tib", "bo",
+ "chi", "zh",
+ "cze", "cs",
+ "dut", "nl",
+ "ger", "de",
+ "gre", "el",
+ "fre", "fr",
+ "geo", "ka",
+ "ice", "is",
+ "mac", "mk",
+ "mao", "mi",
+ "may", "ms",
+ "per", "fa",
+ "rum", "ro",
+ "scc", "hbs-srp",
+ "slo", "sk",
+ "wel", "cy",
+ // Deprecated 2-letter codes, replaced by modern equivalent (including macrolanguage)
+ // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988"
+ "id", "ms-ind",
+ "iw", "he",
+ "heb", "he",
+ "ji", "yi",
+ // Individual macrolanguage codes mapped back to full macrolanguage code.
+ // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage
+ "in", "ms-ind",
+ "ind", "ms-ind",
+ "nb", "no-nob",
+ "nob", "no-nob",
+ "nn", "no-nno",
+ "nno", "no-nno",
+ "tw", "ak-twi",
+ "twi", "ak-twi",
+ "bs", "hbs-bos",
+ "bos", "hbs-bos",
+ "hr", "hbs-hrv",
+ "hrv", "hbs-hrv",
+ "sr", "hbs-srp",
+ "srp", "hbs-srp",
+ "cmn", "zh-cmn",
+ "hak", "zh-hak",
+ "nan", "zh-nan",
+ "hsn", "zh-hsn"
+ };
+
+ // "Grandfathered tags", replaced by modern equivalents (including macrolanguage)
+ // See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry.
+ private static final String[] isoGrandfatheredTagReplacements =
+ new String[] {
+ "i-lux", "lb",
+ "i-hak", "zh-hak",
+ "i-navajo", "nv",
+ "no-bok", "no-nob",
+ "no-nyn", "no-nno",
+ "zh-guoyu", "zh-cmn",
+ "zh-hakka", "zh-hak",
+ "zh-min-nan", "zh-nan",
+ "zh-xiang", "zh-hsn"
+ };
+
+ /**
+ * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit in the order "most
+ * significant bit first".
+ */
+ private static final int[] CRC32_BYTES_MSBF = {
+ 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2,
+ 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3,
+ 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC,
+ 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011,
+ 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E,
+ 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF,
+ 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90,
+ 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95,
+ 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A,
+ 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C,
+ 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13,
+ 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE,
+ 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1,
+ 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20,
+ 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F,
+ 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A,
+ 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055,
+ 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34,
+ 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632,
+ 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F,
+ 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0,
+ 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91,
+ 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E,
+ 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B,
+ 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604,
+ 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615,
+ 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A,
+ 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640,
+ 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F,
+ 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E,
+ 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651,
+ 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654,
+ 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB,
+ 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA,
+ 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5,
+ 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668,
+ 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4
+ };
+
+ /**
+ * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit in the order "most
+ * significant bit first".
+ */
+ private static final int[] CRC8_BYTES_MSBF = {
+ 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A,
+ 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53,
+ 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4,
+ 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1,
+ 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1,
+ 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88,
+ 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F,
+ 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
+ 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B,
+ 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2,
+ 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75,
+ 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10,
+ 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40,
+ 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39,
+ 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE,
+ 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
+ 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4,
+ 0xF3
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java
new file mode 100644
index 0000000000..7b56886dba
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util;
+
+import androidx.annotation.Nullable;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * {@link XmlPullParser} utility methods.
+ */
+public final class XmlPullParserUtil {
+
+ private XmlPullParserUtil() {}
+
+ /**
+ * Returns whether the current event is an end tag with the specified name.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param name The specified name.
+ * @return Whether the current event is an end tag with the specified name.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return isEndTag(xpp) && xpp.getName().equals(name);
+ }
+
+ /**
+ * Returns whether the current event is an end tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @return Whether the current event is an end tag.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.END_TAG;
+ }
+
+ /**
+ * Returns whether the current event is a start tag with the specified name.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param name The specified name.
+ * @return Whether the current event is a start tag with the specified name.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+ return isStartTag(xpp) && xpp.getName().equals(name);
+ }
+
+ /**
+ * Returns whether the current event is a start tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @return Whether the current event is a start tag.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
+ return xpp.getEventType() == XmlPullParser.START_TAG;
+ }
+
+ /**
+ * Returns whether the current event is a start tag with the specified name. If the current event
+ * has a raw name then its prefix is stripped before matching.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param name The specified name.
+ * @return Whether the current event is a start tag with the specified name.
+ * @throws XmlPullParserException If an error occurs querying the parser.
+ */
+ public static boolean isStartTagIgnorePrefix(XmlPullParser xpp, String name)
+ throws XmlPullParserException {
+ return isStartTag(xpp) && stripPrefix(xpp.getName()).equals(name);
+ }
+
+ /**
+ * Returns the value of an attribute of the current start tag.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param attributeName The name of the attribute.
+ * @return The value of the attribute, or null if the current event is not a start tag or if no
+ * such attribute was found.
+ */
+ public static @Nullable String getAttributeValue(XmlPullParser xpp, String attributeName) {
+ int attributeCount = xpp.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ if (xpp.getAttributeName(i).equals(attributeName)) {
+ return xpp.getAttributeValue(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the value of an attribute of the current start tag. Any raw attribute names in the
+ * current start tag have their prefixes stripped before matching.
+ *
+ * @param xpp The {@link XmlPullParser} to query.
+ * @param attributeName The name of the attribute.
+ * @return The value of the attribute, or null if the current event is not a start tag or if no
+ * such attribute was found.
+ */
+ public static @Nullable String getAttributeValueIgnorePrefix(
+ XmlPullParser xpp, String attributeName) {
+ int attributeCount = xpp.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ if (stripPrefix(xpp.getAttributeName(i)).equals(attributeName)) {
+ return xpp.getAttributeValue(i);
+ }
+ }
+ return null;
+ }
+
+ private static String stripPrefix(String name) {
+ int prefixSeparatorIndex = name.indexOf(':');
+ return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java
new file mode 100644
index 0000000000..49ee4a4d4d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util/package-info.java
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.util;