summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java171
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java33
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java94
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java36
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java30
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java236
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java202
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java73
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java144
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java99
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java243
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java107
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java108
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java83
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java145
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java127
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java101
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java112
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java842
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java44
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java94
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java96
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java96
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java85
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java37
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java102
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java254
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java47
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java270
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java93
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java19
40 files changed, 4677 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java
new file mode 100644
index 0000000000..16f01c4627
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java
@@ -0,0 +1,171 @@
+/*
+ * 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.metadata;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A collection of metadata entries.
+ */
+public final class Metadata implements Parcelable {
+
+ /** A metadata entry. */
+ public interface Entry extends Parcelable {
+
+ /**
+ * Returns the {@link Format} that can be used to decode the wrapped metadata in {@link
+ * #getWrappedMetadataBytes()}, or null if this Entry doesn't contain wrapped metadata.
+ */
+ @Nullable
+ default Format getWrappedMetadataFormat() {
+ return null;
+ }
+
+ /**
+ * Returns the bytes of the wrapped metadata in this Entry, or null if it doesn't contain
+ * wrapped metadata.
+ */
+ @Nullable
+ default byte[] getWrappedMetadataBytes() {
+ return null;
+ }
+ }
+
+ private final Entry[] entries;
+
+ /**
+ * @param entries The metadata entries.
+ */
+ public Metadata(Entry... entries) {
+ this.entries = entries;
+ }
+
+ /**
+ * @param entries The metadata entries.
+ */
+ public Metadata(List<? extends Entry> entries) {
+ this.entries = new Entry[entries.size()];
+ entries.toArray(this.entries);
+ }
+
+ /* package */ Metadata(Parcel in) {
+ entries = new Metadata.Entry[in.readInt()];
+ for (int i = 0; i < entries.length; i++) {
+ entries[i] = in.readParcelable(Entry.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the number of metadata entries.
+ */
+ public int length() {
+ return entries.length;
+ }
+
+ /**
+ * Returns the entry at the specified index.
+ *
+ * @param index The index of the entry.
+ * @return The entry at the specified index.
+ */
+ public Metadata.Entry get(int index) {
+ return entries[index];
+ }
+
+ /**
+ * Returns a copy of this metadata with the entries of the specified metadata appended. Returns
+ * this instance if {@code other} is null.
+ *
+ * @param other The metadata that holds the entries to append. If null, this methods returns this
+ * instance.
+ * @return The metadata instance with the appended entries.
+ */
+ public Metadata copyWithAppendedEntriesFrom(@Nullable Metadata other) {
+ if (other == null) {
+ return this;
+ }
+ return copyWithAppendedEntries(other.entries);
+ }
+
+ /**
+ * Returns a copy of this metadata with the specified entries appended.
+ *
+ * @param entriesToAppend The entries to append.
+ * @return The metadata instance with the appended entries.
+ */
+ public Metadata copyWithAppendedEntries(Entry... entriesToAppend) {
+ if (entriesToAppend.length == 0) {
+ return this;
+ }
+ return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ Metadata other = (Metadata) obj;
+ return Arrays.equals(entries, other.entries);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(entries);
+ }
+
+ @Override
+ public String toString() {
+ return "entries=" + Arrays.toString(entries);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(entries.length);
+ for (Entry entry : entries) {
+ dest.writeParcelable(entry, 0);
+ }
+ }
+
+ public static final Parcelable.Creator<Metadata> CREATOR =
+ new Parcelable.Creator<Metadata>() {
+ @Override
+ public Metadata createFromParcel(Parcel in) {
+ return new Metadata(in);
+ }
+
+ @Override
+ public Metadata[] newArray(int size) {
+ return new Metadata[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java
new file mode 100644
index 0000000000..1bc1c7dc06
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.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.metadata;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Decodes metadata from binary data.
+ */
+public interface MetadataDecoder {
+
+ /**
+ * Decodes a {@link Metadata} element from the provided input buffer.
+ *
+ * @param inputBuffer The input buffer to decode.
+ * @return The decoded metadata object, or null if the metadata could not be decoded.
+ */
+ @Nullable
+ Metadata decode(MetadataInputBuffer inputBuffer);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
new file mode 100644
index 0000000000..30f6aad4a9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
@@ -0,0 +1,94 @@
+/*
+ * 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.metadata;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.icy.IcyDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * A factory for {@link MetadataDecoder} instances.
+ */
+public interface MetadataDecoderFactory {
+
+ /**
+ * Returns whether the factory is able to instantiate a {@link MetadataDecoder} for the given
+ * {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return Whether the factory can instantiate a suitable {@link MetadataDecoder}.
+ */
+ boolean supportsFormat(Format format);
+
+ /**
+ * Creates a {@link MetadataDecoder} for the given {@link Format}.
+ *
+ * @param format The {@link Format}.
+ * @return A new {@link MetadataDecoder}.
+ * @throws IllegalArgumentException If the {@link Format} is not supported.
+ */
+ MetadataDecoder createDecoder(Format format);
+
+ /**
+ * Default {@link MetadataDecoder} implementation.
+ *
+ * <p>The formats supported by this factory are:
+ *
+ * <ul>
+ * <li>ID3 ({@link Id3Decoder})
+ * <li>EMSG ({@link EventMessageDecoder})
+ * <li>SCTE-35 ({@link SpliceInfoDecoder})
+ * <li>ICY ({@link IcyDecoder})
+ * </ul>
+ */
+ MetadataDecoderFactory DEFAULT =
+ new MetadataDecoderFactory() {
+
+ @Override
+ public boolean supportsFormat(Format format) {
+ @Nullable String mimeType = format.sampleMimeType;
+ return MimeTypes.APPLICATION_ID3.equals(mimeType)
+ || MimeTypes.APPLICATION_EMSG.equals(mimeType)
+ || MimeTypes.APPLICATION_SCTE35.equals(mimeType)
+ || MimeTypes.APPLICATION_ICY.equals(mimeType);
+ }
+
+ @Override
+ public MetadataDecoder createDecoder(Format format) {
+ @Nullable String mimeType = format.sampleMimeType;
+ if (mimeType != null) {
+ switch (mimeType) {
+ case MimeTypes.APPLICATION_ID3:
+ return new Id3Decoder();
+ case MimeTypes.APPLICATION_EMSG:
+ return new EventMessageDecoder();
+ case MimeTypes.APPLICATION_SCTE35:
+ return new SpliceInfoDecoder();
+ case MimeTypes.APPLICATION_ICY:
+ return new IcyDecoder();
+ default:
+ break;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Attempted to create decoder for unsupported MIME type: " + mimeType);
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
new file mode 100644
index 0000000000..9a265744ec
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
@@ -0,0 +1,36 @@
+/*
+ * 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.metadata;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+
+/**
+ * A {@link DecoderInputBuffer} for a {@link MetadataDecoder}.
+ */
+public final class MetadataInputBuffer extends DecoderInputBuffer {
+
+ /**
+ * An offset that must be added to the metadata's timestamps after it's been decoded, or
+ * {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.
+ */
+ public long subsampleOffsetUs;
+
+ public MetadataInputBuffer() {
+ super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java
new file mode 100644
index 0000000000..025f9f01bc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataOutput.java
@@ -0,0 +1,30 @@
+/*
+ * 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.metadata;
+
+/**
+ * Receives metadata output.
+ */
+public interface MetadataOutput {
+
+ /**
+ * Called when there is metadata associated with current playback time.
+ *
+ * @param metadata The metadata.
+ */
+ void onMetadata(Metadata metadata);
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java
new file mode 100644
index 0000000000..329f9ffa7d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java
@@ -0,0 +1,236 @@
+/*
+ * 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.metadata;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer;
+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.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A renderer for metadata.
+ */
+public final class MetadataRenderer extends BaseRenderer implements Callback {
+
+ private static final int MSG_INVOKE_RENDERER = 0;
+ // TODO: Holding multiple pending metadata objects is temporary mitigation against
+ // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been
+ // addressed.
+ private static final int MAX_PENDING_METADATA_COUNT = 5;
+
+ private final MetadataDecoderFactory decoderFactory;
+ private final MetadataOutput output;
+ @Nullable private final Handler outputHandler;
+ private final MetadataInputBuffer buffer;
+ private final @NullableType Metadata[] pendingMetadata;
+ private final long[] pendingMetadataTimestamps;
+
+ private int pendingMetadataIndex;
+ private int pendingMetadataCount;
+ @Nullable private MetadataDecoder decoder;
+ private boolean inputStreamEnded;
+ private long subsampleOffsetUs;
+
+ /**
+ * @param output The output.
+ * @param outputLooper The looper associated with the thread on which the output should be called.
+ * If the output makes use of standard Android UI components, then this should normally be the
+ * looper associated with the application's main thread, which can be obtained using {@link
+ * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
+ * directly on the player's internal rendering thread.
+ */
+ public MetadataRenderer(MetadataOutput output, @Nullable Looper outputLooper) {
+ this(output, outputLooper, MetadataDecoderFactory.DEFAULT);
+ }
+
+ /**
+ * @param output The output.
+ * @param outputLooper The looper associated with the thread on which the output should be called.
+ * If the output makes use of standard Android UI components, then this should normally be the
+ * looper associated with the application's main thread, which can be obtained using {@link
+ * android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
+ * directly on the player's internal rendering thread.
+ * @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
+ */
+ public MetadataRenderer(
+ MetadataOutput output, @Nullable Looper outputLooper, MetadataDecoderFactory decoderFactory) {
+ super(C.TRACK_TYPE_METADATA);
+ this.output = Assertions.checkNotNull(output);
+ this.outputHandler =
+ outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
+ this.decoderFactory = Assertions.checkNotNull(decoderFactory);
+ buffer = new MetadataInputBuffer();
+ pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT];
+ pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT];
+ }
+
+ @Override
+ @Capabilities
+ public int supportsFormat(Format format) {
+ if (decoderFactory.supportsFormat(format)) {
+ return RendererCapabilities.create(
+ supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM);
+ } else {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ }
+ }
+
+ @Override
+ protected void onStreamChanged(Format[] formats, long offsetUs) {
+ decoder = decoderFactory.createDecoder(formats[0]);
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) {
+ flushPendingMetadata();
+ inputStreamEnded = false;
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) {
+ if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) {
+ buffer.clear();
+ FormatHolder formatHolder = getFormatHolder();
+ int result = readSource(formatHolder, buffer, false);
+ if (result == C.RESULT_BUFFER_READ) {
+ if (buffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ } else if (buffer.isDecodeOnly()) {
+ // Do nothing. Note this assumes that all metadata buffers can be decoded independently.
+ // If we ever need to support a metadata format where this is not the case, we'll need to
+ // pass the buffer to the decoder and discard the output.
+ } else {
+ buffer.subsampleOffsetUs = subsampleOffsetUs;
+ buffer.flip();
+ @Nullable Metadata metadata = castNonNull(decoder).decode(buffer);
+ if (metadata != null) {
+ List<Metadata.Entry> entries = new ArrayList<>(metadata.length());
+ decodeWrappedMetadata(metadata, entries);
+ if (!entries.isEmpty()) {
+ Metadata expandedMetadata = new Metadata(entries);
+ int index =
+ (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;
+ pendingMetadata[index] = expandedMetadata;
+ pendingMetadataTimestamps[index] = buffer.timeUs;
+ pendingMetadataCount++;
+ }
+ }
+ }
+ } else if (result == C.RESULT_FORMAT_READ) {
+ subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs;
+ }
+ }
+
+ if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) {
+ Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]);
+ invokeRenderer(metadata);
+ pendingMetadata[pendingMetadataIndex] = null;
+ pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT;
+ pendingMetadataCount--;
+ }
+ }
+
+ /**
+ * Iterates through {@code metadata.entries} and checks each one to see if contains wrapped
+ * metadata. If it does, then we recursively decode the wrapped metadata. If it doesn't (recursion
+ * base-case), we add the {@link Metadata.Entry} to {@code decodedEntries} (output parameter).
+ */
+ private void decodeWrappedMetadata(Metadata metadata, List<Metadata.Entry> decodedEntries) {
+ for (int i = 0; i < metadata.length(); i++) {
+ @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat();
+ if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) {
+ MetadataDecoder wrappedMetadataDecoder =
+ decoderFactory.createDecoder(wrappedMetadataFormat);
+ // wrappedMetadataFormat != null so wrappedMetadataBytes must be non-null too.
+ byte[] wrappedMetadataBytes =
+ Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes());
+ buffer.clear();
+ buffer.ensureSpaceForWrite(wrappedMetadataBytes.length);
+ castNonNull(buffer.data).put(wrappedMetadataBytes);
+ buffer.flip();
+ @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer);
+ if (innerMetadata != null) {
+ // The decoding succeeded, so we'll try another level of unwrapping.
+ decodeWrappedMetadata(innerMetadata, decodedEntries);
+ }
+ } else {
+ // Entry doesn't contain any wrapped metadata, so output it directly.
+ decodedEntries.add(metadata.get(i));
+ }
+ }
+ }
+
+ @Override
+ protected void onDisabled() {
+ flushPendingMetadata();
+ decoder = null;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return inputStreamEnded;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ private void invokeRenderer(Metadata metadata) {
+ if (outputHandler != null) {
+ outputHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget();
+ } else {
+ invokeRendererInternal(metadata);
+ }
+ }
+
+ private void flushPendingMetadata() {
+ Arrays.fill(pendingMetadata, null);
+ pendingMetadataIndex = 0;
+ pendingMetadataCount = 0;
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_INVOKE_RENDERER:
+ invokeRendererInternal((Metadata) msg.obj);
+ return true;
+ default:
+ // Should never happen.
+ throw new IllegalStateException();
+ }
+ }
+
+ private void invokeRendererInternal(Metadata metadata) {
+ output.onMetadata(metadata);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
new file mode 100644
index 0000000000..01aac27a27
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -0,0 +1,202 @@
+/*
+ * 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.metadata.emsg;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+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.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/** An Event Message (emsg) as defined in ISO 23009-1. */
+public final class EventMessage implements Metadata.Entry {
+
+ /**
+ * emsg scheme_id_uri from the <a href="https://aomediacodec.github.io/av1-id3/#semantics">CMAF
+ * spec</a>.
+ */
+ @VisibleForTesting public static final String ID3_SCHEME_ID_AOM = "https://aomedia.org/emsg/ID3";
+
+ /**
+ * The Apple-hosted scheme_id equivalent to {@code ID3_SCHEME_ID_AOM} - used before AOM adoption.
+ */
+ private static final String ID3_SCHEME_ID_APPLE =
+ "https://developer.apple.com/streaming/emsg-id3";
+
+ /**
+ * scheme_id_uri from section 7.3.2 of <a
+ * href="https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%20214-3%202015.pdf">SCTE 214-3
+ * 2015</a>.
+ */
+ @VisibleForTesting public static final String SCTE35_SCHEME_ID = "urn:scte:scte35:2014:bin";
+
+ private static final Format ID3_FORMAT =
+ Format.createSampleFormat(
+ /* id= */ null, MimeTypes.APPLICATION_ID3, Format.OFFSET_SAMPLE_RELATIVE);
+ private static final Format SCTE35_FORMAT =
+ Format.createSampleFormat(
+ /* id= */ null, MimeTypes.APPLICATION_SCTE35, Format.OFFSET_SAMPLE_RELATIVE);
+
+ /** The message scheme. */
+ public final String schemeIdUri;
+
+ /**
+ * The value for the event.
+ */
+ public final String value;
+
+ /**
+ * The duration of the event in milliseconds.
+ */
+ public final long durationMs;
+
+ /**
+ * The instance identifier.
+ */
+ public final long id;
+
+ /**
+ * The body of the message.
+ */
+ public final byte[] messageData;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param schemeIdUri The message scheme.
+ * @param value The value for the event.
+ * @param durationMs The duration of the event in milliseconds.
+ * @param id The instance identifier.
+ * @param messageData The body of the message.
+ */
+ public EventMessage(
+ String schemeIdUri, String value, long durationMs, long id, byte[] messageData) {
+ this.schemeIdUri = schemeIdUri;
+ this.value = value;
+ this.durationMs = durationMs;
+ this.id = id;
+ this.messageData = messageData;
+ }
+
+ /* package */ EventMessage(Parcel in) {
+ schemeIdUri = castNonNull(in.readString());
+ value = castNonNull(in.readString());
+ durationMs = in.readLong();
+ id = in.readLong();
+ messageData = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ @Nullable
+ public Format getWrappedMetadataFormat() {
+ switch (schemeIdUri) {
+ case ID3_SCHEME_ID_AOM:
+ case ID3_SCHEME_ID_APPLE:
+ return ID3_FORMAT;
+ case SCTE35_SCHEME_ID:
+ return SCTE35_FORMAT;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public byte[] getWrappedMetadataBytes() {
+ return getWrappedMetadataFormat() != null ? messageData : null;
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ result = 31 * result + (int) (durationMs ^ (durationMs >>> 32));
+ result = 31 * result + (int) (id ^ (id >>> 32));
+ result = 31 * result + Arrays.hashCode(messageData);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ EventMessage other = (EventMessage) obj;
+ return durationMs == other.durationMs
+ && id == other.id
+ && Util.areEqual(schemeIdUri, other.schemeIdUri)
+ && Util.areEqual(value, other.value)
+ && Arrays.equals(messageData, other.messageData);
+ }
+
+ @Override
+ public String toString() {
+ return "EMSG: scheme="
+ + schemeIdUri
+ + ", id="
+ + id
+ + ", durationMs="
+ + durationMs
+ + ", value="
+ + value;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(schemeIdUri);
+ dest.writeString(value);
+ dest.writeLong(durationMs);
+ dest.writeLong(id);
+ dest.writeByteArray(messageData);
+ }
+
+ public static final Parcelable.Creator<EventMessage> CREATOR =
+ new Parcelable.Creator<EventMessage>() {
+
+ @Override
+ public EventMessage createFromParcel(Parcel in) {
+ return new EventMessage(in);
+ }
+
+ @Override
+ public EventMessage[] newArray(int size) {
+ return new EventMessage[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
new file mode 100644
index 0000000000..09b0a69395
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * 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.metadata.emsg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/** Decodes data encoded by {@link EventMessageEncoder}. */
+public final class EventMessageDecoder implements MetadataDecoder {
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+ byte[] data = buffer.array();
+ int size = buffer.limit();
+ return new Metadata(decode(new ParsableByteArray(data, size)));
+ }
+
+ public EventMessage decode(ParsableByteArray emsgData) {
+ String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString());
+ String value = Assertions.checkNotNull(emsgData.readNullTerminatedString());
+ long durationMs = emsgData.readUnsignedInt();
+ long id = emsgData.readUnsignedInt();
+ byte[] messageData =
+ Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit());
+ return new EventMessage(schemeIdUri, value, durationMs, id, messageData);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java
new file mode 100644
index 0000000000..261e39ae70
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java
@@ -0,0 +1,73 @@
+/*
+ * 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.metadata.emsg;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Encodes data that can be decoded by {@link EventMessageDecoder}. This class isn't thread safe.
+ */
+public final class EventMessageEncoder {
+
+ private final ByteArrayOutputStream byteArrayOutputStream;
+ private final DataOutputStream dataOutputStream;
+
+ public EventMessageEncoder() {
+ byteArrayOutputStream = new ByteArrayOutputStream(512);
+ dataOutputStream = new DataOutputStream(byteArrayOutputStream);
+ }
+
+ /**
+ * Encodes an {@link EventMessage} to a byte array that can be decoded by {@link
+ * EventMessageDecoder}.
+ *
+ * @param eventMessage The event message to be encoded.
+ * @return The serialized byte array.
+ */
+ public byte[] encode(EventMessage eventMessage) {
+ byteArrayOutputStream.reset();
+ try {
+ writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri);
+ String nonNullValue = eventMessage.value != null ? eventMessage.value : "";
+ writeNullTerminatedString(dataOutputStream, nonNullValue);
+ writeUnsignedInt(dataOutputStream, eventMessage.durationMs);
+ writeUnsignedInt(dataOutputStream, eventMessage.id);
+ dataOutputStream.write(eventMessage.messageData);
+ dataOutputStream.flush();
+ return byteArrayOutputStream.toByteArray();
+ } catch (IOException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void writeNullTerminatedString(DataOutputStream dataOutputStream, String value)
+ throws IOException {
+ dataOutputStream.writeBytes(value);
+ dataOutputStream.writeByte(0);
+ }
+
+ private static void writeUnsignedInt(DataOutputStream outputStream, long value)
+ throws IOException {
+ outputStream.writeByte((int) (value >>> 24) & 0xFF);
+ outputStream.writeByte((int) (value >>> 16) & 0xFF);
+ outputStream.writeByte((int) (value >>> 8) & 0xFF);
+ outputStream.writeByte((int) value & 0xFF);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java
new file mode 100644
index 0000000000..3e54b59a8c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/emsg/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.metadata.emsg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java
new file mode 100644
index 0000000000..8a7ffbd976
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/PictureFrame.java
@@ -0,0 +1,144 @@
+/*
+ * 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.metadata.flac;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import java.util.Arrays;
+
+/** A picture parsed from a FLAC file. */
+public final class PictureFrame implements Metadata.Entry {
+
+ /** The type of the picture. */
+ public final int pictureType;
+ /** The mime type of the picture. */
+ public final String mimeType;
+ /** A description of the picture. */
+ public final String description;
+ /** The width of the picture in pixels. */
+ public final int width;
+ /** The height of the picture in pixels. */
+ public final int height;
+ /** The color depth of the picture in bits-per-pixel. */
+ public final int depth;
+ /** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */
+ public final int colors;
+ /** The encoded picture data. */
+ public final byte[] pictureData;
+
+ public PictureFrame(
+ int pictureType,
+ String mimeType,
+ String description,
+ int width,
+ int height,
+ int depth,
+ int colors,
+ byte[] pictureData) {
+ this.pictureType = pictureType;
+ this.mimeType = mimeType;
+ this.description = description;
+ this.width = width;
+ this.height = height;
+ this.depth = depth;
+ this.colors = colors;
+ this.pictureData = pictureData;
+ }
+
+ /* package */ PictureFrame(Parcel in) {
+ this.pictureType = in.readInt();
+ this.mimeType = castNonNull(in.readString());
+ this.description = castNonNull(in.readString());
+ this.width = in.readInt();
+ this.height = in.readInt();
+ this.depth = in.readInt();
+ this.colors = in.readInt();
+ this.pictureData = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public String toString() {
+ return "Picture: mimeType=" + mimeType + ", description=" + description;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PictureFrame other = (PictureFrame) obj;
+ return (pictureType == other.pictureType)
+ && mimeType.equals(other.mimeType)
+ && description.equals(other.description)
+ && (width == other.width)
+ && (height == other.height)
+ && (depth == other.depth)
+ && (colors == other.colors)
+ && Arrays.equals(pictureData, other.pictureData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + pictureType;
+ result = 31 * result + mimeType.hashCode();
+ result = 31 * result + description.hashCode();
+ result = 31 * result + width;
+ result = 31 * result + height;
+ result = 31 * result + depth;
+ result = 31 * result + colors;
+ result = 31 * result + Arrays.hashCode(pictureData);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(pictureType);
+ dest.writeString(mimeType);
+ dest.writeString(description);
+ dest.writeInt(width);
+ dest.writeInt(height);
+ dest.writeInt(depth);
+ dest.writeInt(colors);
+ dest.writeByteArray(pictureData);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<PictureFrame> CREATOR =
+ new Parcelable.Creator<PictureFrame>() {
+
+ @Override
+ public PictureFrame createFromParcel(Parcel in) {
+ return new PictureFrame(in);
+ }
+
+ @Override
+ public PictureFrame[] newArray(int size) {
+ return new PictureFrame[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java
new file mode 100644
index 0000000000..b777582b5d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/VorbisComment.java
@@ -0,0 +1,99 @@
+/*
+ * 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.metadata.flac;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+
+/** A vorbis comment. */
+public final class VorbisComment implements Metadata.Entry {
+
+ /** The key. */
+ public final String key;
+
+ /** The value. */
+ public final String value;
+
+ /**
+ * @param key The key.
+ * @param value The value.
+ */
+ public VorbisComment(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ /* package */ VorbisComment(Parcel in) {
+ this.key = castNonNull(in.readString());
+ this.value = castNonNull(in.readString());
+ }
+
+ @Override
+ public String toString() {
+ return "VC: " + key + "=" + value;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ VorbisComment other = (VorbisComment) obj;
+ return key.equals(other.key) && value.equals(other.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + key.hashCode();
+ result = 31 * result + value.hashCode();
+ return result;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(key);
+ dest.writeString(value);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<VorbisComment> CREATOR =
+ new Parcelable.Creator<VorbisComment>() {
+
+ @Override
+ public VorbisComment createFromParcel(Parcel in) {
+ return new VorbisComment(in);
+ }
+
+ @Override
+ public VorbisComment[] newArray(int size) {
+ return new VorbisComment[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java
new file mode 100644
index 0000000000..02353ec303
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/flac/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.metadata.flac;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
new file mode 100644
index 0000000000..1d44219eda
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java
@@ -0,0 +1,101 @@
+/*
+ * 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.metadata.icy;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Decodes ICY stream information. */
+public final class IcyDecoder implements MetadataDecoder {
+
+ private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL);
+ private static final String STREAM_KEY_NAME = "streamtitle";
+ private static final String STREAM_KEY_URL = "streamurl";
+
+ private final CharsetDecoder utf8Decoder;
+ private final CharsetDecoder iso88591Decoder;
+
+ public IcyDecoder() {
+ utf8Decoder = Charset.forName(C.UTF8_NAME).newDecoder();
+ iso88591Decoder = Charset.forName(C.ISO88591_NAME).newDecoder();
+ }
+
+ @Override
+ @SuppressWarnings("ByteBufferBackingArray")
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+ @Nullable String icyString = decodeToString(buffer);
+ byte[] icyBytes = new byte[buffer.limit()];
+ buffer.get(icyBytes);
+
+ if (icyString == null) {
+ return new Metadata(new IcyInfo(icyBytes, /* title= */ null, /* url= */ null));
+ }
+
+ @Nullable String name = null;
+ @Nullable String url = null;
+ int index = 0;
+ Matcher matcher = METADATA_ELEMENT.matcher(icyString);
+ while (matcher.find(index)) {
+ @Nullable String key = Util.toLowerInvariant(matcher.group(1));
+ @Nullable String value = matcher.group(2);
+ switch (key) {
+ case STREAM_KEY_NAME:
+ name = value;
+ break;
+ case STREAM_KEY_URL:
+ url = value;
+ break;
+ }
+ index = matcher.end();
+ }
+ return new Metadata(new IcyInfo(icyBytes, name, url));
+ }
+
+ // The ICY spec doesn't specify a character encoding, and there's no way to communicate one
+ // either. So try decoding UTF-8 first, then fall back to ISO-8859-1.
+ // https://github.com/google/ExoPlayer/issues/6753
+ @Nullable
+ private String decodeToString(ByteBuffer data) {
+ try {
+ return utf8Decoder.decode(data).toString();
+ } catch (CharacterCodingException e) {
+ // Fall through to try ISO-8859-1 decoding.
+ } finally {
+ utf8Decoder.reset();
+ data.rewind();
+ }
+ try {
+ return iso88591Decoder.decode(data).toString();
+ } catch (CharacterCodingException e) {
+ return null;
+ } finally {
+ iso88591Decoder.reset();
+ data.rewind();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
new file mode 100644
index 0000000000..638e7594eb
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyHeaders.java
@@ -0,0 +1,243 @@
+/*
+ * 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.metadata.icy;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+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.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.List;
+import java.util.Map;
+
+/** ICY headers. */
+public final class IcyHeaders implements Metadata.Entry {
+
+ public static final String REQUEST_HEADER_ENABLE_METADATA_NAME = "Icy-MetaData";
+ public static final String REQUEST_HEADER_ENABLE_METADATA_VALUE = "1";
+
+ private static final String TAG = "IcyHeaders";
+
+ private static final String RESPONSE_HEADER_BITRATE = "icy-br";
+ private static final String RESPONSE_HEADER_GENRE = "icy-genre";
+ private static final String RESPONSE_HEADER_NAME = "icy-name";
+ private static final String RESPONSE_HEADER_URL = "icy-url";
+ private static final String RESPONSE_HEADER_PUB = "icy-pub";
+ private static final String RESPONSE_HEADER_METADATA_INTERVAL = "icy-metaint";
+
+ /**
+ * Parses {@link IcyHeaders} from response headers.
+ *
+ * @param responseHeaders The response headers.
+ * @return The parsed {@link IcyHeaders}, or {@code null} if no ICY headers were present.
+ */
+ @Nullable
+ public static IcyHeaders parse(Map<String, List<String>> responseHeaders) {
+ boolean icyHeadersPresent = false;
+ int bitrate = Format.NO_VALUE;
+ String genre = null;
+ String name = null;
+ String url = null;
+ boolean isPublic = false;
+ int metadataInterval = C.LENGTH_UNSET;
+
+ List<String> headers = responseHeaders.get(RESPONSE_HEADER_BITRATE);
+ if (headers != null) {
+ String bitrateHeader = headers.get(0);
+ try {
+ bitrate = Integer.parseInt(bitrateHeader) * 1000;
+ if (bitrate > 0) {
+ icyHeadersPresent = true;
+ } else {
+ Log.w(TAG, "Invalid bitrate: " + bitrateHeader);
+ bitrate = Format.NO_VALUE;
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid bitrate header: " + bitrateHeader);
+ }
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_GENRE);
+ if (headers != null) {
+ genre = headers.get(0);
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_NAME);
+ if (headers != null) {
+ name = headers.get(0);
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_URL);
+ if (headers != null) {
+ url = headers.get(0);
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_PUB);
+ if (headers != null) {
+ isPublic = headers.get(0).equals("1");
+ icyHeadersPresent = true;
+ }
+ headers = responseHeaders.get(RESPONSE_HEADER_METADATA_INTERVAL);
+ if (headers != null) {
+ String metadataIntervalHeader = headers.get(0);
+ try {
+ metadataInterval = Integer.parseInt(metadataIntervalHeader);
+ if (metadataInterval > 0) {
+ icyHeadersPresent = true;
+ } else {
+ Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
+ metadataInterval = C.LENGTH_UNSET;
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Invalid metadata interval: " + metadataIntervalHeader);
+ }
+ }
+ return icyHeadersPresent
+ ? new IcyHeaders(bitrate, genre, name, url, isPublic, metadataInterval)
+ : null;
+ }
+
+ /**
+ * Bitrate in bits per second ({@code (icy-br * 1000)}), or {@link Format#NO_VALUE} if the header
+ * was not present.
+ */
+ public final int bitrate;
+ /** The genre ({@code icy-genre}). */
+ @Nullable public final String genre;
+ /** The stream name ({@code icy-name}). */
+ @Nullable public final String name;
+ /** The URL of the radio station ({@code icy-url}). */
+ @Nullable public final String url;
+ /**
+ * Whether the radio station is listed ({@code icy-pub}), or {@code false} if the header was not
+ * present.
+ */
+ public final boolean isPublic;
+
+ /**
+ * The interval in bytes between metadata chunks ({@code icy-metaint}), or {@link C#LENGTH_UNSET}
+ * if the header was not present.
+ */
+ public final int metadataInterval;
+
+ /**
+ * @param bitrate See {@link #bitrate}.
+ * @param genre See {@link #genre}.
+ * @param name See {@link #name See}.
+ * @param url See {@link #url}.
+ * @param isPublic See {@link #isPublic}.
+ * @param metadataInterval See {@link #metadataInterval}.
+ */
+ public IcyHeaders(
+ int bitrate,
+ @Nullable String genre,
+ @Nullable String name,
+ @Nullable String url,
+ boolean isPublic,
+ int metadataInterval) {
+ Assertions.checkArgument(metadataInterval == C.LENGTH_UNSET || metadataInterval > 0);
+ this.bitrate = bitrate;
+ this.genre = genre;
+ this.name = name;
+ this.url = url;
+ this.isPublic = isPublic;
+ this.metadataInterval = metadataInterval;
+ }
+
+ /* package */ IcyHeaders(Parcel in) {
+ bitrate = in.readInt();
+ genre = in.readString();
+ name = in.readString();
+ url = in.readString();
+ isPublic = Util.readBoolean(in);
+ metadataInterval = in.readInt();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ IcyHeaders other = (IcyHeaders) obj;
+ return bitrate == other.bitrate
+ && Util.areEqual(genre, other.genre)
+ && Util.areEqual(name, other.name)
+ && Util.areEqual(url, other.url)
+ && isPublic == other.isPublic
+ && metadataInterval == other.metadataInterval;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + bitrate;
+ result = 31 * result + (genre != null ? genre.hashCode() : 0);
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (url != null ? url.hashCode() : 0);
+ result = 31 * result + (isPublic ? 1 : 0);
+ result = 31 * result + metadataInterval;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "IcyHeaders: name=\""
+ + name
+ + "\", genre=\""
+ + genre
+ + "\", bitrate="
+ + bitrate
+ + ", metadataInterval="
+ + metadataInterval;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(bitrate);
+ dest.writeString(genre);
+ dest.writeString(name);
+ dest.writeString(url);
+ Util.writeBoolean(dest, isPublic);
+ dest.writeInt(metadataInterval);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<IcyHeaders> CREATOR =
+ new Parcelable.Creator<IcyHeaders>() {
+
+ @Override
+ public IcyHeaders createFromParcel(Parcel in) {
+ return new IcyHeaders(in);
+ }
+
+ @Override
+ public IcyHeaders[] newArray(int size) {
+ return new IcyHeaders[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
new file mode 100644
index 0000000000..4104e41c64
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/IcyInfo.java
@@ -0,0 +1,107 @@
+/*
+ * 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.metadata.icy;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Arrays;
+
+/** ICY in-stream information. */
+public final class IcyInfo implements Metadata.Entry {
+
+ /** The complete metadata bytes used to construct this IcyInfo. */
+ public final byte[] rawMetadata;
+ /** The stream title if present and decodable, or {@code null}. */
+ @Nullable public final String title;
+ /** The stream URL if present and decodable, or {@code null}. */
+ @Nullable public final String url;
+
+ /**
+ * Construct a new IcyInfo from the source metadata, and optionally a StreamTitle and StreamUrl
+ * that have been extracted.
+ *
+ * @param rawMetadata See {@link #rawMetadata}.
+ * @param title See {@link #title}.
+ * @param url See {@link #url}.
+ */
+ public IcyInfo(byte[] rawMetadata, @Nullable String title, @Nullable String url) {
+ this.rawMetadata = rawMetadata;
+ this.title = title;
+ this.url = url;
+ }
+
+ /* package */ IcyInfo(Parcel in) {
+ rawMetadata = Assertions.checkNotNull(in.createByteArray());
+ title = in.readString();
+ url = in.readString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ IcyInfo other = (IcyInfo) obj;
+ // title & url are derived from rawMetadata, so no need to include them in the comparison.
+ return Arrays.equals(rawMetadata, other.rawMetadata);
+ }
+
+ @Override
+ public int hashCode() {
+ // title & url are derived from rawMetadata, so no need to include them in the hash.
+ return Arrays.hashCode(rawMetadata);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "ICY: title=\"%s\", url=\"%s\", rawMetadata.length=\"%s\"", title, url, rawMetadata.length);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(rawMetadata);
+ dest.writeString(title);
+ dest.writeString(url);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<IcyInfo> CREATOR =
+ new Parcelable.Creator<IcyInfo>() {
+
+ @Override
+ public IcyInfo createFromParcel(Parcel in) {
+ return new IcyInfo(in);
+ }
+
+ @Override
+ public IcyInfo[] newArray(int size) {
+ return new IcyInfo[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java
new file mode 100644
index 0000000000..a8a45e2ef1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/icy/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.metadata.icy;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
new file mode 100644
index 0000000000..f151707e4b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
@@ -0,0 +1,108 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * APIC (Attached Picture) ID3 frame.
+ */
+public final class ApicFrame extends Id3Frame {
+
+ public static final String ID = "APIC";
+
+ public final String mimeType;
+ @Nullable public final String description;
+ public final int pictureType;
+ public final byte[] pictureData;
+
+ public ApicFrame(
+ String mimeType, @Nullable String description, int pictureType, byte[] pictureData) {
+ super(ID);
+ this.mimeType = mimeType;
+ this.description = description;
+ this.pictureType = pictureType;
+ this.pictureData = pictureData;
+ }
+
+ /* package */ ApicFrame(Parcel in) {
+ super(ID);
+ mimeType = castNonNull(in.readString());
+ description = in.readString();
+ pictureType = in.readInt();
+ pictureData = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ApicFrame other = (ApicFrame) obj;
+ return pictureType == other.pictureType && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(description, other.description)
+ && Arrays.equals(pictureData, other.pictureData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + pictureType;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(pictureData);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": mimeType=" + mimeType + ", description=" + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mimeType);
+ dest.writeString(description);
+ dest.writeInt(pictureType);
+ dest.writeByteArray(pictureData);
+ }
+
+ public static final Parcelable.Creator<ApicFrame> CREATOR = new Parcelable.Creator<ApicFrame>() {
+
+ @Override
+ public ApicFrame createFromParcel(Parcel in) {
+ return new ApicFrame(in);
+ }
+
+ @Override
+ public ApicFrame[] newArray(int size) {
+ return new ApicFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
new file mode 100644
index 0000000000..adc66ccdfe
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import java.util.Arrays;
+
+/**
+ * Binary ID3 frame.
+ */
+public final class BinaryFrame extends Id3Frame {
+
+ public final byte[] data;
+
+ public BinaryFrame(String id, byte[] data) {
+ super(id);
+ this.data = data;
+ }
+
+ /* package */ BinaryFrame(Parcel in) {
+ super(castNonNull(in.readString()));
+ data = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ BinaryFrame other = (BinaryFrame) obj;
+ return id.equals(other.id) && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + Arrays.hashCode(data);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeByteArray(data);
+ }
+
+ public static final Parcelable.Creator<BinaryFrame> CREATOR =
+ new Parcelable.Creator<BinaryFrame>() {
+
+ @Override
+ public BinaryFrame createFromParcel(Parcel in) {
+ return new BinaryFrame(in);
+ }
+
+ @Override
+ public BinaryFrame[] newArray(int size) {
+ return new BinaryFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
new file mode 100644
index 0000000000..348781dddf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -0,0 +1,145 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Chapter information ID3 frame.
+ */
+public final class ChapterFrame extends Id3Frame {
+
+ public static final String ID = "CHAP";
+
+ public final String chapterId;
+ public final int startTimeMs;
+ public final int endTimeMs;
+ /**
+ * The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set.
+ */
+ public final long startOffset;
+ /**
+ * The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set.
+ */
+ public final long endOffset;
+ private final Id3Frame[] subFrames;
+
+ public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset,
+ long endOffset, Id3Frame[] subFrames) {
+ super(ID);
+ this.chapterId = chapterId;
+ this.startTimeMs = startTimeMs;
+ this.endTimeMs = endTimeMs;
+ this.startOffset = startOffset;
+ this.endOffset = endOffset;
+ this.subFrames = subFrames;
+ }
+
+ /* package */ ChapterFrame(Parcel in) {
+ super(ID);
+ this.chapterId = castNonNull(in.readString());
+ this.startTimeMs = in.readInt();
+ this.endTimeMs = in.readInt();
+ this.startOffset = in.readLong();
+ this.endOffset = in.readLong();
+ int subFrameCount = in.readInt();
+ subFrames = new Id3Frame[subFrameCount];
+ for (int i = 0; i < subFrameCount; i++) {
+ subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the number of sub-frames.
+ */
+ public int getSubFrameCount() {
+ return subFrames.length;
+ }
+
+ /**
+ * Returns the sub-frame at {@code index}.
+ */
+ public Id3Frame getSubFrame(int index) {
+ return subFrames[index];
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ChapterFrame other = (ChapterFrame) obj;
+ return startTimeMs == other.startTimeMs
+ && endTimeMs == other.endTimeMs
+ && startOffset == other.startOffset
+ && endOffset == other.endOffset
+ && Util.areEqual(chapterId, other.chapterId)
+ && Arrays.equals(subFrames, other.subFrames);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + startTimeMs;
+ result = 31 * result + endTimeMs;
+ result = 31 * result + (int) startOffset;
+ result = 31 * result + (int) endOffset;
+ result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(chapterId);
+ dest.writeInt(startTimeMs);
+ dest.writeInt(endTimeMs);
+ dest.writeLong(startOffset);
+ dest.writeLong(endOffset);
+ dest.writeInt(subFrames.length);
+ for (Id3Frame subFrame : subFrames) {
+ dest.writeParcelable(subFrame, 0);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<ChapterFrame> CREATOR = new Creator<ChapterFrame>() {
+
+ @Override
+ public ChapterFrame createFromParcel(Parcel in) {
+ return new ChapterFrame(in);
+ }
+
+ @Override
+ public ChapterFrame[] newArray(int size) {
+ return new ChapterFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
new file mode 100644
index 0000000000..9451151c16
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -0,0 +1,127 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * Chapter table of contents ID3 frame.
+ */
+public final class ChapterTocFrame extends Id3Frame {
+
+ public static final String ID = "CTOC";
+
+ public final String elementId;
+ public final boolean isRoot;
+ public final boolean isOrdered;
+ public final String[] children;
+ private final Id3Frame[] subFrames;
+
+ public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children,
+ Id3Frame[] subFrames) {
+ super(ID);
+ this.elementId = elementId;
+ this.isRoot = isRoot;
+ this.isOrdered = isOrdered;
+ this.children = children;
+ this.subFrames = subFrames;
+ }
+
+ /* package */
+ ChapterTocFrame(Parcel in) {
+ super(ID);
+ this.elementId = castNonNull(in.readString());
+ this.isRoot = in.readByte() != 0;
+ this.isOrdered = in.readByte() != 0;
+ this.children = castNonNull(in.createStringArray());
+ int subFrameCount = in.readInt();
+ subFrames = new Id3Frame[subFrameCount];
+ for (int i = 0; i < subFrameCount; i++) {
+ subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());
+ }
+ }
+
+ /**
+ * Returns the number of sub-frames.
+ */
+ public int getSubFrameCount() {
+ return subFrames.length;
+ }
+
+ /**
+ * Returns the sub-frame at {@code index}.
+ */
+ public Id3Frame getSubFrame(int index) {
+ return subFrames[index];
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ChapterTocFrame other = (ChapterTocFrame) obj;
+ return isRoot == other.isRoot
+ && isOrdered == other.isOrdered
+ && Util.areEqual(elementId, other.elementId)
+ && Arrays.equals(children, other.children)
+ && Arrays.equals(subFrames, other.subFrames);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (isRoot ? 1 : 0);
+ result = 31 * result + (isOrdered ? 1 : 0);
+ result = 31 * result + (elementId != null ? elementId.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(elementId);
+ dest.writeByte((byte) (isRoot ? 1 : 0));
+ dest.writeByte((byte) (isOrdered ? 1 : 0));
+ dest.writeStringArray(children);
+ dest.writeInt(subFrames.length);
+ for (Id3Frame subFrame : subFrames) {
+ dest.writeParcelable(subFrame, 0);
+ }
+ }
+
+ public static final Creator<ChapterTocFrame> CREATOR = new Creator<ChapterTocFrame>() {
+
+ @Override
+ public ChapterTocFrame createFromParcel(Parcel in) {
+ return new ChapterTocFrame(in);
+ }
+
+ @Override
+ public ChapterTocFrame[] newArray(int size) {
+ return new ChapterTocFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
new file mode 100644
index 0000000000..98b8c79a96
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
@@ -0,0 +1,101 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Comment ID3 frame.
+ */
+public final class CommentFrame extends Id3Frame {
+
+ public static final String ID = "COMM";
+
+ public final String language;
+ public final String description;
+ public final String text;
+
+ public CommentFrame(String language, String description, String text) {
+ super(ID);
+ this.language = language;
+ this.description = description;
+ this.text = text;
+ }
+
+ /* package */ CommentFrame(Parcel in) {
+ super(ID);
+ language = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ text = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ CommentFrame other = (CommentFrame) obj;
+ return Util.areEqual(description, other.description) && Util.areEqual(language, other.language)
+ && Util.areEqual(text, other.text);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (language != null ? language.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (text != null ? text.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": language=" + language + ", description=" + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(language);
+ dest.writeString(text);
+ }
+
+ public static final Parcelable.Creator<CommentFrame> CREATOR =
+ new Parcelable.Creator<CommentFrame>() {
+
+ @Override
+ public CommentFrame createFromParcel(Parcel in) {
+ return new CommentFrame(in);
+ }
+
+ @Override
+ public CommentFrame[] newArray(int size) {
+ return new CommentFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
new file mode 100644
index 0000000000..58a208a76a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
@@ -0,0 +1,112 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * GEOB (General Encapsulated Object) ID3 frame.
+ */
+public final class GeobFrame extends Id3Frame {
+
+ public static final String ID = "GEOB";
+
+ public final String mimeType;
+ public final String filename;
+ public final String description;
+ public final byte[] data;
+
+ public GeobFrame(String mimeType, String filename, String description, byte[] data) {
+ super(ID);
+ this.mimeType = mimeType;
+ this.filename = filename;
+ this.description = description;
+ this.data = data;
+ }
+
+ /* package */ GeobFrame(Parcel in) {
+ super(ID);
+ mimeType = castNonNull(in.readString());
+ filename = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ data = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ GeobFrame other = (GeobFrame) obj;
+ return Util.areEqual(mimeType, other.mimeType) && Util.areEqual(filename, other.filename)
+ && Util.areEqual(description, other.description) && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ result = 31 * result + (filename != null ? filename.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(data);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id
+ + ": mimeType="
+ + mimeType
+ + ", filename="
+ + filename
+ + ", description="
+ + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mimeType);
+ dest.writeString(filename);
+ dest.writeString(description);
+ dest.writeByteArray(data);
+ }
+
+ public static final Parcelable.Creator<GeobFrame> CREATOR = new Parcelable.Creator<GeobFrame>() {
+
+ @Override
+ public GeobFrame createFromParcel(Parcel in) {
+ return new GeobFrame(in);
+ }
+
+ @Override
+ public GeobFrame[] newArray(int size) {
+ return new GeobFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
new file mode 100644
index 0000000000..36e004ed52
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -0,0 +1,842 @@
+/*
+ * 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.metadata.id3;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Decodes ID3 tags.
+ */
+public final class Id3Decoder implements MetadataDecoder {
+
+ /**
+ * A predicate for determining whether individual frames should be decoded.
+ */
+ public interface FramePredicate {
+
+ /**
+ * Returns whether a frame with the specified parameters should be decoded.
+ *
+ * @param majorVersion The major version of the ID3 tag.
+ * @param id0 The first byte of the frame ID.
+ * @param id1 The second byte of the frame ID.
+ * @param id2 The third byte of the frame ID.
+ * @param id3 The fourth byte of the frame ID.
+ * @return Whether the frame should be decoded.
+ */
+ boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3);
+
+ }
+
+ /** A predicate that indicates no frames should be decoded. */
+ public static final FramePredicate NO_FRAMES_PREDICATE =
+ (majorVersion, id0, id1, id2, id3) -> false;
+
+ private static final String TAG = "Id3Decoder";
+
+ /** The first three bytes of a well formed ID3 tag header. */
+ public static final int ID3_TAG = 0x00494433;
+ /**
+ * Length of an ID3 tag header.
+ */
+ public static final int ID3_HEADER_LENGTH = 10;
+
+ private static final int FRAME_FLAG_V3_IS_COMPRESSED = 0x0080;
+ private static final int FRAME_FLAG_V3_IS_ENCRYPTED = 0x0040;
+ private static final int FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER = 0x0020;
+ private static final int FRAME_FLAG_V4_IS_COMPRESSED = 0x0008;
+ private static final int FRAME_FLAG_V4_IS_ENCRYPTED = 0x0004;
+ private static final int FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER = 0x0040;
+ private static final int FRAME_FLAG_V4_IS_UNSYNCHRONIZED = 0x0002;
+ private static final int FRAME_FLAG_V4_HAS_DATA_LENGTH = 0x0001;
+
+ private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0;
+ private static final int ID3_TEXT_ENCODING_UTF_16 = 1;
+ private static final int ID3_TEXT_ENCODING_UTF_16BE = 2;
+ private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
+
+ @Nullable private final FramePredicate framePredicate;
+
+ public Id3Decoder() {
+ this(null);
+ }
+
+ /**
+ * @param framePredicate Determines which frames are decoded. May be null to decode all frames.
+ */
+ public Id3Decoder(@Nullable FramePredicate framePredicate) {
+ this.framePredicate = framePredicate;
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ @Nullable
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+ return decode(buffer.array(), buffer.limit());
+ }
+
+ /**
+ * Decodes ID3 tags.
+ *
+ * @param data The bytes to decode ID3 tags from.
+ * @param size Amount of bytes in {@code data} to read.
+ * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could
+ * not be decoded.
+ */
+ @Nullable
+ public Metadata decode(byte[] data, int size) {
+ List<Id3Frame> id3Frames = new ArrayList<>();
+ ParsableByteArray id3Data = new ParsableByteArray(data, size);
+
+ Id3Header id3Header = decodeHeader(id3Data);
+ if (id3Header == null) {
+ return null;
+ }
+
+ int startPosition = id3Data.getPosition();
+ int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
+ int framesSize = id3Header.framesSize;
+ if (id3Header.isUnsynchronized) {
+ framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
+ }
+ id3Data.setLimit(startPosition + framesSize);
+
+ boolean unsignedIntFrameSizeHack = false;
+ if (!validateFrames(id3Data, id3Header.majorVersion, frameHeaderSize, false)) {
+ if (id3Header.majorVersion == 4 && validateFrames(id3Data, 4, frameHeaderSize, true)) {
+ unsignedIntFrameSizeHack = true;
+ } else {
+ Log.w(TAG, "Failed to validate ID3 tag with majorVersion=" + id3Header.majorVersion);
+ return null;
+ }
+ }
+
+ while (id3Data.bytesLeft() >= frameHeaderSize) {
+ Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ if (frame != null) {
+ id3Frames.add(frame);
+ }
+ }
+
+ return new Metadata(id3Frames);
+ }
+
+ /**
+ * @param data A {@link ParsableByteArray} from which the header should be read.
+ * @return The parsed header, or null if the ID3 tag is unsupported.
+ */
+ @Nullable
+ private static Id3Header decodeHeader(ParsableByteArray data) {
+ if (data.bytesLeft() < ID3_HEADER_LENGTH) {
+ Log.w(TAG, "Data too short to be an ID3 tag");
+ return null;
+ }
+
+ int id = data.readUnsignedInt24();
+ if (id != ID3_TAG) {
+ Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id));
+ return null;
+ }
+
+ int majorVersion = data.readUnsignedByte();
+ data.skipBytes(1); // Skip minor version.
+ int flags = data.readUnsignedByte();
+ int framesSize = data.readSynchSafeInt();
+
+ if (majorVersion == 2) {
+ boolean isCompressed = (flags & 0x40) != 0;
+ if (isCompressed) {
+ Log.w(TAG, "Skipped ID3 tag with majorVersion=2 and undefined compression scheme");
+ return null;
+ }
+ } else if (majorVersion == 3) {
+ boolean hasExtendedHeader = (flags & 0x40) != 0;
+ if (hasExtendedHeader) {
+ int extendedHeaderSize = data.readInt(); // Size excluding size field.
+ data.skipBytes(extendedHeaderSize);
+ framesSize -= (extendedHeaderSize + 4);
+ }
+ } else if (majorVersion == 4) {
+ boolean hasExtendedHeader = (flags & 0x40) != 0;
+ if (hasExtendedHeader) {
+ int extendedHeaderSize = data.readSynchSafeInt(); // Size including size field.
+ data.skipBytes(extendedHeaderSize - 4);
+ framesSize -= extendedHeaderSize;
+ }
+ boolean hasFooter = (flags & 0x10) != 0;
+ if (hasFooter) {
+ framesSize -= 10;
+ }
+ } else {
+ Log.w(TAG, "Skipped ID3 tag with unsupported majorVersion=" + majorVersion);
+ return null;
+ }
+
+ // isUnsynchronized is advisory only in version 4. Frame level flags are used instead.
+ boolean isUnsynchronized = majorVersion < 4 && (flags & 0x80) != 0;
+ return new Id3Header(majorVersion, isUnsynchronized, framesSize);
+ }
+
+ private static boolean validateFrames(ParsableByteArray id3Data, int majorVersion,
+ int frameHeaderSize, boolean unsignedIntFrameSizeHack) {
+ int startPosition = id3Data.getPosition();
+ try {
+ while (id3Data.bytesLeft() >= frameHeaderSize) {
+ // Read the next frame header.
+ int id;
+ long frameSize;
+ int flags;
+ if (majorVersion >= 3) {
+ id = id3Data.readInt();
+ frameSize = id3Data.readUnsignedInt();
+ flags = id3Data.readUnsignedShort();
+ } else {
+ id = id3Data.readUnsignedInt24();
+ frameSize = id3Data.readUnsignedInt24();
+ flags = 0;
+ }
+ // Validate the frame header and skip to the next one.
+ if (id == 0 && frameSize == 0 && flags == 0) {
+ // We've reached zero padding after the end of the final frame.
+ return true;
+ } else {
+ if (majorVersion == 4 && !unsignedIntFrameSizeHack) {
+ // Parse the data size as a synchsafe integer, as per the spec.
+ if ((frameSize & 0x808080L) != 0) {
+ return false;
+ }
+ frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
+ | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
+ }
+ boolean hasGroupIdentifier = false;
+ boolean hasDataLength = false;
+ if (majorVersion == 4) {
+ hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;
+ hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;
+ } else if (majorVersion == 3) {
+ hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;
+ // A V3 frame has data length if and only if it's compressed.
+ hasDataLength = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;
+ }
+ int minimumFrameSize = 0;
+ if (hasGroupIdentifier) {
+ minimumFrameSize++;
+ }
+ if (hasDataLength) {
+ minimumFrameSize += 4;
+ }
+ if (frameSize < minimumFrameSize) {
+ return false;
+ }
+ if (id3Data.bytesLeft() < frameSize) {
+ return false;
+ }
+ id3Data.skipBytes((int) frameSize); // flags
+ }
+ }
+ return true;
+ } finally {
+ id3Data.setPosition(startPosition);
+ }
+ }
+
+ @Nullable
+ private static Id3Frame decodeFrame(
+ int majorVersion,
+ ParsableByteArray id3Data,
+ boolean unsignedIntFrameSizeHack,
+ int frameHeaderSize,
+ @Nullable FramePredicate framePredicate) {
+ int frameId0 = id3Data.readUnsignedByte();
+ int frameId1 = id3Data.readUnsignedByte();
+ int frameId2 = id3Data.readUnsignedByte();
+ int frameId3 = majorVersion >= 3 ? id3Data.readUnsignedByte() : 0;
+
+ int frameSize;
+ if (majorVersion == 4) {
+ frameSize = id3Data.readUnsignedIntToInt();
+ if (!unsignedIntFrameSizeHack) {
+ frameSize = (frameSize & 0xFF) | (((frameSize >> 8) & 0xFF) << 7)
+ | (((frameSize >> 16) & 0xFF) << 14) | (((frameSize >> 24) & 0xFF) << 21);
+ }
+ } else if (majorVersion == 3) {
+ frameSize = id3Data.readUnsignedIntToInt();
+ } else /* id3Header.majorVersion == 2 */ {
+ frameSize = id3Data.readUnsignedInt24();
+ }
+
+ int flags = majorVersion >= 3 ? id3Data.readUnsignedShort() : 0;
+ if (frameId0 == 0 && frameId1 == 0 && frameId2 == 0 && frameId3 == 0 && frameSize == 0
+ && flags == 0) {
+ // We must be reading zero padding at the end of the tag.
+ id3Data.setPosition(id3Data.limit());
+ return null;
+ }
+
+ int nextFramePosition = id3Data.getPosition() + frameSize;
+ if (nextFramePosition > id3Data.limit()) {
+ Log.w(TAG, "Frame size exceeds remaining tag data");
+ id3Data.setPosition(id3Data.limit());
+ return null;
+ }
+
+ if (framePredicate != null
+ && !framePredicate.evaluate(majorVersion, frameId0, frameId1, frameId2, frameId3)) {
+ // Filtered by the predicate.
+ id3Data.setPosition(nextFramePosition);
+ return null;
+ }
+
+ // Frame flags.
+ boolean isCompressed = false;
+ boolean isEncrypted = false;
+ boolean isUnsynchronized = false;
+ boolean hasDataLength = false;
+ boolean hasGroupIdentifier = false;
+ if (majorVersion == 3) {
+ isCompressed = (flags & FRAME_FLAG_V3_IS_COMPRESSED) != 0;
+ isEncrypted = (flags & FRAME_FLAG_V3_IS_ENCRYPTED) != 0;
+ hasGroupIdentifier = (flags & FRAME_FLAG_V3_HAS_GROUP_IDENTIFIER) != 0;
+ // A V3 frame has data length if and only if it's compressed.
+ hasDataLength = isCompressed;
+ } else if (majorVersion == 4) {
+ hasGroupIdentifier = (flags & FRAME_FLAG_V4_HAS_GROUP_IDENTIFIER) != 0;
+ isCompressed = (flags & FRAME_FLAG_V4_IS_COMPRESSED) != 0;
+ isEncrypted = (flags & FRAME_FLAG_V4_IS_ENCRYPTED) != 0;
+ isUnsynchronized = (flags & FRAME_FLAG_V4_IS_UNSYNCHRONIZED) != 0;
+ hasDataLength = (flags & FRAME_FLAG_V4_HAS_DATA_LENGTH) != 0;
+ }
+
+ if (isCompressed || isEncrypted) {
+ Log.w(TAG, "Skipping unsupported compressed or encrypted frame");
+ id3Data.setPosition(nextFramePosition);
+ return null;
+ }
+
+ if (hasGroupIdentifier) {
+ frameSize--;
+ id3Data.skipBytes(1);
+ }
+ if (hasDataLength) {
+ frameSize -= 4;
+ id3Data.skipBytes(4);
+ }
+ if (isUnsynchronized) {
+ frameSize = removeUnsynchronization(id3Data, frameSize);
+ }
+
+ try {
+ Id3Frame frame;
+ if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
+ && (majorVersion == 2 || frameId3 == 'X')) {
+ frame = decodeTxxxFrame(id3Data, frameSize);
+ } else if (frameId0 == 'T') {
+ String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
+ frame = decodeTextInformationFrame(id3Data, frameSize, id);
+ } else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X'
+ && (majorVersion == 2 || frameId3 == 'X')) {
+ frame = decodeWxxxFrame(id3Data, frameSize);
+ } else if (frameId0 == 'W') {
+ String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
+ frame = decodeUrlLinkFrame(id3Data, frameSize, id);
+ } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
+ frame = decodePrivFrame(id3Data, frameSize);
+ } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
+ && (frameId3 == 'B' || majorVersion == 2)) {
+ frame = decodeGeobFrame(id3Data, frameSize);
+ } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
+ : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
+ frame = decodeApicFrame(id3Data, frameSize, majorVersion);
+ } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
+ && (frameId3 == 'M' || majorVersion == 2)) {
+ frame = decodeCommentFrame(id3Data, frameSize);
+ } else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') {
+ frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
+ frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ } else if (frameId0 == 'M' && frameId1 == 'L' && frameId2 == 'L' && frameId3 == 'T') {
+ frame = decodeMlltFrame(id3Data, frameSize);
+ } else {
+ String id = getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3);
+ frame = decodeBinaryFrame(id3Data, frameSize, id);
+ }
+ if (frame == null) {
+ Log.w(TAG, "Failed to decode frame: id="
+ + getFrameId(majorVersion, frameId0, frameId1, frameId2, frameId3) + ", frameSize="
+ + frameSize);
+ }
+ return frame;
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, "Unsupported character encoding");
+ return null;
+ } finally {
+ id3Data.setPosition(nextFramePosition);
+ }
+ }
+
+ @Nullable
+ private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ if (frameSize < 1) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int descriptionEndIndex = indexOfEos(data, 0, encoding);
+ String description = new String(data, 0, descriptionEndIndex, charset);
+
+ int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
+ String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);
+
+ return new TextInformationFrame("TXXX", description, value);
+ }
+
+ @Nullable
+ private static TextInformationFrame decodeTextInformationFrame(
+ ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException {
+ if (frameSize < 1) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int valueEndIndex = indexOfEos(data, 0, encoding);
+ String value = new String(data, 0, valueEndIndex, charset);
+
+ return new TextInformationFrame(id, null, value);
+ }
+
+ @Nullable
+ private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ if (frameSize < 1) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int descriptionEndIndex = indexOfEos(data, 0, encoding);
+ String description = new String(data, 0, descriptionEndIndex, charset);
+
+ int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
+ String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1");
+
+ return new UrlLinkFrame("WXXX", description, url);
+ }
+
+ private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize,
+ String id) throws UnsupportedEncodingException {
+ byte[] data = new byte[frameSize];
+ id3Data.readBytes(data, 0, frameSize);
+
+ int urlEndIndex = indexOfZeroByte(data, 0);
+ String url = new String(data, 0, urlEndIndex, "ISO-8859-1");
+
+ return new UrlLinkFrame(id, null, url);
+ }
+
+ private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ byte[] data = new byte[frameSize];
+ id3Data.readBytes(data, 0, frameSize);
+
+ int ownerEndIndex = indexOfZeroByte(data, 0);
+ String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1");
+
+ int privateDataStartIndex = ownerEndIndex + 1;
+ byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length);
+
+ return new PrivFrame(owner, privateData);
+ }
+
+ private static GeobFrame decodeGeobFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ int mimeTypeEndIndex = indexOfZeroByte(data, 0);
+ String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1");
+
+ int filenameStartIndex = mimeTypeEndIndex + 1;
+ int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding);
+ String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset);
+
+ int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
+ int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
+ String description =
+ decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset);
+
+ int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length);
+
+ return new GeobFrame(mimeType, filename, description, objectData);
+ }
+
+ private static ApicFrame decodeApicFrame(ParsableByteArray id3Data, int frameSize,
+ int majorVersion) throws UnsupportedEncodingException {
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[frameSize - 1];
+ id3Data.readBytes(data, 0, frameSize - 1);
+
+ String mimeType;
+ int mimeTypeEndIndex;
+ if (majorVersion == 2) {
+ mimeTypeEndIndex = 2;
+ mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1"));
+ if ("image/jpg".equals(mimeType)) {
+ mimeType = "image/jpeg";
+ }
+ } else {
+ mimeTypeEndIndex = indexOfZeroByte(data, 0);
+ mimeType = Util.toLowerInvariant(new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"));
+ if (mimeType.indexOf('/') == -1) {
+ mimeType = "image/" + mimeType;
+ }
+ }
+
+ int pictureType = data[mimeTypeEndIndex + 1] & 0xFF;
+
+ int descriptionStartIndex = mimeTypeEndIndex + 2;
+ int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
+ String description = new String(data, descriptionStartIndex,
+ descriptionEndIndex - descriptionStartIndex, charset);
+
+ int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length);
+
+ return new ApicFrame(mimeType, description, pictureType, pictureData);
+ }
+
+ @Nullable
+ private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
+ throws UnsupportedEncodingException {
+ if (frameSize < 4) {
+ // Frame is malformed.
+ return null;
+ }
+
+ int encoding = id3Data.readUnsignedByte();
+ String charset = getCharsetName(encoding);
+
+ byte[] data = new byte[3];
+ id3Data.readBytes(data, 0, 3);
+ String language = new String(data, 0, 3);
+
+ data = new byte[frameSize - 4];
+ id3Data.readBytes(data, 0, frameSize - 4);
+
+ int descriptionEndIndex = indexOfEos(data, 0, encoding);
+ String description = new String(data, 0, descriptionEndIndex, charset);
+
+ int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
+ int textEndIndex = indexOfEos(data, textStartIndex, encoding);
+ String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset);
+
+ return new CommentFrame(language, description, text);
+ }
+
+ private static ChapterFrame decodeChapterFrame(
+ ParsableByteArray id3Data,
+ int frameSize,
+ int majorVersion,
+ boolean unsignedIntFrameSizeHack,
+ int frameHeaderSize,
+ @Nullable FramePredicate framePredicate)
+ throws UnsupportedEncodingException {
+ int framePosition = id3Data.getPosition();
+ int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
+ String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition,
+ "ISO-8859-1");
+ id3Data.setPosition(chapterIdEndIndex + 1);
+
+ int startTime = id3Data.readInt();
+ int endTime = id3Data.readInt();
+ long startOffset = id3Data.readUnsignedInt();
+ if (startOffset == 0xFFFFFFFFL) {
+ startOffset = C.POSITION_UNSET;
+ }
+ long endOffset = id3Data.readUnsignedInt();
+ if (endOffset == 0xFFFFFFFFL) {
+ endOffset = C.POSITION_UNSET;
+ }
+
+ ArrayList<Id3Frame> subFrames = new ArrayList<>();
+ int limit = framePosition + frameSize;
+ while (id3Data.getPosition() < limit) {
+ Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ if (frame != null) {
+ subFrames.add(frame);
+ }
+ }
+
+ Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
+ subFrames.toArray(subFrameArray);
+ return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray);
+ }
+
+ private static ChapterTocFrame decodeChapterTOCFrame(
+ ParsableByteArray id3Data,
+ int frameSize,
+ int majorVersion,
+ boolean unsignedIntFrameSizeHack,
+ int frameHeaderSize,
+ @Nullable FramePredicate framePredicate)
+ throws UnsupportedEncodingException {
+ int framePosition = id3Data.getPosition();
+ int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
+ String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition,
+ "ISO-8859-1");
+ id3Data.setPosition(elementIdEndIndex + 1);
+
+ int ctocFlags = id3Data.readUnsignedByte();
+ boolean isRoot = (ctocFlags & 0x0002) != 0;
+ boolean isOrdered = (ctocFlags & 0x0001) != 0;
+
+ int childCount = id3Data.readUnsignedByte();
+ String[] children = new String[childCount];
+ for (int i = 0; i < childCount; i++) {
+ int startIndex = id3Data.getPosition();
+ int endIndex = indexOfZeroByte(id3Data.data, startIndex);
+ children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1");
+ id3Data.setPosition(endIndex + 1);
+ }
+
+ ArrayList<Id3Frame> subFrames = new ArrayList<>();
+ int limit = framePosition + frameSize;
+ while (id3Data.getPosition() < limit) {
+ Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
+ frameHeaderSize, framePredicate);
+ if (frame != null) {
+ subFrames.add(frame);
+ }
+ }
+
+ Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
+ subFrames.toArray(subFrameArray);
+ return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray);
+ }
+
+ private static MlltFrame decodeMlltFrame(ParsableByteArray id3Data, int frameSize) {
+ // See ID3v2.4.0 native frames subsection 4.6.
+ int mpegFramesBetweenReference = id3Data.readUnsignedShort();
+ int bytesBetweenReference = id3Data.readUnsignedInt24();
+ int millisecondsBetweenReference = id3Data.readUnsignedInt24();
+ int bitsForBytesDeviation = id3Data.readUnsignedByte();
+ int bitsForMillisecondsDeviation = id3Data.readUnsignedByte();
+
+ ParsableBitArray references = new ParsableBitArray();
+ references.reset(id3Data);
+ int referencesBits = 8 * (frameSize - 10);
+ int bitsPerReference = bitsForBytesDeviation + bitsForMillisecondsDeviation;
+ int referencesCount = referencesBits / bitsPerReference;
+ int[] bytesDeviations = new int[referencesCount];
+ int[] millisecondsDeviations = new int[referencesCount];
+ for (int i = 0; i < referencesCount; i++) {
+ int bytesDeviation = references.readBits(bitsForBytesDeviation);
+ int millisecondsDeviation = references.readBits(bitsForMillisecondsDeviation);
+ bytesDeviations[i] = bytesDeviation;
+ millisecondsDeviations[i] = millisecondsDeviation;
+ }
+
+ return new MlltFrame(
+ mpegFramesBetweenReference,
+ bytesBetweenReference,
+ millisecondsBetweenReference,
+ bytesDeviations,
+ millisecondsDeviations);
+ }
+
+ private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
+ String id) {
+ byte[] frame = new byte[frameSize];
+ id3Data.readBytes(frame, 0, frameSize);
+
+ return new BinaryFrame(id, frame);
+ }
+
+ /**
+ * Performs in-place removal of unsynchronization for {@code length} bytes starting from
+ * {@link ParsableByteArray#getPosition()}
+ *
+ * @param data Contains the data to be processed.
+ * @param length The length of the data to be processed.
+ * @return The length of the data after processing.
+ */
+ private static int removeUnsynchronization(ParsableByteArray data, int length) {
+ byte[] bytes = data.data;
+ int startPosition = data.getPosition();
+ for (int i = startPosition; i + 1 < startPosition + length; i++) {
+ if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
+ int relativePosition = i - startPosition;
+ System.arraycopy(bytes, i + 2, bytes, i + 1, length - relativePosition - 2);
+ length--;
+ }
+ }
+ return length;
+ }
+
+ /**
+ * Maps encoding byte from ID3v2 frame to a Charset.
+ *
+ * @param encodingByte The value of encoding byte from ID3v2 frame.
+ * @return Charset name.
+ */
+ private static String getCharsetName(int encodingByte) {
+ switch (encodingByte) {
+ case ID3_TEXT_ENCODING_UTF_16:
+ return "UTF-16";
+ case ID3_TEXT_ENCODING_UTF_16BE:
+ return "UTF-16BE";
+ case ID3_TEXT_ENCODING_UTF_8:
+ return "UTF-8";
+ case ID3_TEXT_ENCODING_ISO_8859_1:
+ default:
+ return "ISO-8859-1";
+ }
+ }
+
+ private static String getFrameId(int majorVersion, int frameId0, int frameId1, int frameId2,
+ int frameId3) {
+ return majorVersion == 2 ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+ : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
+ }
+
+ private static int indexOfEos(byte[] data, int fromIndex, int encoding) {
+ int terminationPos = indexOfZeroByte(data, fromIndex);
+
+ // For single byte encoding charsets, we're done.
+ if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) {
+ return terminationPos;
+ }
+
+ // Otherwise ensure an even index and look for a second zero byte.
+ while (terminationPos < data.length - 1) {
+ if (terminationPos % 2 == 0 && data[terminationPos + 1] == (byte) 0) {
+ return terminationPos;
+ }
+ terminationPos = indexOfZeroByte(data, terminationPos + 1);
+ }
+
+ return data.length;
+ }
+
+ private static int indexOfZeroByte(byte[] data, int fromIndex) {
+ for (int i = fromIndex; i < data.length; i++) {
+ if (data[i] == (byte) 0) {
+ return i;
+ }
+ }
+ return data.length;
+ }
+
+ private static int delimiterLength(int encodingByte) {
+ return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8)
+ ? 1 : 2;
+ }
+
+ /**
+ * Copies the specified range of an array, or returns a zero length array if the range is invalid.
+ *
+ * @param data The array from which to copy.
+ * @param from The start of the range to copy (inclusive).
+ * @param to The end of the range to copy (exclusive).
+ * @return The copied data, or a zero length array if the range is invalid.
+ */
+ private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) {
+ if (to <= from) {
+ // Invalid or zero length range.
+ return Util.EMPTY_BYTE_ARRAY;
+ }
+ return Arrays.copyOfRange(data, from, to);
+ }
+
+ /**
+ * Returns a string obtained by decoding the specified range of {@code data} using the specified
+ * {@code charsetName}. An empty string is returned if the range is invalid.
+ *
+ * @param data The array from which to decode the string.
+ * @param from The start of the range.
+ * @param to The end of the range (exclusive).
+ * @param charsetName The name of the Charset to use.
+ * @return The decoded string, or an empty string if the range is invalid.
+ * @throws UnsupportedEncodingException If the Charset is not supported.
+ */
+ private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName)
+ throws UnsupportedEncodingException {
+ if (to <= from || to > data.length) {
+ return "";
+ }
+ return new String(data, from, to - from, charsetName);
+ }
+
+ private static final class Id3Header {
+
+ private final int majorVersion;
+ private final boolean isUnsynchronized;
+ private final int framesSize;
+
+ public Id3Header(int majorVersion, boolean isUnsynchronized, int framesSize) {
+ this.majorVersion = majorVersion;
+ this.isUnsynchronized = isUnsynchronized;
+ this.framesSize = framesSize;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
new file mode 100644
index 0000000000..f96b5e752c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
@@ -0,0 +1,44 @@
+/*
+ * 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.metadata.id3;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+
+/**
+ * Base class for ID3 frames.
+ */
+public abstract class Id3Frame implements Metadata.Entry {
+
+ /**
+ * The frame ID.
+ */
+ public final String id;
+
+ public Id3Frame(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
new file mode 100644
index 0000000000..ab8ccff343
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/InternalFrame.java
@@ -0,0 +1,97 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/** Internal ID3 frame that is intended for use by the player. */
+public final class InternalFrame extends Id3Frame {
+
+ public static final String ID = "----";
+
+ public final String domain;
+ public final String description;
+ public final String text;
+
+ public InternalFrame(String domain, String description, String text) {
+ super(ID);
+ this.domain = domain;
+ this.description = description;
+ this.text = text;
+ }
+
+ /* package */ InternalFrame(Parcel in) {
+ super(ID);
+ domain = castNonNull(in.readString());
+ description = castNonNull(in.readString());
+ text = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ InternalFrame other = (InternalFrame) obj;
+ return Util.areEqual(description, other.description)
+ && Util.areEqual(domain, other.domain)
+ && Util.areEqual(text, other.text);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (domain != null ? domain.hashCode() : 0);
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (text != null ? text.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": domain=" + domain + ", description=" + description;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(domain);
+ dest.writeString(text);
+ }
+
+ public static final Creator<InternalFrame> CREATOR =
+ new Creator<InternalFrame>() {
+
+ @Override
+ public InternalFrame createFromParcel(Parcel in) {
+ return new InternalFrame(in);
+ }
+
+ @Override
+ public InternalFrame[] newArray(int size) {
+ return new InternalFrame[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java
new file mode 100644
index 0000000000..441235d7c9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/MlltFrame.java
@@ -0,0 +1,114 @@
+/*
+ * 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.metadata.id3;
+
+import android.os.Parcel;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/** MPEG location lookup table frame. */
+public final class MlltFrame extends Id3Frame {
+
+ public static final String ID = "MLLT";
+
+ public final int mpegFramesBetweenReference;
+ public final int bytesBetweenReference;
+ public final int millisecondsBetweenReference;
+ public final int[] bytesDeviations;
+ public final int[] millisecondsDeviations;
+
+ public MlltFrame(
+ int mpegFramesBetweenReference,
+ int bytesBetweenReference,
+ int millisecondsBetweenReference,
+ int[] bytesDeviations,
+ int[] millisecondsDeviations) {
+ super(ID);
+ this.mpegFramesBetweenReference = mpegFramesBetweenReference;
+ this.bytesBetweenReference = bytesBetweenReference;
+ this.millisecondsBetweenReference = millisecondsBetweenReference;
+ this.bytesDeviations = bytesDeviations;
+ this.millisecondsDeviations = millisecondsDeviations;
+ }
+
+ /* package */
+ MlltFrame(Parcel in) {
+ super(ID);
+ this.mpegFramesBetweenReference = in.readInt();
+ this.bytesBetweenReference = in.readInt();
+ this.millisecondsBetweenReference = in.readInt();
+ this.bytesDeviations = Util.castNonNull(in.createIntArray());
+ this.millisecondsDeviations = Util.castNonNull(in.createIntArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ MlltFrame other = (MlltFrame) obj;
+ return mpegFramesBetweenReference == other.mpegFramesBetweenReference
+ && bytesBetweenReference == other.bytesBetweenReference
+ && millisecondsBetweenReference == other.millisecondsBetweenReference
+ && Arrays.equals(bytesDeviations, other.bytesDeviations)
+ && Arrays.equals(millisecondsDeviations, other.millisecondsDeviations);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mpegFramesBetweenReference;
+ result = 31 * result + bytesBetweenReference;
+ result = 31 * result + millisecondsBetweenReference;
+ result = 31 * result + Arrays.hashCode(bytesDeviations);
+ result = 31 * result + Arrays.hashCode(millisecondsDeviations);
+ return result;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mpegFramesBetweenReference);
+ dest.writeInt(bytesBetweenReference);
+ dest.writeInt(millisecondsBetweenReference);
+ dest.writeIntArray(bytesDeviations);
+ dest.writeIntArray(millisecondsDeviations);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<MlltFrame> CREATOR =
+ new Creator<MlltFrame>() {
+
+ @Override
+ public MlltFrame createFromParcel(Parcel in) {
+ return new MlltFrame(in);
+ }
+
+ @Override
+ public MlltFrame[] newArray(int size) {
+ return new MlltFrame[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
new file mode 100644
index 0000000000..248d9996dd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
@@ -0,0 +1,94 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+
+/**
+ * PRIV (Private) ID3 frame.
+ */
+public final class PrivFrame extends Id3Frame {
+
+ public static final String ID = "PRIV";
+
+ public final String owner;
+ public final byte[] privateData;
+
+ public PrivFrame(String owner, byte[] privateData) {
+ super(ID);
+ this.owner = owner;
+ this.privateData = privateData;
+ }
+
+ /* package */ PrivFrame(Parcel in) {
+ super(ID);
+ owner = castNonNull(in.readString());
+ privateData = castNonNull(in.createByteArray());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PrivFrame other = (PrivFrame) obj;
+ return Util.areEqual(owner, other.owner) && Arrays.equals(privateData, other.privateData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (owner != null ? owner.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(privateData);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": owner=" + owner;
+ }
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(owner);
+ dest.writeByteArray(privateData);
+ }
+
+ public static final Parcelable.Creator<PrivFrame> CREATOR = new Parcelable.Creator<PrivFrame>() {
+
+ @Override
+ public PrivFrame createFromParcel(Parcel in) {
+ return new PrivFrame(in);
+ }
+
+ @Override
+ public PrivFrame[] newArray(int size) {
+ return new PrivFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
new file mode 100644
index 0000000000..c0bd36ccf7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
@@ -0,0 +1,96 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Text information ID3 frame.
+ */
+public final class TextInformationFrame extends Id3Frame {
+
+ @Nullable public final String description;
+ public final String value;
+
+ public TextInformationFrame(String id, @Nullable String description, String value) {
+ super(id);
+ this.description = description;
+ this.value = value;
+ }
+
+ /* package */ TextInformationFrame(Parcel in) {
+ super(castNonNull(in.readString()));
+ description = in.readString();
+ value = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TextInformationFrame other = (TextInformationFrame) obj;
+ return id.equals(other.id) && Util.areEqual(description, other.description)
+ && Util.areEqual(value, other.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": description=" + description + ": value=" + value;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(description);
+ dest.writeString(value);
+ }
+
+ public static final Parcelable.Creator<TextInformationFrame> CREATOR =
+ new Parcelable.Creator<TextInformationFrame>() {
+
+ @Override
+ public TextInformationFrame createFromParcel(Parcel in) {
+ return new TextInformationFrame(in);
+ }
+
+ @Override
+ public TextInformationFrame[] newArray(int size) {
+ return new TextInformationFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
new file mode 100644
index 0000000000..ced474960e
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -0,0 +1,96 @@
+/*
+ * 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.metadata.id3;
+
+import static org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Url link ID3 frame.
+ */
+public final class UrlLinkFrame extends Id3Frame {
+
+ @Nullable public final String description;
+ public final String url;
+
+ public UrlLinkFrame(String id, @Nullable String description, String url) {
+ super(id);
+ this.description = description;
+ this.url = url;
+ }
+
+ /* package */ UrlLinkFrame(Parcel in) {
+ super(castNonNull(in.readString()));
+ description = in.readString();
+ url = castNonNull(in.readString());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ UrlLinkFrame other = (UrlLinkFrame) obj;
+ return id.equals(other.id) && Util.areEqual(description, other.description)
+ && Util.areEqual(url, other.url);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + id.hashCode();
+ result = 31 * result + (description != null ? description.hashCode() : 0);
+ result = 31 * result + (url != null ? url.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return id + ": url=" + url;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(id);
+ dest.writeString(description);
+ dest.writeString(url);
+ }
+
+ public static final Parcelable.Creator<UrlLinkFrame> CREATOR =
+ new Parcelable.Creator<UrlLinkFrame>() {
+
+ @Override
+ public UrlLinkFrame createFromParcel(Parcel in) {
+ return new UrlLinkFrame(in);
+ }
+
+ @Override
+ public UrlLinkFrame[] newArray(int size) {
+ return new UrlLinkFrame[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java
new file mode 100644
index 0000000000..87b20161df
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/id3/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.metadata.id3;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java
new file mode 100644
index 0000000000..e5775f7acc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.metadata;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java
new file mode 100644
index 0000000000..3437c8dd73
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java
@@ -0,0 +1,85 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+/**
+ * Represents a private command as defined in SCTE35, Section 9.3.6.
+ */
+public final class PrivateCommand extends SpliceCommand {
+
+ /**
+ * The {@code pts_adjustment} as defined in SCTE35, Section 9.2.
+ */
+ public final long ptsAdjustment;
+ /**
+ * The identifier as defined in SCTE35, Section 9.3.6.
+ */
+ public final long identifier;
+ /**
+ * The private bytes as defined in SCTE35, Section 9.3.6.
+ */
+ public final byte[] commandBytes;
+
+ private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) {
+ this.ptsAdjustment = ptsAdjustment;
+ this.identifier = identifier;
+ this.commandBytes = commandBytes;
+ }
+
+ private PrivateCommand(Parcel in) {
+ ptsAdjustment = in.readLong();
+ identifier = in.readLong();
+ commandBytes = Util.castNonNull(in.createByteArray());
+ }
+
+ /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData,
+ int commandLength, long ptsAdjustment) {
+ long identifier = sectionData.readUnsignedInt();
+ byte[] privateBytes = new byte[commandLength - 4 /* identifier size */];
+ sectionData.readBytes(privateBytes, 0, privateBytes.length);
+ return new PrivateCommand(identifier, privateBytes, ptsAdjustment);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(ptsAdjustment);
+ dest.writeLong(identifier);
+ dest.writeByteArray(commandBytes);
+ }
+
+ public static final Parcelable.Creator<PrivateCommand> CREATOR =
+ new Parcelable.Creator<PrivateCommand>() {
+
+ @Override
+ public PrivateCommand createFromParcel(Parcel in) {
+ return new PrivateCommand(in);
+ }
+
+ @Override
+ public PrivateCommand[] newArray(int size) {
+ return new PrivateCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
new file mode 100644
index 0000000000..866a7ec8bc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
@@ -0,0 +1,37 @@
+/*
+ * 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.metadata.scte35;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+
+/**
+ * Superclass for SCTE35 splice commands.
+ */
+public abstract class SpliceCommand implements Metadata.Entry {
+
+ @Override
+ public String toString() {
+ return "SCTE-35 splice command: type=" + getClass().getSimpleName();
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
new file mode 100644
index 0000000000..a90bddb078
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
@@ -0,0 +1,102 @@
+/*
+ * 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.metadata.scte35;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.nio.ByteBuffer;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * Decodes splice info sections and produces splice commands.
+ */
+public final class SpliceInfoDecoder implements MetadataDecoder {
+
+ private static final int TYPE_SPLICE_NULL = 0x00;
+ private static final int TYPE_SPLICE_SCHEDULE = 0x04;
+ private static final int TYPE_SPLICE_INSERT = 0x05;
+ private static final int TYPE_TIME_SIGNAL = 0x06;
+ private static final int TYPE_PRIVATE_COMMAND = 0xFF;
+
+ private final ParsableByteArray sectionData;
+ private final ParsableBitArray sectionHeader;
+
+ @MonotonicNonNull private TimestampAdjuster timestampAdjuster;
+
+ public SpliceInfoDecoder() {
+ sectionData = new ParsableByteArray();
+ sectionHeader = new ParsableBitArray();
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ public Metadata decode(MetadataInputBuffer inputBuffer) {
+ ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data);
+
+ // Internal timestamps adjustment.
+ if (timestampAdjuster == null
+ || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
+ timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs);
+ timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs);
+ }
+
+ byte[] data = buffer.array();
+ int size = buffer.limit();
+ sectionData.reset(data, size);
+ sectionHeader.reset(data, size);
+ // table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2),
+ // section_length(12), protocol_version(8), encrypted_packet(1), encryption_algorithm(6).
+ sectionHeader.skipBits(39);
+ long ptsAdjustment = sectionHeader.readBits(1);
+ ptsAdjustment = (ptsAdjustment << 32) | sectionHeader.readBits(32);
+ // cw_index(8), tier(12).
+ sectionHeader.skipBits(20);
+ int spliceCommandLength = sectionHeader.readBits(12);
+ int spliceCommandType = sectionHeader.readBits(8);
+ @Nullable SpliceCommand command = null;
+ // Go to the start of the command by skipping all fields up to command_type.
+ sectionData.skipBytes(14);
+ switch (spliceCommandType) {
+ case TYPE_SPLICE_NULL:
+ command = new SpliceNullCommand();
+ break;
+ case TYPE_SPLICE_SCHEDULE:
+ command = SpliceScheduleCommand.parseFromSection(sectionData);
+ break;
+ case TYPE_SPLICE_INSERT:
+ command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment,
+ timestampAdjuster);
+ break;
+ case TYPE_TIME_SIGNAL:
+ command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster);
+ break;
+ case TYPE_PRIVATE_COMMAND:
+ command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ return command == null ? new Metadata() : new Metadata(command);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
new file mode 100644
index 0000000000..5993efb10f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
@@ -0,0 +1,254 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a splice insert command defined in SCTE35, Section 9.3.3.
+ */
+public final class SpliceInsertCommand extends SpliceCommand {
+
+ /**
+ * The splice event id.
+ */
+ public final long spliceEventId;
+ /**
+ * True if the event with id {@link #spliceEventId} has been canceled.
+ */
+ public final boolean spliceEventCancelIndicator;
+ /**
+ * If true, the splice event is an opportunity to exit from the network feed. If false, indicates
+ * an opportunity to return to the network feed.
+ */
+ public final boolean outOfNetworkIndicator;
+ /**
+ * Whether the splice mode is program splice mode, whereby all PIDs/components are to be spliced.
+ * If false, splicing is done per PID/component.
+ */
+ public final boolean programSpliceFlag;
+ /**
+ * Whether splicing should be done at the nearest opportunity. If false, splicing should be done
+ * at the moment indicated by {@link #programSplicePlaybackPositionUs} or
+ * {@link ComponentSplice#componentSplicePlaybackPositionUs}, depending on
+ * {@link #programSpliceFlag}.
+ */
+ public final boolean spliceImmediateFlag;
+ /**
+ * If {@link #programSpliceFlag} is true, the PTS at which the program splice should occur.
+ * {@link C#TIME_UNSET} otherwise.
+ */
+ public final long programSplicePts;
+ /**
+ * Equivalent to {@link #programSplicePts} but in the playback timebase.
+ */
+ public final long programSplicePlaybackPositionUs;
+ /**
+ * If {@link #programSpliceFlag} is false, a non-empty list containing the
+ * {@link ComponentSplice}s. Otherwise, an empty list.
+ */
+ public final List<ComponentSplice> componentSpliceList;
+ /**
+ * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether
+ * {@link #breakDurationUs} should be used to know when to return to the network feed. If
+ * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined.
+ */
+ public final boolean autoReturn;
+ /**
+ * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is present.
+ */
+ public final long breakDurationUs;
+ /**
+ * The unique program id as defined in SCTE35, Section 9.3.3.
+ */
+ public final int uniqueProgramId;
+ /**
+ * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.3.
+ */
+ public final int availNum;
+ /**
+ * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.3.
+ */
+ public final int availsExpected;
+
+ private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator,
+ boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag,
+ long programSplicePts, long programSplicePlaybackPositionUs,
+ List<ComponentSplice> componentSpliceList, boolean autoReturn, long breakDurationUs,
+ int uniqueProgramId, int availNum, int availsExpected) {
+ this.spliceEventId = spliceEventId;
+ this.spliceEventCancelIndicator = spliceEventCancelIndicator;
+ this.outOfNetworkIndicator = outOfNetworkIndicator;
+ this.programSpliceFlag = programSpliceFlag;
+ this.spliceImmediateFlag = spliceImmediateFlag;
+ this.programSplicePts = programSplicePts;
+ this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs;
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ this.autoReturn = autoReturn;
+ this.breakDurationUs = breakDurationUs;
+ this.uniqueProgramId = uniqueProgramId;
+ this.availNum = availNum;
+ this.availsExpected = availsExpected;
+ }
+
+ private SpliceInsertCommand(Parcel in) {
+ spliceEventId = in.readLong();
+ spliceEventCancelIndicator = in.readByte() == 1;
+ outOfNetworkIndicator = in.readByte() == 1;
+ programSpliceFlag = in.readByte() == 1;
+ spliceImmediateFlag = in.readByte() == 1;
+ programSplicePts = in.readLong();
+ programSplicePlaybackPositionUs = in.readLong();
+ int componentSpliceListSize = in.readInt();
+ List<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListSize);
+ for (int i = 0; i < componentSpliceListSize; i++) {
+ componentSpliceList.add(ComponentSplice.createFromParcel(in));
+ }
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ autoReturn = in.readByte() == 1;
+ breakDurationUs = in.readLong();
+ uniqueProgramId = in.readInt();
+ availNum = in.readInt();
+ availsExpected = in.readInt();
+ }
+
+ /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData,
+ long ptsAdjustment, TimestampAdjuster timestampAdjuster) {
+ long spliceEventId = sectionData.readUnsignedInt();
+ // splice_event_cancel_indicator(1), reserved(7).
+ boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;
+ boolean outOfNetworkIndicator = false;
+ boolean programSpliceFlag = false;
+ boolean spliceImmediateFlag = false;
+ long programSplicePts = C.TIME_UNSET;
+ List<ComponentSplice> componentSplices = Collections.emptyList();
+ int uniqueProgramId = 0;
+ int availNum = 0;
+ int availsExpected = 0;
+ boolean autoReturn = false;
+ long breakDurationUs = C.TIME_UNSET;
+ if (!spliceEventCancelIndicator) {
+ int headerByte = sectionData.readUnsignedByte();
+ outOfNetworkIndicator = (headerByte & 0x80) != 0;
+ programSpliceFlag = (headerByte & 0x40) != 0;
+ boolean durationFlag = (headerByte & 0x20) != 0;
+ spliceImmediateFlag = (headerByte & 0x10) != 0;
+ if (programSpliceFlag && !spliceImmediateFlag) {
+ programSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);
+ }
+ if (!programSpliceFlag) {
+ int componentCount = sectionData.readUnsignedByte();
+ componentSplices = new ArrayList<>(componentCount);
+ for (int i = 0; i < componentCount; i++) {
+ int componentTag = sectionData.readUnsignedByte();
+ long componentSplicePts = C.TIME_UNSET;
+ if (!spliceImmediateFlag) {
+ componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);
+ }
+ componentSplices.add(new ComponentSplice(componentTag, componentSplicePts,
+ timestampAdjuster.adjustTsTimestamp(componentSplicePts)));
+ }
+ }
+ if (durationFlag) {
+ long firstByte = sectionData.readUnsignedByte();
+ autoReturn = (firstByte & 0x80) != 0;
+ long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+ breakDurationUs = breakDuration90khz * 1000 / 90;
+ }
+ uniqueProgramId = sectionData.readUnsignedShort();
+ availNum = sectionData.readUnsignedByte();
+ availsExpected = sectionData.readUnsignedByte();
+ }
+ return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+ programSpliceFlag, spliceImmediateFlag, programSplicePts,
+ timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn,
+ breakDurationUs, uniqueProgramId, availNum, availsExpected);
+ }
+
+ /**
+ * Holds splicing information for specific splice insert command components.
+ */
+ public static final class ComponentSplice {
+
+ public final int componentTag;
+ public final long componentSplicePts;
+ public final long componentSplicePlaybackPositionUs;
+
+ private ComponentSplice(int componentTag, long componentSplicePts,
+ long componentSplicePlaybackPositionUs) {
+ this.componentTag = componentTag;
+ this.componentSplicePts = componentSplicePts;
+ this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs;
+ }
+
+ public void writeToParcel(Parcel dest) {
+ dest.writeInt(componentTag);
+ dest.writeLong(componentSplicePts);
+ dest.writeLong(componentSplicePlaybackPositionUs);
+ }
+
+ public static ComponentSplice createFromParcel(Parcel in) {
+ return new ComponentSplice(in.readInt(), in.readLong(), in.readLong());
+ }
+
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(spliceEventId);
+ dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0));
+ dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0));
+ dest.writeByte((byte) (programSpliceFlag ? 1 : 0));
+ dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0));
+ dest.writeLong(programSplicePts);
+ dest.writeLong(programSplicePlaybackPositionUs);
+ int componentSpliceListSize = componentSpliceList.size();
+ dest.writeInt(componentSpliceListSize);
+ for (int i = 0; i < componentSpliceListSize; i++) {
+ componentSpliceList.get(i).writeToParcel(dest);
+ }
+ dest.writeByte((byte) (autoReturn ? 1 : 0));
+ dest.writeLong(breakDurationUs);
+ dest.writeInt(uniqueProgramId);
+ dest.writeInt(availNum);
+ dest.writeInt(availsExpected);
+ }
+
+ public static final Parcelable.Creator<SpliceInsertCommand> CREATOR =
+ new Parcelable.Creator<SpliceInsertCommand>() {
+
+ @Override
+ public SpliceInsertCommand createFromParcel(Parcel in) {
+ return new SpliceInsertCommand(in);
+ }
+
+ @Override
+ public SpliceInsertCommand[] newArray(int size) {
+ return new SpliceInsertCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java
new file mode 100644
index 0000000000..afc88bbeab
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java
@@ -0,0 +1,47 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+
+/**
+ * Represents a splice null command as defined in SCTE35, Section 9.3.1.
+ */
+public final class SpliceNullCommand extends SpliceCommand {
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // Do nothing.
+ }
+
+ public static final Creator<SpliceNullCommand> CREATOR =
+ new Creator<SpliceNullCommand>() {
+
+ @Override
+ public SpliceNullCommand createFromParcel(Parcel in) {
+ return new SpliceNullCommand();
+ }
+
+ @Override
+ public SpliceNullCommand[] newArray(int size) {
+ return new SpliceNullCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
new file mode 100644
index 0000000000..e1d369bc87
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
@@ -0,0 +1,270 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a splice schedule command as defined in SCTE35, Section 9.3.2.
+ */
+public final class SpliceScheduleCommand extends SpliceCommand {
+
+ /**
+ * Represents a splice event as contained in a {@link SpliceScheduleCommand}.
+ */
+ public static final class Event {
+
+ /**
+ * The splice event id.
+ */
+ public final long spliceEventId;
+ /**
+ * True if the event with id {@link #spliceEventId} has been canceled.
+ */
+ public final boolean spliceEventCancelIndicator;
+ /**
+ * If true, the splice event is an opportunity to exit from the network feed. If false,
+ * indicates an opportunity to return to the network feed.
+ */
+ public final boolean outOfNetworkIndicator;
+ /**
+ * Whether the splice mode is program splice mode, whereby all PIDs/components are to be
+ * spliced. If false, splicing is done per PID/component.
+ */
+ public final boolean programSpliceFlag;
+ /**
+ * Represents the time of the signaled splice event as the number of seconds since 00 hours UTC,
+ * January 6th, 1980, with the count of intervening leap seconds included.
+ */
+ public final long utcSpliceTime;
+ /**
+ * If {@link #programSpliceFlag} is false, a non-empty list containing the
+ * {@link ComponentSplice}s. Otherwise, an empty list.
+ */
+ public final List<ComponentSplice> componentSpliceList;
+ /**
+ * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether
+ * {@link #breakDurationUs} should be used to know when to return to the network feed. If
+ * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined.
+ */
+ public final boolean autoReturn;
+ /**
+ * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is
+ * present.
+ */
+ public final long breakDurationUs;
+ /**
+ * The unique program id as defined in SCTE35, Section 9.3.2.
+ */
+ public final int uniqueProgramId;
+ /**
+ * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.2.
+ */
+ public final int availNum;
+ /**
+ * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.2.
+ */
+ public final int availsExpected;
+
+ private Event(long spliceEventId, boolean spliceEventCancelIndicator,
+ boolean outOfNetworkIndicator, boolean programSpliceFlag,
+ List<ComponentSplice> componentSpliceList, long utcSpliceTime, boolean autoReturn,
+ long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) {
+ this.spliceEventId = spliceEventId;
+ this.spliceEventCancelIndicator = spliceEventCancelIndicator;
+ this.outOfNetworkIndicator = outOfNetworkIndicator;
+ this.programSpliceFlag = programSpliceFlag;
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ this.utcSpliceTime = utcSpliceTime;
+ this.autoReturn = autoReturn;
+ this.breakDurationUs = breakDurationUs;
+ this.uniqueProgramId = uniqueProgramId;
+ this.availNum = availNum;
+ this.availsExpected = availsExpected;
+ }
+
+ private Event(Parcel in) {
+ this.spliceEventId = in.readLong();
+ this.spliceEventCancelIndicator = in.readByte() == 1;
+ this.outOfNetworkIndicator = in.readByte() == 1;
+ this.programSpliceFlag = in.readByte() == 1;
+ int componentSpliceListLength = in.readInt();
+ ArrayList<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListLength);
+ for (int i = 0; i < componentSpliceListLength; i++) {
+ componentSpliceList.add(ComponentSplice.createFromParcel(in));
+ }
+ this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
+ this.utcSpliceTime = in.readLong();
+ this.autoReturn = in.readByte() == 1;
+ this.breakDurationUs = in.readLong();
+ this.uniqueProgramId = in.readInt();
+ this.availNum = in.readInt();
+ this.availsExpected = in.readInt();
+ }
+
+ private static Event parseFromSection(ParsableByteArray sectionData) {
+ long spliceEventId = sectionData.readUnsignedInt();
+ // splice_event_cancel_indicator(1), reserved(7).
+ boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;
+ boolean outOfNetworkIndicator = false;
+ boolean programSpliceFlag = false;
+ long utcSpliceTime = C.TIME_UNSET;
+ ArrayList<ComponentSplice> componentSplices = new ArrayList<>();
+ int uniqueProgramId = 0;
+ int availNum = 0;
+ int availsExpected = 0;
+ boolean autoReturn = false;
+ long breakDurationUs = C.TIME_UNSET;
+ if (!spliceEventCancelIndicator) {
+ int headerByte = sectionData.readUnsignedByte();
+ outOfNetworkIndicator = (headerByte & 0x80) != 0;
+ programSpliceFlag = (headerByte & 0x40) != 0;
+ boolean durationFlag = (headerByte & 0x20) != 0;
+ if (programSpliceFlag) {
+ utcSpliceTime = sectionData.readUnsignedInt();
+ }
+ if (!programSpliceFlag) {
+ int componentCount = sectionData.readUnsignedByte();
+ componentSplices = new ArrayList<>(componentCount);
+ for (int i = 0; i < componentCount; i++) {
+ int componentTag = sectionData.readUnsignedByte();
+ long componentUtcSpliceTime = sectionData.readUnsignedInt();
+ componentSplices.add(new ComponentSplice(componentTag, componentUtcSpliceTime));
+ }
+ }
+ if (durationFlag) {
+ long firstByte = sectionData.readUnsignedByte();
+ autoReturn = (firstByte & 0x80) != 0;
+ long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+ breakDurationUs = breakDuration90khz * 1000 / 90;
+ }
+ uniqueProgramId = sectionData.readUnsignedShort();
+ availNum = sectionData.readUnsignedByte();
+ availsExpected = sectionData.readUnsignedByte();
+ }
+ return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+ programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, breakDurationUs,
+ uniqueProgramId, availNum, availsExpected);
+ }
+
+ private void writeToParcel(Parcel dest) {
+ dest.writeLong(spliceEventId);
+ dest.writeByte((byte) (spliceEventCancelIndicator ? 1 : 0));
+ dest.writeByte((byte) (outOfNetworkIndicator ? 1 : 0));
+ dest.writeByte((byte) (programSpliceFlag ? 1 : 0));
+ int componentSpliceListSize = componentSpliceList.size();
+ dest.writeInt(componentSpliceListSize);
+ for (int i = 0; i < componentSpliceListSize; i++) {
+ componentSpliceList.get(i).writeToParcel(dest);
+ }
+ dest.writeLong(utcSpliceTime);
+ dest.writeByte((byte) (autoReturn ? 1 : 0));
+ dest.writeLong(breakDurationUs);
+ dest.writeInt(uniqueProgramId);
+ dest.writeInt(availNum);
+ dest.writeInt(availsExpected);
+ }
+
+ private static Event createFromParcel(Parcel in) {
+ return new Event(in);
+ }
+
+ }
+
+ /**
+ * Holds splicing information for specific splice schedule command components.
+ */
+ public static final class ComponentSplice {
+
+ public final int componentTag;
+ public final long utcSpliceTime;
+
+ private ComponentSplice(int componentTag, long utcSpliceTime) {
+ this.componentTag = componentTag;
+ this.utcSpliceTime = utcSpliceTime;
+ }
+
+ private static ComponentSplice createFromParcel(Parcel in) {
+ return new ComponentSplice(in.readInt(), in.readLong());
+ }
+
+ private void writeToParcel(Parcel dest) {
+ dest.writeInt(componentTag);
+ dest.writeLong(utcSpliceTime);
+ }
+
+ }
+
+ /**
+ * The list of scheduled events.
+ */
+ public final List<Event> events;
+
+ private SpliceScheduleCommand(List<Event> events) {
+ this.events = Collections.unmodifiableList(events);
+ }
+
+ private SpliceScheduleCommand(Parcel in) {
+ int eventsSize = in.readInt();
+ ArrayList<Event> events = new ArrayList<>(eventsSize);
+ for (int i = 0; i < eventsSize; i++) {
+ events.add(Event.createFromParcel(in));
+ }
+ this.events = Collections.unmodifiableList(events);
+ }
+
+ /* package */ static SpliceScheduleCommand parseFromSection(ParsableByteArray sectionData) {
+ int spliceCount = sectionData.readUnsignedByte();
+ ArrayList<Event> events = new ArrayList<>(spliceCount);
+ for (int i = 0; i < spliceCount; i++) {
+ events.add(Event.parseFromSection(sectionData));
+ }
+ return new SpliceScheduleCommand(events);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ int eventsSize = events.size();
+ dest.writeInt(eventsSize);
+ for (int i = 0; i < eventsSize; i++) {
+ events.get(i).writeToParcel(dest);
+ }
+ }
+
+ public static final Parcelable.Creator<SpliceScheduleCommand> CREATOR =
+ new Parcelable.Creator<SpliceScheduleCommand>() {
+
+ @Override
+ public SpliceScheduleCommand createFromParcel(Parcel in) {
+ return new SpliceScheduleCommand(in);
+ }
+
+ @Override
+ public SpliceScheduleCommand[] newArray(int size) {
+ return new SpliceScheduleCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java
new file mode 100644
index 0000000000..f50a029f1b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java
@@ -0,0 +1,93 @@
+/*
+ * 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.metadata.scte35;
+
+import android.os.Parcel;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * Represents a time signal command as defined in SCTE35, Section 9.3.4.
+ */
+public final class TimeSignalCommand extends SpliceCommand {
+
+ /**
+ * A PTS value, as defined in SCTE35, Section 9.3.4.
+ */
+ public final long ptsTime;
+ /**
+ * Equivalent to {@link #ptsTime} but in the playback timebase.
+ */
+ public final long playbackPositionUs;
+
+ private TimeSignalCommand(long ptsTime, long playbackPositionUs) {
+ this.ptsTime = ptsTime;
+ this.playbackPositionUs = playbackPositionUs;
+ }
+
+ /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData,
+ long ptsAdjustment, TimestampAdjuster timestampAdjuster) {
+ long ptsTime = parseSpliceTime(sectionData, ptsAdjustment);
+ long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime);
+ return new TimeSignalCommand(ptsTime, playbackPositionUs);
+ }
+
+ /**
+ * Parses pts_time from splice_time(), defined in Section 9.4.1. Returns {@link C#TIME_UNSET}, if
+ * time_specified_flag is false.
+ *
+ * @param sectionData The section data from which the pts_time is parsed.
+ * @param ptsAdjustment The pts adjustment provided by the splice info section header.
+ * @return The pts_time defined by splice_time(), or {@link C#TIME_UNSET}, if time_specified_flag
+ * is false.
+ */
+ /* package */ static long parseSpliceTime(ParsableByteArray sectionData, long ptsAdjustment) {
+ long firstByte = sectionData.readUnsignedByte();
+ long ptsTime = C.TIME_UNSET;
+ if ((firstByte & 0x80) != 0 /* time_specified_flag */) {
+ // See SCTE35 9.2.1 for more information about pts adjustment.
+ ptsTime = (firstByte & 0x01) << 32 | sectionData.readUnsignedInt();
+ ptsTime += ptsAdjustment;
+ ptsTime &= 0x1FFFFFFFFL;
+ }
+ return ptsTime;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(ptsTime);
+ dest.writeLong(playbackPositionUs);
+ }
+
+ public static final Creator<TimeSignalCommand> CREATOR =
+ new Creator<TimeSignalCommand>() {
+
+ @Override
+ public TimeSignalCommand createFromParcel(Parcel in) {
+ return new TimeSignalCommand(in.readLong(), in.readLong());
+ }
+
+ @Override
+ public TimeSignalCommand[] newArray(int size) {
+ return new TimeSignalCommand[size];
+ }
+
+ };
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java
new file mode 100644
index 0000000000..17ce76bb9f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata/scte35/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.metadata.scte35;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;