diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/metadata')
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; |