diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/util')
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} < 0, {@code toIndex} > {@code + * list.size()}, or {@code fromIndex} > {@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 ≥ 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 < right, or a positive value if left + * > 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 (> 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: + * <, >, :, ", /, \, |, ?, 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; |